diff --git a/README.md b/README.md index e3fe8d7..e92ea63 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ In case if your homeserver forbids such method of registration, you can try regi Eventually, your bot will hit upon a rate limit. Even if there are only 2 users (beleive me, I tested!) To solve this problem, you can override rate limits for your bot if you are an administrator of a homeserver that hosts your bot OR in good relationships with one! ;) Anyway, you should login to your synape database and type -``INSERT INTO ratelimit_override VALUES ("@name-of-your-bot:name-of-your-home.server", 0, 0);`` +``INSERT INTO ratelimit_override VALUES ('@name-of-your-bot:name-of-your-home.server', 0, 0);`` That will do the trick. ## Actual programm ### Docker diff --git a/scheme.psql b/scheme.psql index 58e0b3c..d34cbc0 100644 --- a/scheme.psql +++ b/scheme.psql @@ -46,10 +46,13 @@ CREATE TABLE IF NOT EXISTS cities ( country VARCHAR(64) ); +CREATE TABLE IF NOT EXISTS read_profiles ( + sender VARCHAR(64), + recipient VARCHAR(64) +); + CREATE EXTENSION IF NOT EXISTS fuzzystrmatch SCHEMA public; - - CREATE OR REPLACE FUNCTION deg2rad(double precision) RETURNS double precision AS 'SELECT $1 * 0.01745329' LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; -- lat1, lng1, lat2, lng2, range CREATE OR REPLACE FUNCTION check_distance(double precision, double precision, double precision, double precision, double precision) RETURNS BOOLEAN AS ' diff --git a/src/db.js b/src/db.js index 2c2409e..95c8e4c 100644 --- a/src/db.js +++ b/src/db.js @@ -1,6 +1,6 @@ import pg from 'pg'; import fs from 'fs'; -import { convertMsgType, logError } from './utils.js'; +import { logError } from './utils.js'; import { exec } from 'child_process'; const { Client } = pg; @@ -29,219 +29,4 @@ const getClient = async () => { return client; }; -export const db = await getClient(); - - -const getAmountOfUserPictures = async (roomId) => { - return (await db.query("SELECT COUNT(*) FROM media WHERE owner = $1 AND purpose = 'p'", [roomId])).rows[0].count; -}; - -const getCurrentUserAction = async (roomId) => { - return (await db.query('SELECT current_action FROM users WHERE room_id = $1', [roomId])).rows[0].current_action; -}; - -const setUserState = async (roomId, state) => { - await db.query("UPDATE users SET current_action = $1 WHERE room_id = $2", [state, roomId]); -}; - -const appendUserPictures = async (roomId, mxc, type) => { - await db.query("INSERT INTO media (owner, type, purpose, url) VALUES ($1, $2, 'p', $3)", [roomId, type, mxc]); -}; - -const fullEraseUser = async (roomId) => { - await eraseUser(roomId) - await eraseUserLikes(roomId); - await eraseUserMedia(roomId); - await eraseUserMessages(roomId); -} - -const eraseUser = async (roomId) => { - await db.query("DELETE FROM users WHERE room_id = $1", [roomId]); -}; - -const eraseUserLikes = async (roomId) => { - await db.query("DELETE FROM likes WHERE sender = $1 OR recipient = $1", [roomId]); -}; - -const eraseUserMedia = async (roomId) => { - await db.query("DELETE FROM media WHERE owner = $1", [roomId]); -}; - -const eraseUserMessages = async (roomid) => { - await db.query("DELETE FROM messages WHERE sender = $1 OR recipient = $1", [roomId]); -} - -const eraseUserPfp = async (roomId) => { - await db.query("DELETE FROM media WHERE owner = $1 AND purpose = 'p'", [roomId]); -} - -const selectProfilesForUser = async (roomId) => { - const { myrange, myinterest, mysex, myage, mycity } = (await db.query(`SELECT range AS myrange, - interest AS myinterest, - sex AS mysex, - age AS myage, - location AS mycity - FROM users WHERE room_id = $1`, [roomId]) - ).rows[0]; - const { lat, lng } = (await db.query("SELECT lat, lng FROM cities WHERE ID = $1", [mycity])).rows[0]; - //Selecting profiles other than user's and fitting their needs - let user = (await db.query(`SELECT - room_id, name, age, sex, description, location, range FROM users - WHERE - age::numeric <@ ANY(ARRAY[numrange($1 - 2, $1 + 2)]) - AND room_id != $2 - AND ${myinterest !== 'b' ? "sex = $3" : "$3 = $3 AND $4 = $4 AND $5 = $5 AND $6 = $6 AND $7 = $7"} - AND ${myrange !== 0 ? - `location = ANY(ARRAY(SELECT ID - FROM cities - WHERE - check_distance($6::double precision, $7::double precision, lat, lng, $5::double precision) - AND (check_distance($6::double precision, $7::double precision, lat, lng, range) OR range = 0) - ))` - : - `range = 0 OR - check_distance($6::double precision, $7::double precision, (SELECT lat FROM cities WHERE ID = location), (SELECT lng FROM cities WHERE ID = location), range)` - } - AND (interest = $4 OR interest = 'b') - ORDER BY RANDOM() - LIMIT 1`, [myage, roomId, myinterest, mysex, myrange, lat, lng]) - ).rows[0]; - - if (!user) return null; - let media = await getUserProfilePictures(user.room_id); - - user.media = media; - return user; -}; - -const setUserCurrentlyViewingProfile = async (roomId, anotherRoomId) => { - await db.query("UPDATE users SET currently_viewing = $1 WHERE room_id = $2", [anotherRoomId, roomId]); -}; - -const getUserCurrentlyViewingProfile = async (roomId) => { - return (await db.query("SELECT currently_viewing FROM users WHERE room_id = $1", [roomId])).rows[0].currently_viewing; -}; - -//Newlike is a room id of a user who was liked_profiles -const appendUserLikes = async (roomId, newLike) => { - //We need to delete likes that was read from both, so we can add a new (the same) like. - await db.query(`DELETE FROM likes l1 - USING likes l2 - WHERE l1.sender = l2.recipient - AND l1.recipient = l2.sender - AND l1.read = TRUE - AND l2.read = TRUE`); - - await db.query("INSERT INTO likes (sender, recipient) VALUES ($1, $2) ON CONFLICT DO NOTHING", [roomId, newLike]); -}; - -const getUserLikes = async (roomId) => { - return (await db.query("SELECT recipient FROM likes WHERE sender = $1", [roomId])).rows[0].recipient; -}; - -const checkForMutualLike = async (roomId1, roomId2) => { - return (await db.query(`SELECT COUNT(*) > 0 AS value - FROM likes l1 - JOIN likes l2 ON l1.sender = l2.recipient AND l1.recipient = l2.sender - WHERE (l1.read = FALSE OR l2.read = FALSE) - AND l1.sender = $1 - AND l1.recipient = $2`, [roomId1, roomId2])).rows[0].value; -}; - -const getProfileInfo = async (roomId) => { - let user = (await db.query("SELECT mx_id, room_id, name, age, sex, description, location FROM users WHERE room_id = $1", [roomId])).rows[0]; - if (!user) return null; - let media = await getUserProfilePictures(user.room_id); - user.media = media; - return user; -}; - -const getUserProfilePictures = async (roomId) => { - return (await db.query("SELECT url, type FROM media WHERE owner = $1 AND purpose = 'p'", [roomId])).rows; -}; - -const getAllLikesForUser = async (roomId) => { - return (await db.query("SELECT sender FROM likes WHERE recipient = $1 AND read = FALSE", [roomId])).rows; -}; - -const markLikeAsRead = async (roomId, recipient,) => { - await db.query("UPDATE likes SET read = TRUE WHERE sender = $1 AND recipient = $2", [roomId, recipient]); -}; - -const uploadMediaAsMessage = async (roomId, type, mxc) => { - await db.query("INSERT INTO media(owner, type, purpose, url) VALUES ($1, $2, 'm', $3)", [roomId, convertMsgType(type), mxc]); -}; - -const insertMessageIntoDB = async (roomId, recipient, type, content) => { - let rowAmount = (await db.query("INSERT INTO messages (sender, recipient, type, content) VALUES ($1, $2, $3, $4) ON CONFLICT(sender, recipient) DO NOTHING RETURNING *", [roomId, recipient, convertMsgType(type), content])).rowCount; - return rowAmount == 1; -}; - -const getUnreadMessages = async (roomId) => { - return (await db.query("SELECT (SELECT mx_id FROM users WHERE room_id = sender LIMIT 1), sender, type, content FROM messages WHERE recipient = $1 AND read = FALSE", [roomId])).rows -}; - -const markMessageAsRead = async (roomId, recipient) => { - return (await db.query("UPDATE messages SET read = TRUE WHERE sender = $1 AND recipient = $2", [roomId, recipient])); -} - -const setUserLanguage = async (roomId, language) => { - await db.query("UPDATE users SET language = $1 WHERE room_id = $2", [language, roomId]); -} - -const getUserLanguage = async (roomId) => { - return (await db.query("SELECT language FROM users WHERE room_id = $1", [roomId])).rows[0].language; -} - -const findCity = async (name) => { - return (await db.query(`SELECT ID, name, lat, lng, country, levenshtein(name, $1) AS similarity - FROM cities - ORDER BY similarity ASC - LIMIT 5`, [name]) - ).rows; -} - -const getCityNameByID = async (id) => { - return (await db.query(`SELECT name FROM cities WHERE ID = $1`, [id])).rows[0].name; -} - -const getCountryNameByID = async (id) => { - return (await db.query(`SELECT country FROM cities WHERE ID = $1`, [id])).rows[0].country; -} - -const doesUserExist = async (roomId) => { - return (await db.query("SELECT EXISTS (SELECT * FROM users WHERE room_id = $1)", [roomId])).rows[0].exists; -} - -const createUser = async (mx_id, roomId) => { - await db.query("INSERT INTO users(mx_id, room_id, current_action) VALUES ($1, $2, $3)", [mx_id, roomId, "wait_start"]); -} - -export { - fullEraseUser, - appendUserPictures, - setUserState, - getCurrentUserAction, - getAmountOfUserPictures, - selectProfilesForUser, - setUserCurrentlyViewingProfile, - getUserCurrentlyViewingProfile, - appendUserLikes, - getUserLikes, - checkForMutualLike, - getProfileInfo, - getAllLikesForUser, - markLikeAsRead, - uploadMediaAsMessage, - insertMessageIntoDB, - getUnreadMessages, - markMessageAsRead, - setUserLanguage, - getUserLanguage, - findCity, - getCityNameByID, - getCountryNameByID, - doesUserExist, - createUser, - eraseUserPfp -}; \ No newline at end of file +export const db = await getClient(); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 564151d..8a38463 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,5 @@ import { - EncryptionAlgorithm, MatrixClient, - MessageEvent, RustSdkCryptoStorageProvider, SimpleFsStorageProvider, } from "matrix-bot-sdk"; @@ -13,307 +11,368 @@ import { logError, logInfo, readConfig, - uploadMediaFromEvent -} from './utils.js'; - -import { - appendUserLikes, - appendUserPictures, - checkForMutualLike, - fullEraseUser, - getAmountOfUserPictures, - getCurrentUserAction, - getUnreadMessages, - getUserCurrentlyViewingProfile, - insertMessageIntoDB, - markMessageAsRead, - setUserState, + uploadMediaFromEvent, uploadMediaAsMessage, - setUserLanguage, - getUserLanguage, findCity, - doesUserExist, - createUser, - eraseUserPfp, - getProfileInfo -} from './db.js'; - -import { - processRequest, - showRandomProfileToUser, - showNewLikes, - wait_start, - range, - interest, - pictures, - loc, - age, - sex, - showProfileToUser, - showProfile -} from "./interactions.js"; - -import { db } from "./db.js"; + insertMessageIntoDB +} from './utils.js'; import fs from "fs"; import { I18n } from "i18n-js"; -const config = readConfig(); +import { User } from "./user.js"; -const homeserverUrl = config.homeserverURL; -const accessToken = config.token; -const maxAmountOfPhotoesPerUser = config.maxAmountOfPhotoesPerUser; +const config = readConfig(); const crypto = new RustSdkCryptoStorageProvider("./bot_data/encryption_bot_sled", StoreType.Sled); const storage = new SimpleFsStorageProvider("./bot_data/bot.json"); -const client = new MatrixClient(homeserverUrl, accessToken, storage, crypto); +const client = new MatrixClient(config.homeserverURL, config.token, storage, crypto); client.on("room.message", async (roomId, event) => { - try { - if (event.sender === await client.getUserId()) return; + if (event.sender === await client.getUserId()) return; + if (event.content?.msgtype !== 'm.text' && event.content?.msgtype !== 'm.image' && event.content?.msgtype !== 'm.video') return; - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); + const i18n = new I18n({ + en: JSON.parse(fs.readFileSync("./translations/en.json")), + ru: JSON.parse(fs.readFileSync("./translations/ru.json")) + }); - if (!await doesUserExist(roomId)) { - i18n.locale = 'en' - await client.sendText(roomId, i18n.t(["errors", "usernotexists"])); - i18n.locale = 'ru' - await client.sendText(roomId, i18n.t(["errors", "usernotexists"])); - let mx_id = event.sender; - await createUser(mx_id, roomId); - return; - } + let user = new User(roomId); - if (event.content?.msgtype !== 'm.text' && event.content?.msgtype !== 'm.image' && event.content?.msgtype !== 'm.video') return; + if (!await user.exists()) { + i18n.locale = 'en' + await client.sendText(roomId, i18n.t(["errors", "usernotexists"])); + i18n.locale = 'ru' + await client.sendText(roomId, i18n.t(["errors", "usernotexists"])); + let mxid = event.sender; - let current_action = await getCurrentUserAction(roomId); - let answer = event.content.body; - let msgtype = event.content.msgtype + await user.init(mxid); + return; + } - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; + const current_action = await user.getCurrentAction(); - switch (current_action) { - case "wait_start": - await wait_start(client, roomId, current_action, answer, false); - break; - case "wait_start_u": - await wait_start(client, roomId, current_action, answer, true); - break; + const language = await user.getLanguage(); + i18n.locale = language; - case "location": - await loc(client, roomId, current_action, answer, false); - break; - case "location_u": - await loc(client, roomId, current_action, answer, true); - break; + let answer = event.content.body; + let msgtype = event.content.msgtype - case "range": - await range(client, roomId, current_action, answer, false); - break; - case "range_u": - await range(client, roomId, current_action, answer, true); - break; - case "name": - await processRequest(client, roomId, current_action, answer, 'age'); - break; - case "name_u": - await processRequest(client, roomId, current_action, answer, 'menu'); - break; + // // Updating actions are "-u" + let action = current_action.split("-") - case "age": - await age(client, roomId, current_action, answer, false); - break; - case "age_u": - await age(client, roomId, current_action, answer, true); - break; + switch (action[0]) { + case "wait_start": + let command = answer.split(" "); + if (command[0] !== "!start") return; + if (command[1] !== "ru" && command[1] !== "en") return; - case "sex": - await sex(client, roomId, current_action, answer, false); - break; - case "sex_u": - await sex(client, roomId, current_action, answer, true); - break; + await user.set("language", command[1]); + await user.setState("location"); + await client.sendText(roomId, i18n.t(["setup", "location"])); + break; + case "location": + let number = parseInt(answer); + if (!number) { + const cities = await findCity(answer); + await client.sendText(roomId, i18n.t(["setup", "choosecity"], { + number1: cities[0].id, + name1: cities[0].name, + country1: cities[0].country, + lat1: cities[0].lat, + lng1: cities[0].lng, - case "interest": - await interest(client, roomId, current_action, answer, false); - break; - case "interest_u": - await interest(client, roomId, current_action, answer, true); - break; + number2: cities[1].id, + name2: cities[1].name, + country2: cities[1].country, + lat2: cities[1].lat, + lng2: cities[1].lng, - case "description": - await processRequest(client, roomId, current_action, answer, 'pictures'); - break; - case "description_u": - await processRequest(client, roomId, current_action, answer, 'menu'); - break; + number3: cities[2].id, + name3: cities[2].name, + country3: cities[2].country, + lat3: cities[2].lat, + lng3: cities[2].lng, - case "pictures": - pictures(client, roomId, current_action, event, false); - break; - case "pictures_u": - pictures(client, roomId, current_action, event, true); - break; + number4: cities[3].id, + name4: cities[3].name, + country4: cities[3].country, + lat4: cities[3].lat, + lng4: cities[3].lng, - case "language_u": - if (answer !== "ru" && answer !== "en") return; - await setUserLanguage(roomId, answer); - i18n.locale = answer; - await setUserState(roomId, "menu"); - await client.sendText(roomId, i18n.t(["general", "menu"])); - break; - - case "view_profiles": - if (answer == '👍️' || answer == '❤️' || answer == '1') { - let currently_viewing = await getUserCurrentlyViewingProfile(roomId); - await appendUserLikes(roomId, currently_viewing); - let value = await checkForMutualLike(roomId, currently_viewing); - if (value) { - await client.sendText(currently_viewing, i18n.t(["general", "newlikes"])); - await client.sendText(roomId, i18n.t(["general", "newlikes"])); - } - } else if (answer == '💌' || answer == '3') { - await setUserState(roomId, 'send_message'); - await client.sendText(roomId, i18n.t(["general", "message"])); - return; - } else if (answer == '🏠️' || answer == '4') { - await client.sendText(roomId, i18n.t(["general", "menu"])); - await setUserState(roomId, 'menu'); - return; - } - await showRandomProfileToUser(client, roomId); - break; - case 'send_message': - let recipient = await getUserCurrentlyViewingProfile(roomId); - let content; - if (msgtype == "m.image" || msgtype == "m.video") { - let mxc = await uploadMediaFromEvent(client, event); - await uploadMediaAsMessage(roomId, msgtype, mxc); - content = mxc; - } else { - content = answer; - } - if (await insertMessageIntoDB(roomId, recipient, msgtype, content)) { - await client.sendText(recipient, i18n.t(["general", "newmessage"])); - await client.sendText(roomId, i18n.t(["general", "messagesent"])); - } else { - await client.sendText(roomId, i18n.t(["errors", "alreadymessaged"])); - } - await setUserState(roomId, 'view_profiles'); - await showRandomProfileToUser(client, roomId); - break; - case 'menu': - switch (answer) { - case '0': - let profile = await getProfileInfo(roomId); - await showProfile(client, roomId, profile); - await client.sendText(roomId, i18n.t(["general", "menu"])); - break; - case '1': - await setUserState(roomId, 'view_profiles'); - await showRandomProfileToUser(client, roomId); - break; - case '2': - await showNewLikes(client, roomId); - await client.sendText(roomId, i18n.t(["general", "menu"])); - break; - case '3': - let unreadMessages = await getUnreadMessages(roomId); - if (!unreadMessages || unreadMessages.length == 0) { - await client.sendText(roomId, i18n.t(["general", "nonewmessages"])); - return; - } - await client.sendText(roomId, i18n.t(["general", "msg"])); - - for (let message of unreadMessages) { - await client.sendText(roomId, i18n.t(["general", "showmessage"], { user: message.mx_id })); - - if (message.type == "t") { - await client.sendText(roomId, message.content) - } else if (message.type == "p" || message.type == "v") { - let msgtype; - - if (message.type == "p") { - msgtype = "m.image"; - } else if (message.type == "v") { - msgtype = "m.video"; - } - - await client.sendMessage(roomId, { - msgtype: msgtype, - body: "Message", - url: message.content - }); - } - await markMessageAsRead(message.sender, roomId); - } - await client.sendText(roomId, i18n.t(["general", "menu"])); - break; - case '4': - setUserState(roomId, 'location_u'); - await client.sendText(roomId, i18n.t(["setup", "location"])); - break; - case '5': - setUserState(roomId, 'range_u'); - await client.sendText(roomId, i18n.t(["setup", "range"])); - break; - case '6': - setUserState(roomId, 'name_u'); - await client.sendText(roomId, i18n.t(["setup", "name"])); - break; - case '7': - setUserState(roomId, 'age_u'); - await client.sendText(roomId, i18n.t(["setup", "age"])); - break; - case '8': - setUserState(roomId, 'sex_u'); - await client.sendText(roomId, i18n.t(["setup", "sex"])); - break; - case '9': - setUserState(roomId, 'interest_u'); - await client.sendText(roomId, i18n.t(["setup", "interest"])); - break; - case '10': - setUserState(roomId, 'description_u'); - await client.sendText(roomId, i18n.t(["setup", "description"])); - break; - case '11': - await eraseUserPfp(roomId); - setUserState(roomId, 'pictures_u'); - await client.sendText(roomId, i18n.t(["setup", "pictures"])); - break; - case '12': - setUserState(roomId, 'language_u'); - await client.sendText(roomId, i18n.t(["setup", "language"])); - default: - await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); - await client.sendText(roomId, i18n.t(["general", "menu"])); - break; - } - break; - default: - await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); + number5: cities[4].id, + name5: cities[4].name, + country5: cities[4].country, + lat5: cities[4].lat, + lng5: cities[4].lng, + })); return; - } - } catch (e) { - logError(e); + } else { + if (action[1] == "u") + await user.update(answer) + else + await user.process(answer) + } + break; + case "range": + const range = parseInt(answer); + if (!range && range !== 0) + await client.sendText(roomId, i18n.t(["setup", "range"])); + else + if (action[1] == "u") + await user.update(range) + else + await user.process(range); + break; + case "name": + if (action[1] == "u") + await user.update(answer) + else + await user.process(answer); + break; + case "age": + const age = parseInt(answer); + if (!age || age > 100) + await client.sendText(roomId, i18n.t(["setup", "age"])); + else if (age < 14) { + await client.sendText(roomId, i18n.t(["errors", "tooyoung"])); + await client.leaveRoom(roomId); + await user.fullErase(); + return; + } else + if (action[1] == "u") + await user.update(age) + else + await user.process(age); + break; + case "sex": + const sex = answer.toLowerCase().trim(); + if (sex !== "female" && + sex !== "f" && + sex !== "male" && + sex !== "m") + await client.sendText(roomId, i18n.t(["setup", "sex"])); + else + if (action[1] == "u") + await user.update(sex[0]) + else + await user.process(sex[0]); + break; + case "interest": + const interest = answer.toLowerCase().trim(); + if (interest !== "female" && + interest !== "f" && + interest !== "male" && + interest !== "m" && + interest !== "b" && + interest !== "both") + await client.sendText(roomId, i18n.t(["setup", "interest"])); + else + if (action[1] == "u") + await user.update(interest[0]) + else + await user.process(interest[0]); + break; + case "description": + if (action[1] == "u") + await user.update(answer) + else + await user.process(answer); + break; + case "language": + if (answer !== "en" && answer !== "ru") { + await client.sendText(roomId, i18n.t(["setup", "language"])); + return; + } + i18n.locale = answer + user.i18n.locale = answer + if (action[1] == "u") + await user.update(answer); + else + await user.process(answer); + await user.getLanguage() + break; + case "pictures": + if (event.content?.msgtype !== 'm.image' && event.content?.msgtype !== 'm.video') { + await client.sendText(roomId, i18n.t(["setup", "done"])); + await user.setState("view_profiles") + await user.showRandomProfile(); + } else { + + const pictures_count = (await user.getProfilePictures()).length + + if (pictures_count >= config.maxAmountOfPhotoesPerUser) { + + await client.sendText(roomId, i18n.t(["errors", "toomuch"])); + await user.setState("view_profiles"); + await user.showRandomProfile(); + } else { + + let mxc = await uploadMediaFromEvent(client, event); + let type = convertMsgType(msgtype); + await user.addProfilePicture(type, mxc); + const pictures_count = (await user.getProfilePictures()).length; + if (pictures_count < config.maxAmountOfPhotoesPerUser) { + await client.sendText(roomId, i18n.t(["setup", "more"], { amount: String(config.maxAmountOfPhotoesPerUser - pictures_count) })); + } else { + await client.sendText(roomId, i18n.t(["setup", "enough"])); + await client.sendText(roomId, i18n.t(["setup", "done"])); + await user.setState("view_profiles"); + await user.showRandomProfile(); + } + } + } + break; + case "view_profiles": + if (answer == '👍️' || answer == '❤️' || answer == '1') { + let currently_viewing = await user.getCurrentlyViewingProfile(); + + await user.addLikeTo(currently_viewing); + let value = await user.checkForMutualLikeWith(currently_viewing); + if (value) { + await client.sendText(currently_viewing, i18n.t(["general", "newlikes"])); + await client.sendText(roomId, i18n.t(["general", "newlikes"])); + } + } else if (answer == '💌' || answer == '3') { + await user.setState('send_message'); + await client.sendText(roomId, i18n.t(["general", "message"])); + return; + } else if (answer == '🏠️' || answer == '4') { + await client.sendText(roomId, i18n.t(["general", "menu"])); + await user.setState('menu'); + return; + } + await user.showRandomProfile(); + break; + case "send_message": + let recipient = await user.getCurrentlyViewingProfile(); + let content; + + if (msgtype == "m.image" || msgtype == "m.video") { + let mxc = await uploadMediaFromEvent(client, event); + await uploadMediaAsMessage(roomId, msgtype, mxc); + content = mxc; + } else { + content = answer; + } + + if (await insertMessageIntoDB(roomId, recipient, msgtype, content)) { + await client.sendText(recipient, i18n.t(["general", "newmessage"])); + await client.sendText(roomId, i18n.t(["general", "messagesent"])); + } else { + await client.sendText(roomId, i18n.t(["errors", "alreadymessaged"])); + } + await user.setState("view_profiles"); + await user.showRandomProfile(); + break; + case 'menu': + switch (answer) { + case '0': + let profile = await user.getProfileInfo(); + await user.showProfile(profile); + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; + case '1': + await user.setState('view_profiles'); + await user.showRandomProfile(); + break; + case '2': + await user.showNewLikes(); + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; + case '3': + let unreadMessages = await user.getUnreadMessages(); + if (!unreadMessages || unreadMessages.length == 0) { + await client.sendText(roomId, i18n.t(["general", "nonewmessages"])); + return; + } + await client.sendText(roomId, i18n.t(["general", "msg"])); + + for (let message of unreadMessages) { + await client.sendText(roomId, i18n.t(["general", "showmessage"], { user: message.mx_id })); + + if (message.type == "t") { + await client.sendText(roomId, message.content) + } else if (message.type == "p" || message.type == "v") { + let msgtype; + + if (message.type == "p") { + msgtype = "m.image"; + } else if (message.type == "v") { + msgtype = "m.video"; + } + + await client.sendMessage(roomId, { + msgtype: msgtype, + body: "Message", + url: message.content + }); + } + await user.markMessageAsRead(message.sender); + } + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; + case '4': + await user.setState('location-u'); + await client.sendText(roomId, i18n.t(["setup", "location"])); + break; + case '5': + await user.setState('range-u'); + await client.sendText(roomId, i18n.t(["setup", "range"])); + break; + case '6': + await user.setState('name-u'); + await client.sendText(roomId, i18n.t(["setup", "name"])); + break; + case '7': + await user.setState('age-u'); + await client.sendText(roomId, i18n.t(["setup", "age"])); + break; + case '8': + await user.setState('sex-u'); + await client.sendText(roomId, i18n.t(["setup", "sex"])); + break; + case '9': + await user.setState('interest-u'); + await client.sendText(roomId, i18n.t(["setup", "interest"])); + break; + case '10': + await user.setState('description-u'); + await client.sendText(roomId, i18n.t(["setup", "description"])); + break; + case '11': + await user.erasePfp(); + await user.setState('pictures-u'); + await client.sendText(roomId, i18n.t(["setup", "pictures"])); + break; + case '12': + await user.setState('language-u'); + await client.sendText(roomId, i18n.t(["setup", "language"])); + break; + default: + await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; + } + break; + default: + await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); + return; + } }); client.on("room.event", async (roomId, event) => { try { + const user = new User(roomId); + if (event.type === "m.room.member" && event.content?.membership === "leave") { - await fullEraseUser(roomId); - logInfo(`Bot has left a room with ID ${roomId}`); + await user.fullErase(); + logInfo(`Bot has left a room with ID ${roomId}`); client.leaveRoom(roomId); } } catch (e) { @@ -341,21 +400,22 @@ client.on("room.invite", async (roomId, event) => { logInfo(`Bot has joined a room with ID ${roomId}`); let mx_id = event.sender; - - if (!await doesUserExist(roomId)) { - await createUser(mx_id, roomId); + const user = new User(roomId); + if (!await user.exists()) { + await user.init(mx_id); i18n.locale = "en"; await client.sendText(roomId, i18n.t(["general", "welcome"])); i18n.locale = "ru"; await client.sendText(roomId, i18n.t(["general", "welcome"])); } else { - setUserState(roomId, 'view_profiles'); + user.setState('view_profiles'); } } catch (e) { logError(e); } - }); -client.start().then(() => logInfo("Bot started!")); \ No newline at end of file +client.start().then(() => logInfo("Bot started!")); + +export { client }; \ No newline at end of file diff --git a/src/interactions.js b/src/interactions.js deleted file mode 100644 index 2551fd9..0000000 --- a/src/interactions.js +++ /dev/null @@ -1,352 +0,0 @@ -import { - logError, - logInfo, - readConfig, - uploadMediaFromEvent, - convertMsgType -} from './utils.js'; - -import { - appendUserPictures, - checkForMutualLike, - fullEraseUser, - getAmountOfUserPictures, - setUserLanguage, - getUserLanguage, - findCity, - db, - setUserState, - selectProfilesForUser, - setUserCurrentlyViewingProfile, - getProfileInfo, - getAllLikesForUser, - markLikeAsRead, - getCountryNameByID, - getCityNameByID -} from "./db.js"; - -import fs from 'fs'; - -const config = readConfig(); -const maxAmountOfPhotoesPerUser = config.maxAmountOfPhotoesPerUser; - - -import { I18n } from "i18n-js"; - - -const requiresLengths = { - "location": 32, - "description": 512, - "name": 32 -}; - -const processRequest = async (client, roomId, question, answer, nextQuestion) => { - - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - if (answer.length > requiresLengths[question]) { - await client.sendText(roomId, i18n.t(["errors", "toobig"])); - return; - } - let q = question.split("_") - if (q[1] == "u") - await db.query(`UPDATE users SET ${q[0]} = $1 WHERE room_id = $2`, [answer, roomId]); - else - await db.query(`UPDATE users SET ${question} = $1 WHERE room_id = $2`, [answer, roomId]); - - await client.sendText(roomId, i18n.t(["general", "setopt"], { opt: answer })); - if (nextQuestion == "menu") - await client.sendText(roomId, i18n.t(["general", "menu"])); - else - await client.sendText(roomId, i18n.t(["setup", nextQuestion])); - - setUserState(roomId, nextQuestion); -}; - -const showProfile = async (client, roomId, chosenProfile) => { - chosenProfile.country = await getCountryNameByID(chosenProfile.location); - chosenProfile.city = await getCityNameByID(chosenProfile.location); - - let message = - `${chosenProfile.country}, ${chosenProfile.city}. -${chosenProfile.name}, ${chosenProfile.sex == 'm' ? 'male' : 'female'}, ${chosenProfile.age}. -${chosenProfile.description}`; - - await client.sendText(roomId, message); - - if (chosenProfile.media) { - for (let media of chosenProfile.media) { - let msgtype; - if (media.type == 'p') { - msgtype = "m.image"; - } else if (media.type == 'v') { - msgtype = "m.video"; - } - await client.sendMessage(roomId, { - msgtype: msgtype, - body: "Profile media", - url: media.url - }); - } - } -} - -const showRandomProfileToUser = async (client, roomId) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - let chosenProfile = await selectProfilesForUser(roomId); - - if (!chosenProfile) { - await client.sendText(roomId, i18n.t(["errors", "noprofiles"])); - return; - } - - await showProfile(client, roomId, chosenProfile); - - await setUserCurrentlyViewingProfile(roomId, chosenProfile.room_id); - - await client.sendText(roomId, i18n.t(["general", "rate"])); -}; - -const showProfileToUser = async (client, roomId, profileId) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - let profileInfo = await getProfileInfo(profileId); - - await client.sendText(roomId, i18n.t(["general", "showalike"])); - - await showProfile(client, roomId, profileInfo); - - await client.sendText(roomId, i18n.t(["general", "mxid"], { mxid: profileInfo.mx_id })); -}; - -const showNewLikes = async (client, roomId) => { - let likes = (await getAllLikesForUser(roomId)); - for (let liked of likes) { - if (await checkForMutualLike(liked.sender, roomId)) { - await showProfileToUser(client, roomId, liked.sender); - await markLikeAsRead(liked.sender, roomId); - } - } -}; - -const wait_start = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - let a = answer.split(" "); - if (a[0] !== "!start") return; - if (a[1] !== "ru" && a[1] !== "en") return; - await setUserLanguage(roomId, a[1]); - i18n.locale = a[1]; - await setUserState(roomId, update ? "menu" : "location"); - await client.sendText(roomId, i18n.t(["setup", "location"])); -} - -const loc = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - let number = parseInt(answer); - if (!number) { - let cities = await findCity(answer); - await client.sendText(roomId, i18n.t(["setup", "choosecity"], { - number1: cities[0].id, - name1: cities[0].name, - country1: cities[0].country, - lat1: cities[0].lat, - lng1: cities[0].lng, - - number2: cities[1].id, - name2: cities[1].name, - country2: cities[1].country, - lat2: cities[1].lat, - lng2: cities[1].lng, - - number3: cities[2].id, - name3: cities[2].name, - country3: cities[2].country, - lat3: cities[2].lat, - lng3: cities[2].lng, - - number4: cities[3].id, - name4: cities[3].name, - country4: cities[3].country, - lat4: cities[3].lat, - lng4: cities[3].lng, - - number5: cities[4].id, - name5: cities[4].name, - country5: cities[4].country, - lat5: cities[4].lat, - lng5: cities[4].lng, - })); - } else { - await processRequest(client, roomId, current_action, number, update ? "menu" : "range"); - } -} - -const range = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - answer = parseInt(answer.split(" ")[0]); - if (!answer && answer != 0) { - await client.sendText(roomId, i18n.t(["setup", "range"])); - return; - } - await processRequest(client, roomId, current_action, answer, update ? "menu" : "name"); -} - -const age = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - answer = parseInt(answer); - if (!answer) { - await client.sendText(roomId, i18n.t(["setup", "age"])); - return; - } - - if (answer < 14) { - await client.sendText(roomId, i18n.t(["errors", "tooyoung"])); - await client.leaveRoom(roomId); - await fullEraseUser(roomId); - return; - } - await processRequest(client, roomId, current_action, answer, update ? "menu" : "sex"); -} - -const sex = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - answer = answer.toLowerCase().trim(); - if (answer.toLowerCase() != "male" && answer.toLowerCase() != "female") { - await client.sendText(roomId, i18n.t(["errors", "twosexes"])); - return; - } - await processRequest(client, roomId, current_action, answer[0], update ? "menu" : "interest"); -} - -const interest = async (client, roomId, current_action, answer, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - answer = answer.toLowerCase().trim(); - if (answer != "male" && answer != "female" && answer != "both") { - await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); - return; - } - await processRequest(client, roomId, current_action, answer[0], update ? "menu" : "description"); -} - -const pictures = async (client, roomId, current_action, event, update) => { - const i18n = new I18n({ - en: JSON.parse(fs.readFileSync("./translations/en.json")), - ru: JSON.parse(fs.readFileSync("./translations/ru.json")) - }); - - let preferredLanguage = await getUserLanguage(roomId); - if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; - i18n.locale = preferredLanguage; - - const msgtype = event.content.msgtype; - if (event.content?.msgtype !== 'm.image' && event.content?.msgtype !== 'm.video') { - await client.sendText(roomId, i18n.t(["setup", "done"])); - await setUserState(roomId, update ? "menu" : "view_profiles"); - await showRandomProfileToUser(client, roomId); - } else { - let pictures_count = parseInt(await getAmountOfUserPictures(roomId)); - if (pictures_count >= maxAmountOfPhotoesPerUser) { - await client.sendText(roomId, i18n.t(["errors", "toomuch"])); - await setUserState(roomId, update ? "menu" : "view_profiles"); - await showRandomProfileToUser(client, roomId); - } else { - let mxc = await uploadMediaFromEvent(client, event); - let type = convertMsgType(msgtype); - await appendUserPictures(roomId, mxc, type); - let pictures_count = await getAmountOfUserPictures(roomId); - if (pictures_count < maxAmountOfPhotoesPerUser) { - await client.sendText(roomId, i18n.t(["setup", "more"], { amount: String(maxAmountOfPhotoesPerUser - pictures_count) })); - } else { - await client.sendText(roomId, i18n.t(["setup", "enough"])); - await client.sendText(roomId, i18n.t(["setup", "done"])); - await setUserState(roomId, update ? "menu" : "view_profiles"); - await showRandomProfileToUser(client, roomId); - } - } - } -} - -export { - processRequest, - showRandomProfileToUser, - showNewLikes, - showProfileToUser, - wait_start, - loc, - range, - age, - sex, - interest, - pictures, - showProfile -}; \ No newline at end of file diff --git a/src/user.js b/src/user.js new file mode 100644 index 0000000..2929f91 --- /dev/null +++ b/src/user.js @@ -0,0 +1,264 @@ +import { client } from "./index.js"; +import { sequence, max_length, getCityNameByID, getCountryNameByID } from "./utils.js"; +import { I18n } from "i18n-js"; +import fs from "fs"; +import { db } from "./db.js"; + +export class User { + constructor(roomId) { + this.roomId = roomId + this.i18n = new I18n({ + en: JSON.parse(fs.readFileSync("./translations/en.json")), + ru: JSON.parse(fs.readFileSync("./translations/ru.json")) + }); + this.i18n.locale = "en"; + } + + async go(one, two) { + await client.sendText(this.roomId, this.i18n.t([one, two])); + await this.setState(two); + } + + async exists() { + return (await db.query("SELECT EXISTS (SELECT * FROM users WHERE room_id = $1)", [this.roomId])).rows[0].exists; + } + + async init(mxid) { + await db.query("INSERT INTO users(mx_id, room_id, current_action) VALUES ($1, $2, $3)", [mxid, this.roomId, "wait_start"]); + } + + async setState(state) { + await db.query("UPDATE users SET current_action = $1 WHERE room_id = $2", [state, this.roomId]); + } + + async set(property, value) { + //${property} is passed from the code, not user input. It's safe. Maybe. I don't know ;) + await db.query(`UPDATE users SET ${property} = $1 WHERE room_id = $2`, [value, this.roomId]); + } + + async getLanguage() { + const language = (await db.query("SELECT language FROM users WHERE room_id = $1", [this.roomId])).rows[0].language; + this.i18n.locale = language + return language; + } + + async getCurrentAction() { + return (await db.query('SELECT current_action FROM users WHERE room_id = $1', [this.roomId])).rows[0].current_action; + } + + async process(answer) { + const current_action = await this.getCurrentAction(); + + let next_action = "menu"; + if (sequence[current_action]) next_action = sequence[current_action] + if (max_length[current_action] < answer.length) { + await client.sendText(this.roomId, this.i18n.t(["error", "toobig"])); + await client.sendText(this.roomId, this.i18n.t(["setup", current_action])); + return; + } + await this.set(current_action, answer); + await client.sendText(this.roomId, this.i18n.t(["general", "setopt"], { opt: answer })); + + await this.go(next_action == "menu" ? "general" : "setup", next_action); + } + + async update(answer) { + const i18n = new I18n({ + en: JSON.parse(fs.readFileSync("./translations/en.json")), + ru: JSON.parse(fs.readFileSync("./translations/ru.json")) + }); + i18n.locale = await this.getLanguage(); + + //when updating a field, current_action = -u. + const current_action = (await this.getCurrentAction()).split("-")[0]; + + if (max_length[current_action] < answer.length) { + await client.sendText(this.roomId, i18n.t(["error", "toobig"])); + await client.sendText(this.roomId, i18n.t(["setup", current_action])); + return; + } + await this.set(current_action, answer); + await this.markProfileAsUnRead(); + await this.getLanguage() + await client.sendText(this.roomId, i18n.t(["general", "setopt"], { opt: answer })); + await this.go("general", "menu"); + } + + async getProfilePictures() { + return (await db.query("SELECT url, type FROM media WHERE owner = $1 AND purpose = 'p'", [this.roomId])).rows; + } + + async addProfilePicture(type, mxc) { + await db.query("INSERT INTO media (owner, type, purpose, url) VALUES ($1, $2, 'p', $3)", [this.roomId, type, mxc]); + } + + async getProfileInfo() { + let user = (await db.query("SELECT mx_id, room_id, name, age, sex, description, location FROM users WHERE room_id = $1", [this.roomId])).rows[0]; + if (!user) return null; + const u = new User(user.room_id); + const media = await u.getProfilePictures(); + user.media = media; + return user; + } + + async markProfileAsRead(whoRead) { + await db.query("INSERT INTO read_profiles(sender, recipient) VALUES ($1, $2)", [whoRead, this.roomId]); + } + + async markProfileAsUnRead() { + await db.query("DELETE FROM read_profiles WHERE recipient = $1", [this.roomId]); + } + + async selectRandomProfile() { + const { myrange, myinterest, mysex, myage, mycity } = (await db.query(`SELECT range AS myrange, + interest AS myinterest, + sex AS mysex, + age AS myage, + location AS mycity + FROM users WHERE room_id = $1`, [this.roomId]) + ).rows[0]; + + const { lat, lng } = (await db.query("SELECT lat, lng FROM cities WHERE ID = $1", [mycity])).rows[0]; + //TODO: figure out how to move it to PG function + let user = (await db.query(`SELECT + room_id, name, age, sex, description, location, range FROM users + WHERE + age::numeric <@ ANY(ARRAY[numrange($1 - 2, $1 + 2)]) + AND room_id != $2 + AND ${myinterest !== 'b' ? "sex = $3" : "$3 = $3 AND $4 = $4 AND $5 = $5 AND $6 = $6 AND $7 = $7"} + AND ${myrange !== 0 ? + `location = ANY(ARRAY(SELECT ID + FROM cities + WHERE + check_distance($6::double precision, $7::double precision, lat, lng, $5::double precision) + AND (check_distance($6::double precision, $7::double precision, lat, lng, range) OR range = 0) + ))` + : + `range = 0 OR + check_distance($6::double precision, $7::double precision, (SELECT lat FROM cities WHERE ID = location), (SELECT lng FROM cities WHERE ID = location), range)` + } + AND (interest = $4 OR interest = 'b') + AND NOT (room_id = ANY(ARRAY(SELECT recipient FROM read_profiles))) + ORDER BY RANDOM() + LIMIT 1`, [myage, this.roomId, myinterest, mysex, myrange, lat, lng]) + ).rows[0]; + + if (!user) return null; + const u = new User(user.room_id); + const media = await u.getProfilePictures(); + u.markProfileAsRead(this.roomId); + + user.media = media; + return user; + } + + async showProfile(profile) { + if (!profile) { + await client.sendText(this.roomId, this.i18n.t(["errors", "noprofiles"])); + return; + } + + profile.country = await getCountryNameByID(profile.location); + profile.city = await getCityNameByID(profile.location); + + let message = + `${profile.country}, ${profile.city}. +${profile.name}, ${profile.sex == 'm' ? 'male' : 'female'}, ${profile.age}. +${profile.description}`; + + await client.sendText(this.roomId, message); + + if (profile.media) { + for (let media of profile.media) { + let msgtype; + if (media.type == 'p') { + msgtype = "m.image"; + } else if (media.type == 'v') { + msgtype = "m.video"; + } + await client.sendMessage(this.roomId, { + msgtype: msgtype, + body: "Profile media", + url: media.url + }); + } + } + } + + async showRandomProfile() { + let profile = await this.selectRandomProfile(); + await this.showProfile(profile); + } + + async getCurrentlyViewingProfile() { + return (await db.query("SELECT currently_viewing FROM users WHERE room_id = $1", [this.roomId])).rows[0].currently_viewing; + } + + async addLikeTo(like) { + await db.query(`DELETE FROM likes l1 + USING likes l2 + WHERE l1.sender = l2.recipient + AND l1.recipient = l2.sender + AND l1.read = TRUE + AND l2.read = TRUE`); + + await db.query("INSERT INTO likes (sender, recipient) VALUES ($1, $2) ON CONFLICT DO NOTHING", [this.roomId, like]); + } + + async checkForMutualLikeWith(person) { + return (await db.query(`SELECT COUNT(*) > 0 AS value + FROM likes l1 + JOIN likes l2 ON l1.sender = l2.recipient AND l1.recipient = l2.sender + WHERE (l1.read = FALSE OR l2.read = FALSE) + AND l1.sender = $1 + AND l1.recipient = $2`, [this.roomId, person])).rows[0].value; + } + + async getLikes() { + return (await db.query("SELECT sender FROM likes WHERE recipient = $1 AND read = FALSE", [this.roomId])).rows; + } + + async markLikeAsRead(sender) { + await db.query("UPDATE likes SET read = TRUE WHERE sender = $1 AND recipient = $2", [sender, this.roomId]); + } + + async showNewLikes() { + let likes = (await this.getLikes()); + for (let liked of likes) { + if (await this.checkForMutualLikeWith(liked.sender)) { + await this.showProfile(liked.sender); + await this.markLikeAsRead(liked.sender); + } + } + } + + async markMessageAsRead(sender) { + return (await db.query("UPDATE messages SET read = TRUE WHERE sender = $1 AND recipient = $2", [sender, this.roomId])); + } + + async getUnreadMessages() { + return (await db.query("SELECT (SELECT mx_id FROM users WHERE room_id = sender LIMIT 1), sender, type, content FROM messages WHERE recipient = $1 AND read = FALSE", [this.roomId])).rows + } + + async erasePfp() { + await db.query("DELETE FROM media WHERE owner = $1 AND purpose = 'p'", [this.roomId]); + } + + async eraseMessages() { + await db.query("DELETE FROM messages WHERE sender = $1 OR recipient = $1", [this.roomId]); + } + + async eraseMedia() { + await db.query("DELETE FROM media WHERE owner = $1", [this.roomId]); + } + + async eraseLikes() { + await db.query("DELETE FROM likes WHERE sender = $1 OR recipient = $1", [this.roomId]); + } + + async fullErase() { + await this.eraseMessages() + await this.eraseMedia() + await this.eraseLikes() + } +} diff --git a/src/utils.js b/src/utils.js index f3cbc11..ab4fc1e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,12 +1,6 @@ import fs from 'fs'; -import { - EncryptionAlgorithm, - MatrixClient, - MessageEvent, - RustSdkCryptoStorageProvider, - SimpleFsStorageProvider, -} from "matrix-bot-sdk"; - +import { MessageEvent } from "matrix-bot-sdk"; +import { db } from "./db.js" const configPath = './config.json'; const logError = (message) => { @@ -47,6 +41,10 @@ const uploadMediaFromEvent = async (client, event) => { return mxc; }; +const uploadMediaAsMessage = async (roomId, type, mxc) => { + await db.query("INSERT INTO media(owner, type, purpose, url) VALUES ($1, $2, 'm', $3)", [roomId, convertMsgType(type), mxc]); +}; + const convertMsgType = (msgtype) => { switch (msgtype) { case "m.image": @@ -60,4 +58,57 @@ const convertMsgType = (msgtype) => { } }; -export { readConfig, logError, logInfo, uploadMediaFromEvent, convertMsgType }; \ No newline at end of file + +const getCityNameByID = async (id) => { + return (await db.query(`SELECT name FROM cities WHERE ID = $1`, [id])).rows[0].name; +} + +const getCountryNameByID = async (id) => { + return (await db.query(`SELECT country FROM cities WHERE ID = $1`, [id])).rows[0].country; +} + +const findCity = async (name) => { + return (await db.query(`SELECT ID, name, lat, lng, country, levenshtein(name, $1) AS similarity + FROM cities + ORDER BY similarity ASC + LIMIT 5`, [name]) + ).rows; +} + +const insertMessageIntoDB = async (roomId, recipient, type, content) => { + let rowAmount = (await db.query("INSERT INTO messages (sender, recipient, type, content) VALUES ($1, $2, $3, $4) ON CONFLICT(sender, recipient) DO NOTHING RETURNING *", [roomId, recipient, convertMsgType(type), content])).rowCount; + return rowAmount == 1; +}; + +const sequence = { + "wait_start": "location", + "location": "range", + "range": "name", + "name": "age", + "age": "sex", + "sex": "interest", + "interest": "description", + "description": "pictures", + "pictures": "view_profiles" +}; + +const max_length = { + "name": 32, + "description": 512, + "message": 128 +} + +export { + readConfig, + logError, + logInfo, + uploadMediaFromEvent, + convertMsgType, + sequence, + max_length, + getCityNameByID, + getCountryNameByID, + findCity, + insertMessageIntoDB, + uploadMediaAsMessage +}; \ No newline at end of file