diff --git a/keyserver/src/database/migration-config.js b/keyserver/src/database/migration-config.js index 916c5eafe..65293e05d 100644 --- a/keyserver/src/database/migration-config.js +++ b/keyserver/src/database/migration-config.js @@ -1,860 +1,871 @@ // @flow import fs from 'fs'; import bots from 'lib/facts/bots.js'; import genesis from 'lib/facts/genesis.js'; import { policyTypes } from 'lib/facts/policies.js'; import { specialRoles } from 'lib/permissions/special-roles.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { permissionsToRemoveInMigration } from 'lib/utils/migration-utils.js'; import { dbQuery, SQL } from '../database/database.js'; import { processMessagesInDBForSearch } from '../database/search-utils.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { createScriptViewer } from '../session/scripts.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { updateRolesAndPermissionsForAllThreads } from '../updaters/thread-permission-updaters.js'; import { updateThread } from '../updaters/thread-updaters.js'; import { ensureUserCredentials } from '../user/checks.js'; import { createPickledOlmAccount, publishPrekeysToIdentity, } from '../utils/olm-utils.js'; import { synchronizeInviteLinksWithBlobs } from '../utils/synchronize-invite-links-with-blobs.js'; const botViewer = createScriptViewer(bots.commbot.userID); const migrations: $ReadOnlyMap Promise> = new Map([ [ 0, async () => { await makeSureBaseRoutePathExists('facts/commapp_url.json'); await makeSureBaseRoutePathExists('facts/squadcal_url.json'); }, ], [ 1, async () => { try { await fs.promises.unlink('facts/url.json'); } catch {} }, ], [ 2, async () => { await fixBaseRoutePathForLocalhost('facts/commapp_url.json'); await fixBaseRoutePathForLocalhost('facts/squadcal_url.json'); }, ], [3, updateRolesAndPermissionsForAllThreads], [ 4, async () => { await dbQuery(SQL`ALTER TABLE uploads ADD INDEX container (container)`); }, ], [ 5, async () => { await dbQuery(SQL` ALTER TABLE cookies ADD device_id varchar(255) DEFAULT NULL, ADD public_key varchar(255) DEFAULT NULL, ADD social_proof varchar(255) DEFAULT NULL; `); }, ], [ 7, async () => { await dbQuery( SQL` ALTER TABLE users DROP COLUMN IF EXISTS public_key, MODIFY hash char(60) COLLATE utf8mb4_bin DEFAULT NULL; ALTER TABLE sessions DROP COLUMN IF EXISTS public_key; `, { multipleStatements: true }, ); }, ], [ 8, async () => { await dbQuery( SQL` ALTER TABLE users ADD COLUMN IF NOT EXISTS ethereum_address char(42) DEFAULT NULL; `, ); }, ], [ 9, async () => { await dbQuery( SQL` ALTER TABLE messages ADD COLUMN IF NOT EXISTS target_message bigint(20) DEFAULT NULL; ALTER TABLE messages ADD INDEX target_message (target_message); `, { multipleStatements: true }, ); }, ], [ 10, async () => { await dbQuery(SQL` CREATE TABLE IF NOT EXISTS policy_acknowledgments ( user bigint(20) NOT NULL, policy varchar(255) NOT NULL, date bigint(20) NOT NULL, confirmed tinyint(1) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (user, policy) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); }, ], [ 11, async () => { const time = Date.now(); await dbQuery(SQL` INSERT IGNORE INTO policy_acknowledgments (policy, user, date, confirmed) SELECT ${policyTypes.tosAndPrivacyPolicy}, id, ${time}, 1 FROM users `); }, ], [ 12, async () => { await dbQuery(SQL` CREATE TABLE IF NOT EXISTS siwe_nonces ( nonce char(17) NOT NULL, creation_time bigint(20) NOT NULL, PRIMARY KEY (nonce) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); }, ], [ 13, async () => { await Promise.all([ writeSquadCalRoute('facts/squadcal_url.json'), moveToNonApacheConfig('facts/commapp_url.json', '/comm/'), moveToNonApacheConfig('facts/landing_url.json', '/commlanding/'), ]); }, ], [ 14, async () => { await dbQuery(SQL` ALTER TABLE cookies MODIFY COLUMN social_proof mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL; `); }, ], [ 15, async () => { await dbQuery( SQL` ALTER TABLE uploads ADD COLUMN IF NOT EXISTS thread bigint(20) DEFAULT NULL, ADD INDEX IF NOT EXISTS thread (thread); UPDATE uploads SET thread = ( SELECT thread FROM messages WHERE messages.id = uploads.container ); `, { multipleStatements: true }, ); }, ], [ 16, async () => { await dbQuery( SQL` ALTER TABLE cookies DROP COLUMN IF EXISTS public_key; ALTER TABLE cookies ADD COLUMN IF NOT EXISTS signed_identity_keys mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL; `, { multipleStatements: true }, ); }, ], [ 17, async () => { await dbQuery( SQL` ALTER TABLE cookies DROP INDEX device_token, DROP INDEX user_device_token; ALTER TABLE cookies MODIFY device_token mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, ADD UNIQUE KEY device_token (device_token(512)), ADD KEY user_device_token (user,device_token(512)); `, { multipleStatements: true }, ); }, ], [18, updateRolesAndPermissionsForAllThreads], [19, updateRolesAndPermissionsForAllThreads], [ 20, async () => { await dbQuery(SQL` ALTER TABLE threads ADD COLUMN IF NOT EXISTS avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL; `); }, ], [ 21, async () => { await dbQuery(SQL` ALTER TABLE reports DROP INDEX IF EXISTS user, ADD INDEX IF NOT EXISTS user_type_platform_creation_time (user, type, platform, creation_time); `); }, ], [ 22, async () => { await dbQuery( SQL` ALTER TABLE cookies MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE entries MODIFY creator varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE focused MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE memberships MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE messages MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE notifications MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE reports MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE revisions MODIFY author varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE sessions MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE threads MODIFY creator varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE updates MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE uploads MODIFY uploader varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE users MODIFY id varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE relationships_undirected MODIFY user1 varchar(255) CHARSET latin1 COLLATE latin1_bin, MODIFY user2 varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE relationships_directed MODIFY user1 varchar(255) CHARSET latin1 COLLATE latin1_bin, MODIFY user2 varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE user_messages MODIFY recipient varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE settings MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE policy_acknowledgments MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; `, { multipleStatements: true }, ); }, ], [23, updateRolesAndPermissionsForAllThreads], [24, updateRolesAndPermissionsForAllThreads], [ 25, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS message_search ( original_message_id bigint(20) NOT NULL, message_id bigint(20) NOT NULL, processed_content mediumtext COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; ALTER TABLE message_search ADD PRIMARY KEY (original_message_id), ADD FULLTEXT INDEX processed_content (processed_content); `, { multipleStatements: true }, ); }, ], [26, processMessagesInDBForSearch], [ 27, async () => { await dbQuery(SQL` ALTER TABLE messages ADD COLUMN IF NOT EXISTS pinned tinyint(1) UNSIGNED NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS pin_time bigint(20) DEFAULT NULL, ADD INDEX IF NOT EXISTS thread_pinned (thread, pinned); `); }, ], [ 28, async () => { await dbQuery(SQL` ALTER TABLE threads ADD COLUMN IF NOT EXISTS pinned_count int UNSIGNED NOT NULL DEFAULT 0; `); }, ], [29, updateRolesAndPermissionsForAllThreads], [ 30, async () => { await dbQuery(SQL`DROP TABLE versions;`); }, ], [ 31, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS invite_links ( id bigint(20) NOT NULL, name varchar(255) CHARSET latin1 NOT NULL, \`primary\` tinyint(1) UNSIGNED NOT NULL DEFAULT 0, role bigint(20) NOT NULL, community bigint(20) NOT NULL, expiration_time bigint(20), limit_of_uses int UNSIGNED, number_of_uses int UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE invite_links ADD PRIMARY KEY (id), ADD UNIQUE KEY (name), ADD INDEX community_primary (community, \`primary\`); `, { multipleStatements: true }, ); }, ], [ 32, async () => { await dbQuery(SQL` UPDATE messages SET target_message = JSON_VALUE(content, "$.sourceMessageID") WHERE type = ${messageTypes.SIDEBAR_SOURCE}; `); }, ], [ 33, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS olm_sessions ( cookie_id bigint(20) NOT NULL, is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickled_olm_session text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE olm_sessions ADD PRIMARY KEY (cookie_id, is_content); `, { multipleStatements: true }, ); }, ], [ 34, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS olm_accounts ( is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickling_key text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, pickled_olm_account text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE olm_accounts ADD PRIMARY KEY (is_content); `, { multipleStatements: true }, ); }, ], [ 35, async () => { await createOlmAccounts(); }, ], [36, updateRolesAndPermissionsForAllThreads], [ 37, async () => { await dbQuery( SQL` DELETE FROM olm_accounts; DELETE FROM olm_sessions; `, { multipleStatements: true }, ); await createOlmAccounts(); }, ], [ 38, async () => { const [result] = await dbQuery(SQL` SELECT t.id FROM threads t INNER JOIN memberships m ON m.thread = t.id AND m.role > 0 INNER JOIN users u ON u.id = m.user WHERE t.type = ${threadTypes.PRIVATE} AND t.name = u.ethereum_address `); const threadIDs = result.map(({ id }) => id.toString()); while (threadIDs.length > 0) { // Batch 10 updateThread calls at a time const batch = threadIDs.splice(0, 10); await Promise.all( batch.map(threadID => updateThread( botViewer, { threadID, changes: { name: '', }, }, { silenceMessages: true, ignorePermissions: true, }, ), ), ); } }, ], [39, ensureUserCredentials], [ 40, // Tokens from identity service are 512 characters long () => dbQuery( SQL` ALTER TABLE metadata MODIFY COLUMN data VARCHAR(1023) `, ), ], [ 41, () => dbQuery( SQL` ALTER TABLE memberships DROP INDEX user, ADD KEY user_role_thread (user, role, thread) `, ), ], [ 42, async () => { await dbQuery(SQL` ALTER TABLE roles ADD UNIQUE KEY thread_name (thread, name); `); }, ], [ 43, () => dbQuery( SQL` UPDATE threads SET pinned_count = ( SELECT COUNT(*) FROM messages WHERE messages.thread = threads.id AND messages.pinned = 1 ) `, ), ], [ 44, async () => { const { SIDEBAR_SOURCE, TOGGLE_PIN } = messageTypes; const [result] = await dbQuery(SQL` SELECT m1.thread FROM messages m1 LEFT JOIN messages m2 ON m2.id = m1.target_message WHERE m1.type = ${SIDEBAR_SOURCE} AND m2.type = ${TOGGLE_PIN} `); const threadIDs = new Set(); for (const row of result) { threadIDs.add(row.thread.toString()); } await Promise.all( [...threadIDs].map(threadID => deleteThread(botViewer, { threadID }, { ignorePermissions: true }), ), ); }, ], [ 45, () => dbQuery( SQL` ALTER TABLE uploads CHARSET utf8mb4 COLLATE utf8mb4_bin, MODIFY COLUMN type varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, MODIFY COLUMN filename varchar(255) CHARSET utf8mb4 COLLATE utf8mb4_bin NOT NULL, MODIFY COLUMN mime varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, MODIFY COLUMN secret varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL; `, ), ], [ 46, async () => { try { const [content, notif] = await Promise.all([ fetchOlmAccount('content'), fetchOlmAccount('notifications'), ]); await publishPrekeysToIdentity(content.account, notif.account); } catch (e) { console.warn('Encountered error while trying to publish prekeys', e); if (process.env.NODE_ENV !== 'development') { throw e; } } }, ], [ 47, () => dbQuery(SQL`ALTER TABLE cookies MODIFY COLUMN hash char(64) NOT NULL`), ], [ 48, async () => { const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`; const query = SQL` UPDATE memberships mm LEFT JOIN ( SELECT m.thread, MAX(m.id) AS message FROM messages m WHERE m.type != ${messageTypes.CREATE_SUB_THREAD} AND m.thread = ${genesis().id} GROUP BY m.thread ) all_users_query ON mm.thread = all_users_query.thread LEFT JOIN ( SELECT m.thread, stm.user, MAX(m.id) AS message FROM messages m LEFT JOIN memberships stm ON m.type = ${ messageTypes.CREATE_SUB_THREAD } AND stm.thread = m.content WHERE JSON_EXTRACT(stm.permissions, ${visibleExtractString}) IS TRUE AND m.thread = ${genesis().id} GROUP BY m.thread, stm.user ) last_subthread_message_for_user_query ON mm.thread = last_subthread_message_for_user_query.thread AND mm.user = last_subthread_message_for_user_query.user SET mm.last_message = GREATEST(COALESCE(all_users_query.message, 0), COALESCE(last_subthread_message_for_user_query.message, 0)) WHERE mm.thread = ${genesis().id}; `; await dbQuery(query); }, ], [ 49, async () => { if (isDockerEnvironment()) { return; } const defaultCorsConfig = { domain: 'http://localhost:3000', }; await writeJSONToFile(defaultCorsConfig, 'facts/webapp_cors.json'); }, ], [ 50, async () => { await moveToNonApacheConfig('facts/webapp_url.json', '/webapp/'); await moveToNonApacheConfig('facts/keyserver_url.json', '/keyserver/'); }, ], [ 51, async () => { if (permissionsToRemoveInMigration.length === 0) { return; } const setClause = SQL`permissions = JSON_REMOVE(permissions, ${permissionsToRemoveInMigration.map( path => `$.${path}`, )})`; const updateQuery = SQL` UPDATE roles r LEFT JOIN threads t ON t.id = r.thread `; updateQuery.append(SQL`SET `.append(setClause)); updateQuery.append(SQL` WHERE r.name != 'Admins' AND (t.type = ${threadTypes.COMMUNITY_ROOT} OR t.type = ${threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT}) `); await dbQuery(updateQuery); }, ], [ 52, async () => { await dbQuery( SQL` ALTER TABLE roles ADD COLUMN IF NOT EXISTS special_role tinyint(2) UNSIGNED DEFAULT NULL `, ); await updateRolesAndPermissionsForAllThreads(); }, ], [ 53, async () => dbQuery(SQL` ALTER TABLE invite_links ADD COLUMN blob_holder char(36) CHARSET latin1 `), ], [ 54, async () => { await dbQuery( SQL` ALTER TABLE roles ADD COLUMN IF NOT EXISTS special_role tinyint(2) UNSIGNED DEFAULT NULL, DROP KEY IF EXISTS thread, ADD KEY IF NOT EXISTS thread_special_role (thread, special_role); UPDATE roles r JOIN threads t ON r.id = t.default_role SET r.special_role = ${specialRoles.DEFAULT_ROLE}; `, { multipleStatements: true }, ); }, ], [ 55, async () => { await dbQuery( SQL` ALTER TABLE threads DROP COLUMN IF EXISTS default_role `, ); }, ], [ 56, async () => { await dbQuery( SQL` UPDATE roles SET special_role = ${specialRoles.ADMIN_ROLE} WHERE name = 'Admins' `, ); }, ], [57, synchronizeInviteLinksWithBlobs], + [ + 58, + async () => { + await dbQuery( + SQL` + ALTER TABLE updates + MODIFY \`key\` varchar(255) CHARSET latin1 COLLATE latin1_bin + `, + ); + }, + ], ]); const newDatabaseVersion: number = Math.max(...migrations.keys()); async function writeJSONToFile(data: any, filePath: string): Promise { console.warn(`updating ${filePath} to ${JSON.stringify(data)}`); const fileHandle = await fs.promises.open(filePath, 'w'); await fileHandle.writeFile(JSON.stringify(data, null, ' '), 'utf8'); await fileHandle.close(); } async function makeSureBaseRoutePathExists(filePath: string): Promise { let readFile, json; try { readFile = await fs.promises.open(filePath, 'r'); const contents = await readFile.readFile('utf8'); json = JSON.parse(contents); } catch { return; } finally { if (readFile) { await readFile.close(); } } if (json.baseRoutePath) { return; } let baseRoutePath; if (json.baseDomain === 'http://localhost') { baseRoutePath = json.basePath; } else if (filePath.endsWith('commapp_url.json')) { baseRoutePath = '/commweb/'; } else { baseRoutePath = '/'; } const newJSON = { ...json, baseRoutePath }; console.warn(`updating ${filePath} to ${JSON.stringify(newJSON)}`); await writeJSONToFile(newJSON, filePath); } async function fixBaseRoutePathForLocalhost(filePath: string): Promise { let readFile, json; try { readFile = await fs.promises.open(filePath, 'r'); const contents = await readFile.readFile('utf8'); json = JSON.parse(contents); } catch { return; } finally { if (readFile) { await readFile.close(); } } if (json.baseDomain !== 'http://localhost') { return; } const baseRoutePath = '/'; json = { ...json, baseRoutePath }; console.warn(`updating ${filePath} to ${JSON.stringify(json)}`); await writeJSONToFile(json, filePath); } async function moveToNonApacheConfig( filePath: string, routePath: string, ): Promise { if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config const newJSON = { baseDomain: 'http://localhost:3000', basePath: routePath, baseRoutePath: routePath, https: false, proxy: 'none', }; await writeJSONToFile(newJSON, filePath); } async function writeSquadCalRoute(filePath: string): Promise { if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config const newJSON = { baseDomain: 'http://localhost:3000', basePath: '/comm/', baseRoutePath: '/', https: false, proxy: 'apache', }; await writeJSONToFile(newJSON, filePath); } async function createOlmAccounts() { const [pickledContentAccount, pickledNotificationsAccount] = await Promise.all([createPickledOlmAccount(), createPickledOlmAccount()]); await dbQuery( SQL` INSERT INTO olm_accounts (is_content, version, pickling_key, pickled_olm_account) VALUES ( TRUE, 0, ${pickledContentAccount.picklingKey}, ${pickledContentAccount.pickledAccount} ), ( FALSE, 0, ${pickledNotificationsAccount.picklingKey}, ${pickledNotificationsAccount.pickledAccount} ); `, ); } function isDockerEnvironment(): boolean { return !!process.env.COMM_DATABASE_HOST; } export { migrations, newDatabaseVersion, createOlmAccounts }; diff --git a/keyserver/src/database/setup-db.js b/keyserver/src/database/setup-db.js index fb0aaa403..238b46e1e 100644 --- a/keyserver/src/database/setup-db.js +++ b/keyserver/src/database/setup-db.js @@ -1,484 +1,484 @@ // @flow import ashoat from 'lib/facts/ashoat.js'; import bots from 'lib/facts/bots.js'; import genesis from 'lib/facts/genesis.js'; import { usernameMaxLength } from 'lib/shared/account-utils.js'; -import { sortIDs } from 'lib/shared/relationship-utils.js'; +import { sortUserIDs } from 'lib/shared/relationship-utils.js'; import { undirectedStatus } from 'lib/types/relationship-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { createThread } from '../creators/thread-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { updateDBVersion } from '../database/db-version.js'; import { newDatabaseVersion, createOlmAccounts, } from '../database/migration-config.js'; import { createScriptViewer } from '../session/scripts.js'; import { ensureUserCredentials } from '../user/checks.js'; async function setupDB() { await ensureUserCredentials(); await createTables(); await createUsers(); await createThreads(); await setUpMetadataTable(); await createOlmAccounts(); } async function createTables() { await dbQuery( SQL` CREATE TABLE cookies ( id bigint(20) NOT NULL, hash char(64) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin DEFAULT NULL, platform varchar(255) DEFAULT NULL, creation_time bigint(20) NOT NULL, last_used bigint(20) NOT NULL, device_token mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, versions json DEFAULT NULL, device_id varchar(255) DEFAULT NULL, signed_identity_keys mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, social_proof mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, \`primary\` TINYINT(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE days ( id bigint(20) NOT NULL, date date NOT NULL, thread bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE entries ( id bigint(20) NOT NULL, day bigint(20) NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creator varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE focused ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, session bigint(20) NOT NULL, thread bigint(20) NOT NULL, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE ids ( id bigint(20) NOT NULL, table_name varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE memberships ( thread bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, role bigint(20) NOT NULL, permissions json DEFAULT NULL, permissions_for_children json DEFAULT NULL, creation_time bigint(20) NOT NULL, subscription json NOT NULL, last_message bigint(20) NOT NULL DEFAULT 0, last_read_message bigint(20) NOT NULL DEFAULT 0, sender tinyint(1) UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE messages ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, content mediumtext COLLATE utf8mb4_bin, time bigint(20) NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, target_message bigint(20) DEFAULT NULL, pinned tinyint(1) UNSIGNED NOT NULL DEFAULT 0, pin_time bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE notifications ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, thread bigint(20) DEFAULT NULL, message bigint(20) DEFAULT NULL, collapse_key varchar(255) DEFAULT NULL, delivery json NOT NULL, rescinded tinyint(1) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE reports ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, platform varchar(255) NOT NULL, report json NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE revisions ( id bigint(20) NOT NULL, entry bigint(20) NOT NULL, author varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creation_time bigint(20) NOT NULL, session bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE roles ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, name varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, permissions json NOT NULL, creation_time bigint(20) NOT NULL, special_role tinyint(2) UNSIGNED DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE sessions ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, cookie bigint(20) NOT NULL, query json NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, last_validated bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE threads ( id bigint(20) NOT NULL, type tinyint(3) NOT NULL, name varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, description mediumtext COLLATE utf8mb4_bin, parent_thread_id bigint(20) DEFAULT NULL, containing_thread_id bigint(20) DEFAULT NULL, community bigint(20) DEFAULT NULL, depth int UNSIGNED NOT NULL DEFAULT 0, creator varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, creation_time bigint(20) NOT NULL, color char(6) COLLATE utf8mb4_bin NOT NULL, source_message bigint(20) DEFAULT NULL UNIQUE, replies_count int UNSIGNED NOT NULL DEFAULT 0, avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, pinned_count int UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE updates ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, - \`key\` bigint(20) DEFAULT NULL, + \`key\` varchar(255) CHARSET latin1 COLLATE latin1_bin DEFAULT NULL, updater bigint(20) DEFAULT NULL, target bigint(20) DEFAULT NULL, content mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE uploads ( id bigint(20) NOT NULL, thread bigint(20) DEFAULT NULL, uploader varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, container bigint(20) DEFAULT NULL, type varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, filename varchar(255) NOT NULL, mime varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, content longblob NOT NULL, secret varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, creation_time bigint(20) NOT NULL, extra json DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE users ( id varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, username varchar(${usernameMaxLength}) COLLATE utf8mb4_bin NOT NULL, hash char(60) COLLATE utf8mb4_bin DEFAULT NULL, avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, ethereum_address char(42) DEFAULT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE relationships_undirected ( user1 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, user2 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE relationships_directed ( user1 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, user2 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE one_time_keys ( session bigint(20) NOT NULL, one_time_key char(43) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE user_messages ( recipient varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, thread bigint(20) NOT NULL, message bigint(20) NOT NULL, time bigint(20) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE settings ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, name varchar(255) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE metadata ( name varchar(255) NOT NULL, data varchar(1023) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE policy_acknowledgments ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, policy varchar(255) NOT NULL, date bigint(20) NOT NULL, confirmed tinyint(1) UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE siwe_nonces ( nonce char(17) NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE message_search ( original_message_id bigint(20) NOT NULL, message_id bigint(20) NOT NULL, processed_content mediumtext COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE invite_links ( id bigint(20) NOT NULL, name varchar(255) CHARSET latin1 NOT NULL, \`primary\` tinyint(1) UNSIGNED NOT NULL DEFAULT 0, role bigint(20) NOT NULL, community bigint(20) NOT NULL, expiration_time bigint(20), limit_of_uses int UNSIGNED, number_of_uses int UNSIGNED NOT NULL DEFAULT 0, blob_holder char(36) CHARSET latin1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE olm_sessions ( cookie_id bigint(20) NOT NULL, is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickled_olm_session text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; CREATE TABLE olm_accounts ( is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickling_key text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, pickled_olm_account text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE cookies ADD PRIMARY KEY (id), ADD UNIQUE KEY device_token (device_token(512)), ADD KEY user_device_token (user,device_token(512)); ALTER TABLE days ADD PRIMARY KEY (id), ADD UNIQUE KEY date_thread (date,thread) USING BTREE; ALTER TABLE entries ADD PRIMARY KEY (id), ADD UNIQUE KEY creator_creation (creator,creation), ADD KEY day (day); ALTER TABLE focused ADD UNIQUE KEY user_cookie_thread (user,session,thread), ADD KEY thread_user (thread,user); ALTER TABLE ids ADD PRIMARY KEY (id); ALTER TABLE memberships ADD UNIQUE KEY thread_user (thread, user) USING BTREE, ADD KEY role (role) USING BTREE, ADD KEY user_role_thread (user, role, thread); ALTER TABLE messages ADD PRIMARY KEY (id), ADD UNIQUE KEY user_creation (user,creation), ADD KEY thread (thread), ADD INDEX target_message (target_message), ADD INDEX thread_pinned (thread, pinned); ALTER TABLE notifications ADD PRIMARY KEY (id), ADD KEY rescinded_user_collapse_key (rescinded,user,collapse_key) USING BTREE, ADD KEY thread (thread), ADD KEY rescinded_user_thread_message (rescinded,user,thread,message) USING BTREE; ALTER TABLE notifications ADD INDEX user (user); ALTER TABLE reports ADD PRIMARY KEY (id), ADD INDEX user_type_platform_creation_time (user, type, platform, creation_time); ALTER TABLE revisions ADD PRIMARY KEY (id), ADD KEY entry (entry); ALTER TABLE roles ADD PRIMARY KEY (id), ADD KEY thread_special_role (thread, special_role), ADD UNIQUE KEY thread_name (thread, name); ALTER TABLE sessions ADD PRIMARY KEY (id), ADD KEY user (user); ALTER TABLE threads ADD PRIMARY KEY (id), ADD INDEX parent_thread_id (parent_thread_id), ADD INDEX containing_thread_id (containing_thread_id), ADD INDEX community (community); ALTER TABLE updates ADD PRIMARY KEY (id), ADD INDEX user_time (user,time), ADD INDEX target_time (target, time), ADD INDEX user_key_target_type_time (user, \`key\`, target, type, time), ADD INDEX user_key_type_time (user, \`key\`, type, time), ADD INDEX user_key_time (user, \`key\`, time); ALTER TABLE uploads ADD PRIMARY KEY (id), ADD INDEX container (container), ADD INDEX thread (thread); ALTER TABLE users ADD PRIMARY KEY (id), ADD UNIQUE KEY username (username); ALTER TABLE relationships_undirected ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE relationships_directed ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE one_time_keys ADD PRIMARY KEY (session, one_time_key); ALTER TABLE user_messages ADD INDEX recipient_time (recipient, time), ADD INDEX recipient_thread_time (recipient, thread, time), ADD INDEX thread (thread), ADD PRIMARY KEY (recipient, message); ALTER TABLE ids MODIFY id bigint(20) NOT NULL AUTO_INCREMENT; ALTER TABLE settings ADD PRIMARY KEY (user, name); ALTER TABLE metadata ADD PRIMARY KEY (name); ALTER TABLE policy_acknowledgments ADD PRIMARY KEY (user, policy); ALTER TABLE siwe_nonces ADD PRIMARY KEY (nonce); ALTER TABLE message_search ADD PRIMARY KEY (original_message_id), ADD FULLTEXT INDEX processed_content (processed_content); ALTER TABLE invite_links ADD PRIMARY KEY (id), ADD UNIQUE KEY (name), ADD INDEX community_primary (community, \`primary\`); ALTER TABLE olm_sessions ADD PRIMARY KEY (cookie_id, is_content); ALTER TABLE olm_accounts ADD PRIMARY KEY (is_content); `, { multipleStatements: true }, ); } async function createUsers() { - const [user1, user2] = sortIDs(bots.commbot.userID, ashoat.id); + const [user1, user2] = sortUserIDs(bots.commbot.userID, ashoat.id); await dbQuery( SQL` INSERT INTO ids (id, table_name) VALUES (${bots.commbot.userID}, 'users'), (${ashoat.id}, 'users'); INSERT INTO users (id, username, hash, avatar, creation_time) VALUES (${bots.commbot.userID}, 'commbot', '', NULL, 1530049900980), (${ashoat.id}, 'ashoat', '', NULL, 1463588881886); INSERT INTO relationships_undirected (user1, user2, status) VALUES (${user1}, ${user2}, ${undirectedStatus.KNOW_OF}); `, { multipleStatements: true }, ); } const createThreadOptions = { forceAddMembers: true }; async function createThreads() { const insertIDsPromise = dbQuery(SQL` INSERT INTO ids (id, table_name) VALUES (${genesis().id}, 'threads'), (${bots.commbot.staffThreadID}, 'threads') `); const ashoatViewer = createScriptViewer(ashoat.id); const createGenesisPromise = createThread( ashoatViewer, { id: genesis().id, type: threadTypes.GENESIS, name: genesis().name, description: genesis().description, initialMemberIDs: [bots.commbot.userID], }, createThreadOptions, ); await Promise.all([insertIDsPromise, createGenesisPromise]); const commbotViewer = createScriptViewer(bots.commbot.userID); await createThread( commbotViewer, { id: bots.commbot.staffThreadID, type: threadTypes.COMMUNITY_SECRET_SUBTHREAD, initialMemberIDs: [ashoat.id], }, createThreadOptions, ); } async function setUpMetadataTable() { await updateDBVersion(newDatabaseVersion); } export { setupDB }; diff --git a/keyserver/src/updaters/relationship-updaters.js b/keyserver/src/updaters/relationship-updaters.js index 17885c634..e1abd09b7 100644 --- a/keyserver/src/updaters/relationship-updaters.js +++ b/keyserver/src/updaters/relationship-updaters.js @@ -1,371 +1,371 @@ // @flow import invariant from 'invariant'; -import { sortIDs } from 'lib/shared/relationship-utils.js'; +import { sortUserIDs } from 'lib/shared/relationship-utils.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { type RelationshipRequest, type RelationshipErrors, type UndirectedRelationshipRow, relationshipActions, undirectedStatus, directedStatus, } from 'lib/types/relationship-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { NewThreadResponse } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types-enum.js'; import { type UpdateData } from 'lib/types/update-types.js'; import { cartesianProduct } from 'lib/utils/array.js'; import { ServerError } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import createMessages from '../creators/message-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL, mergeOrConditions } from '../database/database.js'; import { fetchFriendRequestRelationshipOperations } from '../fetchers/relationship-fetchers.js'; import { fetchUserInfos } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; async function updateRelationships( viewer: Viewer, request: RelationshipRequest, ): Promise { const { action } = request; if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const uniqueUserIDs = [...new Set(request.userIDs)]; const users = await fetchUserInfos(uniqueUserIDs); let errors: RelationshipErrors = {}; const userIDs: string[] = []; for (const userID of uniqueUserIDs) { if (userID === viewer.userID || !users[userID].username) { const acc = errors.invalid_user || []; errors.invalid_user = [...acc, userID]; } else { userIDs.push(userID); } } if (!userIDs.length) { return Object.freeze({ ...errors }); } const updateIDs = []; if (action === relationshipActions.FRIEND) { // We have to create personal threads before setting the relationship // status. By doing that we make sure that failed thread creation is // reported to the caller and can be repeated - there should be only // one PERSONAL thread per a pair of users and we can safely call it // repeatedly. const threadIDPerUser = await createPersonalThreads(viewer, request); const { userRelationshipOperations, errors: friendRequestErrors } = await fetchFriendRequestRelationshipOperations(viewer, userIDs); errors = { ...errors, ...friendRequestErrors }; const undirectedInsertRows = []; const directedInsertRows = []; const directedDeleteIDs = []; const messageDatas = []; const now = Date.now(); for (const userID in userRelationshipOperations) { const operations = userRelationshipOperations[userID]; - const ids = sortIDs(viewer.userID, userID); + const ids = sortUserIDs(viewer.userID, userID); if (operations.length) { updateIDs.push(userID); } for (const operation of operations) { if (operation === 'delete_directed') { directedDeleteIDs.push(userID); } else if (operation === 'friend') { const [user1, user2] = ids; const status = undirectedStatus.FRIEND; undirectedInsertRows.push({ user1, user2, status }); messageDatas.push({ type: messageTypes.UPDATE_RELATIONSHIP, threadID: threadIDPerUser[userID], creatorID: viewer.userID, targetID: userID, time: now, operation: 'request_accepted', }); } else if (operation === 'pending_friend') { const status = directedStatus.PENDING_FRIEND; directedInsertRows.push([viewer.userID, userID, status]); messageDatas.push({ type: messageTypes.UPDATE_RELATIONSHIP, threadID: threadIDPerUser[userID], creatorID: viewer.userID, targetID: userID, time: now, operation: 'request_sent', }); } else if (operation === 'know_of') { const [user1, user2] = ids; const status = undirectedStatus.KNOW_OF; undirectedInsertRows.push({ user1, user2, status }); } else { invariant(false, `unexpected relationship operation ${operation}`); } } } const promises: Array> = [ updateUndirectedRelationships(undirectedInsertRows), ]; if (directedInsertRows.length) { const directedInsertQuery = SQL` INSERT INTO relationships_directed (user1, user2, status) VALUES ${directedInsertRows} ON DUPLICATE KEY UPDATE status = VALUE(status) `; promises.push(dbQuery(directedInsertQuery)); } if (directedDeleteIDs.length) { const directedDeleteQuery = SQL` DELETE FROM relationships_directed WHERE (user1 = ${viewer.userID} AND user2 IN (${directedDeleteIDs})) OR (status = ${directedStatus.PENDING_FRIEND} AND user1 IN (${directedDeleteIDs}) AND user2 = ${viewer.userID}) `; promises.push(dbQuery(directedDeleteQuery)); } if (messageDatas.length > 0) { promises.push(createMessages(viewer, messageDatas, 'broadcast')); } await Promise.all(promises); } else if (action === relationshipActions.UNFRIEND) { updateIDs.push(...userIDs); const updateRows = userIDs.map(userID => { - const [user1, user2] = sortIDs(viewer.userID, userID); + const [user1, user2] = sortUserIDs(viewer.userID, userID); return { user1, user2, status: undirectedStatus.KNOW_OF }; }); const deleteQuery = SQL` DELETE FROM relationships_directed WHERE status = ${directedStatus.PENDING_FRIEND} AND (user1 = ${viewer.userID} AND user2 IN (${userIDs}) OR user1 IN (${userIDs}) AND user2 = ${viewer.userID}) `; await Promise.all([ updateUndirectedRelationships(updateRows, false), dbQuery(deleteQuery), ]); } else if (action === relationshipActions.BLOCK) { updateIDs.push(...userIDs); const directedRows = []; const undirectedRows = []; for (const userID of userIDs) { directedRows.push([viewer.userID, userID, directedStatus.BLOCKED]); - const [user1, user2] = sortIDs(viewer.userID, userID); + const [user1, user2] = sortUserIDs(viewer.userID, userID); undirectedRows.push({ user1, user2, status: undirectedStatus.KNOW_OF }); } const directedInsertQuery = SQL` INSERT INTO relationships_directed (user1, user2, status) VALUES ${directedRows} ON DUPLICATE KEY UPDATE status = VALUE(status) `; const directedDeleteQuery = SQL` DELETE FROM relationships_directed WHERE status = ${directedStatus.PENDING_FRIEND} AND user1 IN (${userIDs}) AND user2 = ${viewer.userID} `; await Promise.all([ dbQuery(directedInsertQuery), dbQuery(directedDeleteQuery), updateUndirectedRelationships(undirectedRows, false), ]); } else if (action === relationshipActions.UNBLOCK) { updateIDs.push(...userIDs); const query = SQL` DELETE FROM relationships_directed WHERE status = ${directedStatus.BLOCKED} AND user1 = ${viewer.userID} AND user2 IN (${userIDs}) `; await dbQuery(query); } else { invariant(false, `action ${action} is invalid or not supported currently`); } await createUpdates( updateDatasForUserPairs(cartesianProduct([viewer.userID], updateIDs)), ); return Object.freeze({ ...errors }); } function updateDatasForUserPairs( userPairs: $ReadOnlyArray<[string, string]>, ): UpdateData[] { const time = Date.now(); const updateDatas: Array = []; for (const [user1, user2] of userPairs) { updateDatas.push({ type: updateTypes.UPDATE_USER, userID: user1, time, updatedUserID: user2, }); updateDatas.push({ type: updateTypes.UPDATE_USER, userID: user2, time, updatedUserID: user1, }); } return updateDatas; } async function updateUndirectedRelationships( changeset: UndirectedRelationshipRow[], greatest: boolean = true, ) { if (!changeset.length) { return; } const rows = changeset.map(row => [row.user1, row.user2, row.status]); const query = SQL` INSERT INTO relationships_undirected (user1, user2, status) VALUES ${rows} `; if (greatest) { query.append( SQL`ON DUPLICATE KEY UPDATE status = GREATEST(status, VALUE(status))`, ); } else { query.append(SQL`ON DUPLICATE KEY UPDATE status = VALUE(status)`); } await dbQuery(query); } async function updateChangedUndirectedRelationships( changeset: UndirectedRelationshipRow[], ): Promise { if (changeset.length === 0) { return []; } const user2ByUser1: Map> = new Map(); for (const { user1, user2 } of changeset) { if (!user2ByUser1.has(user1)) { user2ByUser1.set(user1, new Set()); } user2ByUser1.get(user1)?.add(user2); } const selectQuery = SQL` SELECT user1, user2, status FROM relationships_undirected WHERE `; const conditions = []; for (const [user1, users] of user2ByUser1) { conditions.push(SQL`(user1 = ${user1} AND user2 IN (${[...users]}))`); } selectQuery.append(mergeOrConditions(conditions)); const [result] = await dbQuery(selectQuery); const existingStatuses = new Map(); for (const row of result) { existingStatuses.set(`${row.user1}|${row.user2}`, row.status); } const insertRows = []; for (const row of changeset) { const existingStatus = existingStatuses.get(`${row.user1}|${row.user2}`); if (!existingStatus || existingStatus < row.status) { insertRows.push([row.user1, row.user2, row.status]); } } if (insertRows.length === 0) { return []; } const insertQuery = SQL` INSERT INTO relationships_undirected (user1, user2, status) VALUES ${insertRows} ON DUPLICATE KEY UPDATE status = GREATEST(status, VALUE(status)) `; await dbQuery(insertQuery); return updateDatasForUserPairs( insertRows.map(([user1, user2]) => [user1, user2]), ); } async function createPersonalThreads( viewer: Viewer, request: RelationshipRequest, ) { invariant( request.action === relationshipActions.FRIEND, 'We should only create a PERSONAL threads when sending a FRIEND request, ' + `but we tried to do that for ${request.action}`, ); const threadIDPerUser: { [string]: string } = {}; const personalThreadsQuery = SQL` SELECT t.id AS threadID, m2.user AS user2 FROM threads t INNER JOIN memberships m1 ON m1.thread = t.id AND m1.user = ${viewer.userID} INNER JOIN memberships m2 ON m2.thread = t.id AND m2.user IN (${request.userIDs}) WHERE t.type = ${threadTypes.PERSONAL} AND m1.role > 0 AND m2.role > 0 `; const [personalThreadsResult] = await dbQuery(personalThreadsQuery); for (const row of personalThreadsResult) { const user2 = row.user2.toString(); threadIDPerUser[user2] = row.threadID.toString(); } const threadCreationPromises: { [string]: Promise } = {}; for (const userID of request.userIDs) { if (threadIDPerUser[userID]) { continue; } threadCreationPromises[userID] = createThread( viewer, { type: threadTypes.PERSONAL, initialMemberIDs: [userID], }, { forceAddMembers: true, updatesForCurrentSession: 'broadcast' }, ); } const personalThreadPerUser = await promiseAll(threadCreationPromises); for (const userID in personalThreadPerUser) { const newThread = personalThreadPerUser[userID]; threadIDPerUser[userID] = newThread.newThreadID; } return threadIDPerUser; } export { updateRelationships, updateDatasForUserPairs, updateUndirectedRelationships, updateChangedUndirectedRelationships, }; diff --git a/keyserver/src/utils/relationship-changeset.js b/keyserver/src/utils/relationship-changeset.js index ac1499e35..b281253c9 100644 --- a/keyserver/src/utils/relationship-changeset.js +++ b/keyserver/src/utils/relationship-changeset.js @@ -1,100 +1,100 @@ // @flow import invariant from 'invariant'; -import { sortIDs } from 'lib/shared/relationship-utils.js'; +import { sortUserIDs } from 'lib/shared/relationship-utils.js'; import { type UndirectedRelationshipRow, undirectedStatus, } from 'lib/types/relationship-types.js'; type RelationshipStatus = 'existing' | 'potentially_missing'; class RelationshipChangeset { relationships: Map = new Map(); finalized: boolean = false; static _getKey(userA: string, userB: string): string { - const [user1, user2] = sortIDs(userA, userB); + const [user1, user2] = sortUserIDs(userA, userB); return `${user1}|${user2}`; } _setRelationshipForKey(key: string, status: RelationshipStatus) { invariant( !this.finalized, 'attempting to set relationship on finalized RelationshipChangeset', ); const currentStatus = this.relationships.get(key); if ( currentStatus === 'existing' || (currentStatus && status === 'potentially_missing') ) { return; } this.relationships.set(key, status); } _setRelationship(userA: string, userB: string, status: RelationshipStatus) { if (userA === userB) { return; } const key = RelationshipChangeset._getKey(userA, userB); this._setRelationshipForKey(key, status); } setAllRelationshipsExist(userIDs: $ReadOnlyArray) { for (let i = 0; i < userIDs.length; i++) { for (let j = i + 1; j < userIDs.length; j++) { this._setRelationship(userIDs[i], userIDs[j], 'existing'); } } } setAllRelationshipsNeeded(userIDs: $ReadOnlyArray) { for (let i = 0; i < userIDs.length; i++) { for (let j = i + 1; j < userIDs.length; j++) { this._setRelationship(userIDs[i], userIDs[j], 'potentially_missing'); } } } setRelationshipExists(userA: string, userB: string) { this._setRelationship(userA, userB, 'existing'); } setRelationshipsNeeded(userID: string, otherUserIDs: $ReadOnlyArray) { for (const otherUserID of otherUserIDs) { this._setRelationship(userID, otherUserID, 'potentially_missing'); } } addAll(other: RelationshipChangeset) { other.finalized = true; for (const [key, status] of other.relationships) { this._setRelationshipForKey(key, status); } } _getRows(): UndirectedRelationshipRow[] { const rows = []; for (const [key, status] of this.relationships) { if (status === 'existing') { continue; } const [user1, user2] = key.split('|'); rows.push({ user1, user2, status: undirectedStatus.KNOW_OF }); } return rows; } getRows(): UndirectedRelationshipRow[] { this.finalized = true; return this._getRows(); } getRowCount(): number { return this._getRows().length; } } export default RelationshipChangeset; diff --git a/lib/shared/relationship-utils.js b/lib/shared/relationship-utils.js index fb98e9135..a10cc6687 100644 --- a/lib/shared/relationship-utils.js +++ b/lib/shared/relationship-utils.js @@ -1,114 +1,122 @@ // @flow import invariant from 'invariant'; import { type RelationshipButton, type UserRelationshipStatus, userRelationshipStatus, relationshipButtons, relationshipActions, type RelationshipAction, } from '../types/relationship-types.js'; import type { UserInfo } from '../types/user-types.js'; -function sortIDs(firstId: string, secondId: string): string[] { - return [Number(firstId), Number(secondId)] - .sort((a, b) => a - b) - .map(num => num.toString()); +function sortUserIDs(firstId: string, secondId: string): string[] { + if (!isNaN(Number(firstId)) && !isNaN(Number(secondId))) { + return [Number(firstId), Number(secondId)] + .sort((a, b) => a - b) + .map(num => num.toString()); + } else if (!isNaN(Number(firstId))) { + return [firstId, secondId]; + } else if (!isNaN(Number(secondId))) { + return [secondId, firstId]; + } + + return [firstId, secondId].sort(); } function getAvailableRelationshipButtons( userInfo: UserInfo, ): RelationshipButton[] { const relationship = userInfo.relationshipStatus; if (relationship === userRelationshipStatus.FRIEND) { return [relationshipButtons.UNFRIEND, relationshipButtons.BLOCK]; } else if (relationship === userRelationshipStatus.BLOCKED_VIEWER) { return [relationshipButtons.BLOCK]; } else if ( relationship === userRelationshipStatus.BOTH_BLOCKED || relationship === userRelationshipStatus.BLOCKED_BY_VIEWER ) { return [relationshipButtons.UNBLOCK]; } else if (relationship === userRelationshipStatus.REQUEST_RECEIVED) { return [ relationshipButtons.ACCEPT, relationshipButtons.REJECT, relationshipButtons.BLOCK, ]; } else if (relationship === userRelationshipStatus.REQUEST_SENT) { return [relationshipButtons.WITHDRAW, relationshipButtons.BLOCK]; } else { return [relationshipButtons.FRIEND, relationshipButtons.BLOCK]; } } function relationshipBlockedInEitherDirection( relationshipStatus: ?UserRelationshipStatus, ): boolean { return ( relationshipStatus === userRelationshipStatus.BLOCKED_VIEWER || relationshipStatus === userRelationshipStatus.BLOCKED_BY_VIEWER || relationshipStatus === userRelationshipStatus.BOTH_BLOCKED ); } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getRelationshipDispatchAction( relationshipButton: RelationshipButton, ): RelationshipAction { if (relationshipButton === relationshipButtons.BLOCK) { return relationshipActions.BLOCK; } else if ( relationshipButton === relationshipButtons.FRIEND || relationshipButton === relationshipButtons.ACCEPT ) { return relationshipActions.FRIEND; } else if ( relationshipButton === relationshipButtons.UNFRIEND || relationshipButton === relationshipButtons.REJECT || relationshipButton === relationshipButtons.WITHDRAW ) { return relationshipActions.UNFRIEND; } else if (relationshipButton === relationshipButtons.UNBLOCK) { return relationshipActions.UNBLOCK; } invariant(false, 'relationshipButton conditions should be exhaustive'); } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getRelationshipActionText( relationshipButton: RelationshipButton, username: string, ): string { switch (relationshipButton) { case relationshipButtons.BLOCK: return `Block ${username}`; case relationshipButtons.FRIEND: return `Add ${username} to friends`; case relationshipButtons.UNFRIEND: return `Unfriend ${username}`; case relationshipButtons.UNBLOCK: return `Unblock ${username}`; case relationshipButtons.ACCEPT: return `Accept friend request from ${username}`; case relationshipButtons.REJECT: return `Reject friend request from ${username}`; case relationshipButtons.WITHDRAW: return `Withdraw request to friend ${username}`; default: invariant(false, 'invalid relationshipButton value'); } } export { - sortIDs, + sortUserIDs, getAvailableRelationshipButtons, relationshipBlockedInEitherDirection, getRelationshipDispatchAction, getRelationshipActionText, }; diff --git a/lib/shared/relationship-utils.test.js b/lib/shared/relationship-utils.test.js new file mode 100644 index 000000000..fc2a2f63f --- /dev/null +++ b/lib/shared/relationship-utils.test.js @@ -0,0 +1,17 @@ +// @flow + +import { sortUserIDs } from './relationship-utils.js'; + +describe('sortUserIDs', () => { + it('should sort id that are numbers, as numbers', () => { + expect(sortUserIDs('100', '99')).toEqual(['99', '100']); + }); + it('should always sort uuid after id that is a number', () => { + expect(sortUserIDs('100A', '99')).toEqual(['99', '100A']); + expect(sortUserIDs('100', '99A')).toEqual(['100', '99A']); + }); + it('should sort uuid lexicographically', () => { + expect(sortUserIDs('100A', '99A')).toEqual(['100A', '99A']); + expect(sortUserIDs('DEF', 'abc')).toEqual(['DEF', 'abc']); + }); +});