"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var friends_exports = {}; __export(friends_exports, { DEFAULT_FILE: () => DEFAULT_FILE, FailureMessage: () => FailureMessage, FriendsDatabase: () => FriendsDatabase, MAX_FRIENDS: () => MAX_FRIENDS, MAX_REQUESTS: () => MAX_REQUESTS, PM: () => PM, findFriendship: () => findFriendship, sendPM: () => sendPM }); module.exports = __toCommonJS(friends_exports); var import_lib = require("../lib"); var import_config_loader = require("./config-loader"); var path = __toESM(require("path")); const MAX_FRIENDS = 100; const MAX_REQUESTS = 6; const DEFAULT_FILE = (0, import_lib.FS)("databases/friends.db").path; const REQUEST_EXPIRY_TIME = 30 * 24 * 60 * 60 * 1e3; const PM_TIMEOUT = 30 * 60 * 1e3; class FailureMessage extends Error { constructor(message) { super(message); this.name = "FailureMessage"; Error.captureStackTrace(this, FailureMessage); } } function sendPM(message, to, from = "~") { const senderID = toID(from); const receiverID = toID(to); const sendingUser = Users.get(senderID); const receivingUser = Users.get(receiverID); const fromIdentity = sendingUser ? sendingUser.getIdentity() : ` ${senderID}`; const toIdentity = receivingUser ? receivingUser.getIdentity() : ` ${receiverID}`; if (from === "~") { return receivingUser?.send(`|pm|~|${toIdentity}|${message}`); } receivingUser?.send(`|pm|${fromIdentity}|${toIdentity}|${message}`); } function canPM(sender, receiver) { if (!receiver?.settings.blockPMs) return true; if (receiver.settings.blockPMs === true) return sender.can("lock"); if (receiver.settings.blockPMs === "friends") return false; return Users.globalAuth.atLeast(sender, receiver.settings.blockPMs); } class FriendsDatabase { constructor(file = DEFAULT_FILE) { this.file = file === ":memory:" ? file : path.resolve(file); } async updateUserCache(user) { user.friends = /* @__PURE__ */ new Set(); const friends = await this.getFriends(user.id); for (const friend of friends) { user.friends.add(friend.userid); } return user.friends; } static setupDatabase(fileName) { const file = fileName || process.env.filename || DEFAULT_FILE; const exists = (0, import_lib.FS)(file).existsSync() || file === ":memory:"; const database = new (require("better-sqlite3"))(file); if (!exists) { database.exec((0, import_lib.FS)("databases/schemas/friends.sql").readSync()); } else { let val; try { val = database.prepare(`SELECT val FROM database_settings WHERE name = 'version'`).get().val; } catch { } const actualVersion = (0, import_lib.FS)(`databases/migrations/friends`).readdirIfExistsSync().length; if (val === void 0) { database.exec((0, import_lib.FS)("databases/schemas/friends.sql").readSync()); } if (typeof val === "number" && val !== actualVersion) { throw new Error(`Friends DB is out of date, please migrate to latest version.`); } } database.exec((0, import_lib.FS)(`databases/schemas/friends-startup.sql`).readSync()); for (const k in FUNCTIONS) { database.function(k, FUNCTIONS[k]); } for (const k in ACTIONS) { try { statements[k] = database.prepare(ACTIONS[k]); } catch (e) { throw new Error(`Friends DB statement crashed: ${ACTIONS[k]} (${e.message})`); } } for (const k in TRANSACTIONS) { transactions[k] = database.transaction(TRANSACTIONS[k]); } statements.expire.run(); return { database, statements }; } async getFriends(userid) { return await this.all("get", [userid, MAX_FRIENDS]) || []; } async getRequests(user) { const sent = /* @__PURE__ */ new Set(); const received = /* @__PURE__ */ new Set(); if (user.settings.blockFriendRequests) { await this.run("deleteReceivedRequests", [user.id]); } const sentResults = await this.all("getSent", [user.id]); if (sentResults === null) return { sent, received }; for (const request of sentResults) { sent.add(request.receiver); } const receivedResults = await this.all("getReceived", [user.id]) || []; if (!receivedResults) { return { received, sent }; } for (const request of receivedResults) { received.add(request.sender); } return { sent, received }; } all(statement, data) { return this.query({ type: "all", data, statement }); } transaction(statement, data) { return this.query({ data, statement, type: "transaction" }); } run(statement, data) { return this.query({ statement, data, type: "run" }); } get(statement, data) { return this.query({ statement, data, type: "get" }); } async query(input) { const process2 = PM.acquire(); if (!process2 || !import_config_loader.Config.usesqlite) { return null; } const result = await process2.query(input); if (result.error) { throw new Chat.ErrorMessage(result.error); } return result.result; } async request(user, receiverID) { const receiver = Users.getExact(receiverID); if (receiverID === user.id || receiver?.previousIDs.includes(user.id)) { throw new Chat.ErrorMessage(`You can't friend yourself.`); } if (receiver?.settings.blockFriendRequests) { throw new Chat.ErrorMessage(`${receiver.name} is blocking friend requests.`); } let buf = import_lib.Utils.html`/uhtml sent-${user.id}, | `; buf += import_lib.Utils.html`
`; buf += `(You can also stop this user from sending you friend requests with /ignore)`; const disclaimer = `/raw Note: If this request is accepted, your friend will be notified when you come online, and you will be notified when they do, unless you opt out of receiving them.`; if (receiver?.settings.blockFriendRequests) { throw new Chat.ErrorMessage(`This user is blocking friend requests.`); } if (!canPM(user, receiver)) { throw new Chat.ErrorMessage(`This user is blocking PMs, and cannot be friended right now.`); } const result = await this.transaction("send", [user.id, receiverID]); if (receiver) { sendPM(`/raw ${user.name} sent you a friend request!`, receiver.id, user.id); sendPM(buf, receiver.id, user.id); sendPM(disclaimer, receiver.id, user.id); } sendPM( `/nonotify You sent a friend request to ${receiver?.connected ? receiver.name : receiverID}!`, user.name ); sendPM( `/uhtml undo-${receiverID},`, user.name ); sendPM(disclaimer, user.id); return result; } async removeRequest(receiverID, senderID) { if (!senderID) throw new Chat.ErrorMessage(`Invalid sender username.`); if (!receiverID) throw new Chat.ErrorMessage(`Invalid receiver username.`); return this.run("deleteRequest", [senderID, receiverID]); } async approveRequest(receiverID, senderID) { return this.transaction("accept", [senderID, receiverID]); } async removeFriend(userid, friendID) { if (!friendID || !userid) throw new Chat.ErrorMessage(`Invalid usernames supplied.`); const result = await this.run("delete", { user1: userid, user2: friendID }); if (result.changes < 1) { throw new Chat.ErrorMessage(`You do not have ${friendID} friended.`); } } writeLogin(user) { return this.run("login", [user, Date.now(), Date.now()]); } hideLoginData(id) { return this.run("hideLogin", [id, Date.now()]); } allowLoginData(id) { return this.run("showLogin", [id]); } async getLastLogin(userid) { const result = await this.get("checkLastLogin", [userid]); return parseInt(result?.["last_login"]) || null; } async getSettings(userid) { return await this.get("getSettings", [userid]) || {}; } setHideList(userid, setting) { const num = setting ? 1 : 0; return this.run("toggleList", [userid, num, num]); } async findFriendship(user1, user2) { user1 = toID(user1); user2 = toID(user2); return !!(await this.get("findFriendship", { user1, user2 }))?.length; } } const statements = {}; const transactions = {}; const ACTIONS = { add: `REPLACE INTO friends (user1, user2) VALUES ($user1, $user2) ON CONFLICT (user1, user2) DO UPDATE SET user1 = $user1, user2 = $user2`, get: `SELECT * FROM friends_simplified f LEFT JOIN friend_settings fs ON f.friend = fs.userid WHERE f.userid = ? LIMIT ?`, delete: `DELETE FROM friends WHERE (user1 = $user1 AND user2 = $user2) OR (user1 = $user2 AND user2 = $user1)`, getSent: `SELECT receiver, sender FROM friend_requests WHERE sender = ?`, getReceived: `SELECT receiver, sender FROM friend_requests WHERE receiver = ?`, insertRequest: `INSERT INTO friend_requests(sender, receiver, sent_at) VALUES (?, ?, ?)`, deleteRequest: `DELETE FROM friend_requests WHERE sender = ? AND receiver = ?`, deleteReceivedRequests: `DELETE FROM friend_requests WHERE receiver = ?`, findFriendship: `SELECT * FROM friends WHERE (user1 = $user1 AND user2 = $user2) OR (user2 = $user1 AND user1 = $user2)`, findRequest: `SELECT count(*) as num FROM friend_requests WHERE (sender = $user1 AND receiver = $user2) OR (sender = $user2 AND receiver = $user1)`, countRequests: `SELECT count(*) as num FROM friend_requests WHERE (sender = ? OR receiver = ?)`, login: `INSERT INTO friend_settings (userid, send_login_data, last_login, public_list) VALUES (?, 0, ?, 0) ON CONFLICT (userid) DO UPDATE SET last_login = ?`, checkLastLogin: `SELECT last_login FROM friend_settings WHERE userid = ?`, deleteLogin: `UPDATE friend_settings SET last_login = 0 WHERE userid = ?`, expire: `DELETE FROM friend_requests WHERE EXISTS(SELECT sent_at FROM friend_requests WHERE should_expire(sent_at) = 1)`, hideLogin: ( // this works since if the insert works, they have no data, which means no public_list `INSERT INTO friend_settings (userid, send_login_data, last_login, public_list) VALUES (?, 1, ?, 0) ON CONFLICT (userid) DO UPDATE SET send_login_data = 1` ), showLogin: `DELETE FROM friend_settings WHERE userid = ? AND send_login_data = 1`, countFriends: `SELECT count(*) as num FROM friends WHERE (user1 = ? OR user2 = ?)`, getSettings: `SELECT * FROM friend_settings WHERE userid = ?`, toggleList: `INSERT INTO friend_settings (userid, send_login_data, last_login, public_list) VALUES (?, 0, 0, ?) ON CONFLICT (userid) DO UPDATE SET public_list = ?` }; const FUNCTIONS = { "should_expire": (sentTime) => { if (Date.now() - sentTime > REQUEST_EXPIRY_TIME) return 1; return 0; } }; const TRANSACTIONS = { send: (requests) => { for (const request of requests) { const [senderID, receiverID] = request; const hasSentRequest = statements.findRequest.get({ user1: senderID, user2: receiverID })["num"]; const friends = statements.countFriends.get(senderID, senderID)["num"]; const totalRequests = statements.countRequests.get(senderID, senderID)["num"]; if (friends >= MAX_FRIENDS) { throw new FailureMessage(`You are at the maximum number of friends.`); } const existingFriendship = statements.findFriendship.all({ user1: senderID, user2: receiverID }); if (existingFriendship.length) { throw new FailureMessage(`You are already friends with '${receiverID}'.`); } if (hasSentRequest) { throw new FailureMessage(`You have already sent a friend request to '${receiverID}'.`); } if (totalRequests >= MAX_REQUESTS) { throw new FailureMessage( `You already have ${MAX_REQUESTS} pending friend requests. Use "/friends view sent" to see your outgoing requests and "/friends view receive" to see your incoming requests.` ); } statements.insertRequest.run(senderID, receiverID, Date.now()); } return { result: [] }; }, add: (requests) => { for (const request of requests) { const [senderID, receiverID] = request; statements.add.run({ user1: senderID, user2: receiverID }); } return { result: [] }; }, accept: (requests) => { for (const request of requests) { const [senderID, receiverID] = request; const friends = statements.get.all(receiverID, 101); if (friends?.length >= MAX_FRIENDS) { throw new FailureMessage(`You are at the maximum number of friends.`); } const { result } = TRANSACTIONS.removeRequest([request]); if (!result.length) throw new FailureMessage(`You have no request pending from ${senderID}.`); TRANSACTIONS.add([request]); } return { result: [] }; }, removeRequest: (requests) => { const result = []; for (const request of requests) { const [to, from] = request; const { changes } = statements.deleteRequest.run(to, from); if (changes) result.push(changes); } return { result }; } }; function findFriendship(users) { setup(); return !!statements.findFriendship.get({ user1: users[0], user2: users[1] }); } const setup = () => { if (!process.send) throw new Error("You should not be using this function in the main process"); if (!Object.keys(statements).length) FriendsDatabase.setupDatabase(); }; const PM = new import_lib.ProcessManager.QueryProcessManager(module, (query) => { const { type, statement, data } = query; const start = Date.now(); const result = {}; try { switch (type) { case "run": result.result = statements[statement].run(data); break; case "get": result.result = statements[statement].get(data); break; case "transaction": result.result = transactions[statement]([data]); break; case "all": result.result = statements[statement].all(data); break; } } catch (e) { if (!e.name.endsWith("FailureMessage")) { result.error = "Sorry! The database process crashed. We've been notified and will fix this."; Monitor.crashlog(e, "A friends database process", query); } else { result.error = e.message; } return result; } const delta = Date.now() - start; if (delta > 1e3) { Monitor.slow(`[Slow friends list query] ${JSON.stringify(query)}`); } return result; }, PM_TIMEOUT, (message) => { if (message.startsWith("SLOW\n")) { Monitor.slow(message.slice(5)); } }); if (require.main === module) { global.Config = require("./config-loader").Config; if (import_config_loader.Config.usesqlite) { FriendsDatabase.setupDatabase(); } if (process.mainModule === module) { global.Monitor = { crashlog(error, source = "A friends database process", details = null) { const repr = JSON.stringify([error.name, error.message, source, details]); process.send(`THROW @!!@${repr} ${error.stack}`); }, slow(message) { process.send(`CALLBACK SLOW ${message}`); } }; process.on("uncaughtException", (err) => { if (import_config_loader.Config.crashguard) { Monitor.crashlog(err, "A friends child process"); } }); import_lib.Repl.start(`friends-${process.pid}`, (cmd) => eval(cmd)); } } else if (!process.send) { PM.spawn(import_config_loader.Config.friendsprocesses || 1); } //# sourceMappingURL=friends.js.map