diff --git a/ts/node/migration/session/migrationV33.ts b/ts/node/migration/session/migrationV33.ts new file mode 100644 index 000000000..8d5218328 --- /dev/null +++ b/ts/node/migration/session/migrationV33.ts @@ -0,0 +1,417 @@ +/* eslint-disable no-unused-expressions */ +import * as BetterSqlite3 from '@signalapp/better-sqlite3'; +import { + ContactInfoSet, + ContactsConfigWrapperNode, + ConvoInfoVolatileWrapperNode, + DisappearingMessageConversationType, + UserConfigWrapperNode, +} from 'libsession_util_nodejs'; +import { isArray, isEmpty, isEqual, isFinite, isNumber } from 'lodash'; +import { CONVERSATION_PRIORITIES } from '../../../models/conversationAttributes'; +import { CONFIG_DUMP_TABLE, ConfigDumpRow } from '../../../types/sqlSharedTypes'; +import { CONVERSATIONS_TABLE, MESSAGES_TABLE, toSqliteBoolean } from '../../database_utility'; +import { + getBlockedNumbersDuringMigration, + getOurAccountKeys, + hasDebugEnvVariable, + writeSessionSchemaVersion, +} from '../sessionMigrations'; +import { DisappearingMessageMode } from '../../../util/expiringMessages'; +import { fromHexToArray } from '../../../session/utils/String'; + +function getContactInfoFromDBValues({ + id, + dbApproved, + dbApprovedMe, + dbBlocked, + dbName, + dbNickname, + priority, + dbProfileUrl, + dbProfileKey, + dbCreatedAtSeconds, + expirationType, + expireTimer, +}: { + id: string; + dbApproved: boolean; + dbApprovedMe: boolean; + dbBlocked: boolean; + dbNickname: string | undefined; + dbName: string | undefined; + priority: number; + dbProfileUrl: string | undefined; + dbProfileKey: string | undefined; + dbCreatedAtSeconds: number; + expirationType: string | undefined; + expireTimer: number | undefined; +}): ContactInfoSet { + const wrapperContact: ContactInfoSet = { + id, + approved: !!dbApproved, + approvedMe: !!dbApprovedMe, + blocked: !!dbBlocked, + priority, + nickname: dbNickname, + name: dbName, + createdAtSeconds: dbCreatedAtSeconds, + expirationMode: + // string must be a valid mode + expirationType && DisappearingMessageMode.includes(expirationType) + ? (expirationType as DisappearingMessageConversationType) + : 'off', + expirationTimerSeconds: + !!expireTimer && isFinite(expireTimer) && expireTimer > 0 ? expireTimer * 1000 : 0, + }; + + if ( + wrapperContact.profilePicture?.url !== dbProfileUrl || + !isEqual(wrapperContact.profilePicture?.key, dbProfileKey) + ) { + wrapperContact.profilePicture = { + url: dbProfileUrl || null, + key: dbProfileKey && !isEmpty(dbProfileKey) ? fromHexToArray(dbProfileKey) : null, + }; + } + + return wrapperContact; +} + +function insertContactIntoContactWrapper( + contact: any, + blockedNumbers: Array, + contactsConfigWrapper: ContactsConfigWrapperNode | null, // set this to null to only insert into the convo volatile wrapper (i.e. for ourConvo case) + volatileConfigWrapper: ConvoInfoVolatileWrapperNode, + db: BetterSqlite3.Database +) { + if (contactsConfigWrapper !== null) { + const dbApproved = !!contact.isApproved || false; + const dbApprovedMe = !!contact.didApproveMe || false; + const dbBlocked = blockedNumbers.includes(contact.id); + const priority = contact.priority || CONVERSATION_PRIORITIES.default; + + const wrapperContact = getContactInfoFromDBValues({ + id: contact.id, + dbApproved, + dbApprovedMe, + dbBlocked, + dbName: contact.displayNameInProfile || undefined, + dbNickname: contact.nickname || undefined, + dbProfileKey: contact.profileKey || undefined, + dbProfileUrl: contact.avatarPointer || undefined, + priority, + dbCreatedAtSeconds: Math.floor((contact.active_at || Date.now()) / 1000), + expirationType: contact.expirationType || 'off', + expireTimer: contact.expirationTimer || 0, + }); + + try { + hasDebugEnvVariable && console.info('Inserting contact into wrapper: ', wrapperContact); + contactsConfigWrapper.set(wrapperContact); + } catch (e) { + console.error( + `contactsConfigWrapper.set during migration failed with ${e.message} for id: ${contact.id}` + ); + // the wrapper did not like something. Try again with just the boolean fields as it's most likely the issue is with one of the strings (which could be recovered) + try { + hasDebugEnvVariable && console.info('Inserting edited contact into wrapper: ', contact.id); + + contactsConfigWrapper.set( + getContactInfoFromDBValues({ + id: contact.id, + dbApproved, + dbApprovedMe, + dbBlocked, + dbName: undefined, + dbNickname: undefined, + dbProfileKey: undefined, + dbProfileUrl: undefined, + priority: CONVERSATION_PRIORITIES.default, + dbCreatedAtSeconds: Math.floor(Date.now() / 1000), + expirationType: 'off', + expireTimer: 0, + }) + ); + } catch (err2) { + // there is nothing else we can do here + console.error( + `contactsConfigWrapper.set during migration failed with ${err2.message} for id: ${contact.id}. Skipping contact entirely` + ); + } + } + } + + try { + const rows = db + .prepare( + ` + SELECT MAX(COALESCE(sent_at, 0)) AS max_sent_at + FROM ${MESSAGES_TABLE} WHERE + conversationId = $conversationId AND + unread = $unread; + ` + ) + .get({ + conversationId: contact.id, + unread: toSqliteBoolean(false), // we want to find the message read with the higher sentAt timestamp + }); + + const maxRead = rows?.max_sent_at; + const lastRead = isNumber(maxRead) && isFinite(maxRead) ? maxRead : 0; + hasDebugEnvVariable && + console.info(`Inserting contact into volatile wrapper maxread: ${contact.id} :${lastRead}`); + volatileConfigWrapper.set1o1(contact.id, lastRead, false); + } catch (e) { + console.error( + `volatileConfigWrapper.set1o1 during migration failed with ${e.message} for id: ${contact.id}. skipping` + ); + } +} + +export default function updateToSessionSchemaVersion33( + currentVersion: number, + db: BetterSqlite3.Database +) { + const targetVersion = 33; + if (currentVersion >= targetVersion) { + return; + } + + // TODO we actually want to update the config wrappers that relate to disappearing messages with the type and seconds + + console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`); + db.transaction(() => { + try { + const keys = getOurAccountKeys(db); + + const userAlreadyCreated = !!keys && !isEmpty(keys.privateEd25519); + + if (!userAlreadyCreated) { + throw new Error('privateEd25519 was empty. Considering no users are logged in'); + } + + const { privateEd25519, publicKeyHex } = keys; + + // Conversation changes + // TODO can this be moved into libsession completely + db.prepare( + `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN expirationType TEXT DEFAULT "off";` + ).run(); + + db.prepare( + `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastDisappearingMessageChangeTimestamp INTEGER DEFAULT 0;` + ).run(); + + db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN hasOutdatedClient TEXT;`).run(); + + // region Disappearing Messages Note to Self + const noteToSelfInfo = db + .prepare( + `UPDATE ${CONVERSATIONS_TABLE} SET + expirationType = $expirationType + WHERE id = $id AND type = 'private' AND expireTimer > 0;` + ) + .run({ expirationType: 'deleteAfterSend', id: publicKeyHex }); + + if (noteToSelfInfo.changes) { + const ourConversation = db + .prepare(`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE id = $id`) + .get({ id: publicKeyHex }); + + const expirySeconds = ourConversation.expireTimer || 0; + + // TODO update with Audric's snippet + // Get existing config wrapper dump and update it + const userConfigWrapperDump = db + .prepare(`SELECT * FROM ${CONFIG_DUMP_TABLE} WHERE variant = 'UserConfig';`) + .get() as ConfigDumpRow | undefined; + + if (userConfigWrapperDump) { + const userConfigData = userConfigWrapperDump.data; + const userProfileWrapper = new UserConfigWrapperNode(privateEd25519, userConfigData); + + userProfileWrapper.setNoteToSelfExpiry(expirySeconds); + + // dump the user wrapper content and save it to the DB + const userDump = userProfileWrapper.dump(); + + const configDumpInfo = db + .prepare( + `INSERT OR REPLACE INTO ${CONFIG_DUMP_TABLE} ( + publicKey, + variant, + data + ) values ( + $publicKey, + $variant, + $data + );` + ) + .run({ + publicKey: publicKeyHex, + variant: 'UserConfig', + data: userDump, + }); + + // TODO Cleanup logging + console.log( + '===================== userConfigWrapperDump configDumpInfo', + configDumpInfo, + '=======================' + ); + } else { + console.log( + '===================== userConfigWrapperDump not found =======================' + ); + } + } + // endregion + + // region Disappearing Messages Private Conversations + const privateConversationsInfo = db + .prepare( + `UPDATE ${CONVERSATIONS_TABLE} SET + expirationType = $expirationType + WHERE type = 'private' AND expirationType = 'off' AND expireTimer > 0;` + ) + .run({ expirationType: 'deleteAfterRead' }); + + if (privateConversationsInfo.changes) { + // this filter is based on the `isContactToStoreInWrapper` function. Note, it has been expanded to check if disappearing messages is on + const contactsToUpdateInWrapper = db + .prepare( + `SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'private' AND active_at > 0 AND priority <> ${CONVERSATION_PRIORITIES.hidden} AND (didApproveMe OR isApproved) AND id <> '$us' AND id NOT LIKE '15%' AND id NOT LIKE '25%' AND expirationType = 'deleteAfterRead' AND expireTimer > 0;` + ) + .all({ + us: publicKeyHex, + }); + + if (isArray(contactsToUpdateInWrapper) && contactsToUpdateInWrapper.length) { + const blockedNumbers = getBlockedNumbersDuringMigration(db); + const contactsWrapperDump = db + .prepare(`SELECT * FROM ${CONFIG_DUMP_TABLE} WHERE variant = 'ContactConfig';`) + .get() as ConfigDumpRow | undefined; + const volatileInfoConfigWrapperDump = db + .prepare( + `SELECT * FROM ${CONFIG_DUMP_TABLE} WHERE variant = 'ConvoInfoVolatileConfig';` + ) + .get() as ConfigDumpRow | undefined; + + if (contactsWrapperDump && volatileInfoConfigWrapperDump) { + const contactsData = contactsWrapperDump.data; + const contactsConfigWrapper = new ContactsConfigWrapperNode( + privateEd25519, + contactsData + ); + const volatileInfoData = volatileInfoConfigWrapperDump.data; + const volatileInfoConfigWrapper = new ConvoInfoVolatileWrapperNode( + privateEd25519, + volatileInfoData + ); + + console.info( + `===================== Starting contact update into wrapper ${contactsToUpdateInWrapper?.length} =======================` + ); + + contactsToUpdateInWrapper.forEach(contact => { + insertContactIntoContactWrapper( + contact, + blockedNumbers, + contactsConfigWrapper, + volatileInfoConfigWrapper, + db + ); + }); + + console.info( + '===================== Done with contact updating =======================' + ); + + // dump the user wrapper content and save it to the DB + const contactsDump = contactsConfigWrapper.dump(); + const contactsDumpInfo = db + .prepare( + `INSERT OR REPLACE INTO ${CONFIG_DUMP_TABLE} ( + publicKey, + variant, + data + ) values ( + $publicKey, + $variant, + $data + );` + ) + .run({ + publicKey: publicKeyHex, + variant: 'ContactConfig', + data: contactsDump, + }); + + // TODO Cleanup logging + console.log( + '===================== contactsConfigWrapper contactsDumpInfo', + contactsDumpInfo, + '=======================' + ); + + const volatileInfoConfigDump = volatileInfoConfigWrapper.dump(); + const volatileInfoConfigDumpInfo = db + .prepare( + `INSERT OR REPLACE INTO ${CONFIG_DUMP_TABLE} ( + publicKey, + variant, + data + ) values ( + $publicKey, + $variant, + $data + );` + ) + .run({ + publicKey: publicKeyHex, + variant: 'ConvoInfoVolatileConfig', + data: volatileInfoConfigDump, + }); + + // TODO Cleanup logging + console.log( + '===================== volatileInfoConfigWrapper volatileInfoConfigDumpInfo', + volatileInfoConfigDumpInfo, + '=======================' + ); + } else { + console.log( + '===================== contactsWrapperDump or volatileInfoConfigWrapperDump was not found =======================' + ); + } + } + } + + // endregion + + // region Disappearing Messages Groups + db.prepare( + `UPDATE ${CONVERSATIONS_TABLE} SET + expirationType = $expirationType + WHERE type = 'group' AND id LIKE '05%' AND expireTimer > 0;` + ).run({ expirationType: 'deleteAfterSend' }); + // endregion + + // Message changes + db.prepare(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN expirationType TEXT;`).run(); + } catch (e) { + console.error( + `Failed to migrate to disappearing messages v2. Might just not have a logged in user yet? `, + e.message, + e.stack, + e + ); + // if we get an exception here, most likely no users are logged in yet. We can just continue the transaction and the wrappers will be created when a user creates a new account. + } + + writeSessionSchemaVersion(targetVersion, db); + })(); + + console.log(`updateToSessionSchemaVersion${targetVersion}: success!`); +} diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 25589ebb3..18c4e415a 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -15,7 +15,6 @@ import { HexKeyPair } from '../../receiver/keypairs'; import { fromHexToArray } from '../../session/utils/String'; import { CONFIG_DUMP_TABLE, - ConfigDumpRow, getCommunityInfoFromDBValues, getContactInfoFromDBValues, getLegacyGroupInfoFromDBValues, @@ -36,8 +35,9 @@ import { import { getIdentityKeys, sqlNode } from '../sql'; import { sleepFor } from '../../session/utils/Promise'; +// import updateToSessionSchemaVersion33 from './session/migrationV33'; -const hasDebugEnvVariable = Boolean(process.env.SESSION_DEBUG); +export const hasDebugEnvVariable = Boolean(process.env.SESSION_DEBUG); // eslint:disable: quotemark one-variable-per-declaration no-unused-expression @@ -104,7 +104,7 @@ const LOKI_SCHEMA_VERSIONS = [ updateToSessionSchemaVersion30, updateToSessionSchemaVersion31, updateToSessionSchemaVersion32, - updateToSessionSchemaVersion33, + // updateToSessionSchemaVersion33, ]; function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) { @@ -1224,7 +1224,6 @@ function insertContactIntoContactWrapper( const dbApprovedMe = !!contact.didApproveMe || false; const dbBlocked = blockedNumbers.includes(contact.id); const priority = contact.priority || CONVERSATION_PRIORITIES.default; - // const expirationTimerSeconds = contact.expireTimer || 0; const wrapperContact = getContactInfoFromDBValues({ id: contact.id, @@ -1237,7 +1236,6 @@ function insertContactIntoContactWrapper( dbProfileUrl: contact.avatarPointer || undefined, priority, dbCreatedAtSeconds: Math.floor((contact.active_at || Date.now()) / 1000), - // expirationTimerSeconds, // FIXME WILL add expirationMode here }); try { @@ -1262,7 +1260,6 @@ function insertContactIntoContactWrapper( dbProfileUrl: undefined, priority: CONVERSATION_PRIORITIES.default, dbCreatedAtSeconds: Math.floor(Date.now() / 1000), - // expirationTimerSeconds: 0, // FIXME WILL add expirationMode here }) ); } catch (err2) { @@ -1440,7 +1437,7 @@ function insertLegacyGroupIntoWrapper( } } -function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) { +export function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) { try { const blockedItem = sqlNode.getItemById('blocked', db); if (!blockedItem) { @@ -1588,7 +1585,7 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite * @param db the database * @returns the keys { privateEd25519: string, publicEd25519: string } */ -function getOurAccountKeys(db: BetterSqlite3.Database) { +export function getOurAccountKeys(db: BetterSqlite3.Database) { const keys = getIdentityKeys(db); return keys; } @@ -1860,143 +1857,11 @@ function updateToSessionSchemaVersion32(currentVersion: number, db: BetterSqlite console.log(`updateToSessionSchemaVersion${targetVersion}: success!`); } -function updateToSessionSchemaVersion33(currentVersion: number, db: BetterSqlite3.Database) { - const targetVersion = 33; - if (currentVersion >= targetVersion) { - return; - } - - // TODO we actually want to update the config wrappers that relate to disappearing messages with the type and seconds - - console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`); - db.transaction(() => { - try { - const keys = getOurAccountKeys(db); - - const userAlreadyCreated = !!keys && !isEmpty(keys.privateEd25519); - - if (!userAlreadyCreated) { - throw new Error('privateEd25519 was empty. Considering no users are logged in'); - } - - const { privateEd25519, publicKeyHex } = keys; - - // Conversation changes - // TODO can this be moved into libsession completely - db.prepare( - `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN expirationType TEXT DEFAULT "off";` - ).run(); - - db.prepare( - `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastDisappearingMessageChangeTimestamp INTEGER DEFAULT 0;` - ).run(); - - db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN hasOutdatedClient TEXT;`).run(); - - // region Note to Self - const noteToSelfInfo = db - .prepare( - `UPDATE ${CONVERSATIONS_TABLE} SET - expirationType = $expirationType - WHERE id = $id AND type = 'private' AND expireTimer > 0;` - ) - .run({ expirationType: 'deleteAfterSend', id: publicKeyHex }); - - if (noteToSelfInfo.changes) { - const ourConversation = db - .prepare(`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE id = $id`) - .get({ id: publicKeyHex }); - - const expirySeconds = ourConversation.expireTimer || 0; - - // Get existing config wrapper dump and update it - const userConfigWrapperDump = db - .prepare(`SELECT * FROM ${CONFIG_DUMP_TABLE} WHERE variant = 'UserConfig';`) - .get() as ConfigDumpRow | undefined; - - if (userConfigWrapperDump) { - const userConfigData = userConfigWrapperDump.data; - const userProfileWrapper = new UserConfigWrapperNode(privateEd25519, userConfigData); - - userProfileWrapper.setExpiry(expirySeconds); - - // dump the user wrapper content and save it to the DB - const userDump = userProfileWrapper.dump(); - - const configDumpInfo = db - .prepare( - `INSERT OR REPLACE INTO ${CONFIG_DUMP_TABLE} ( - publicKey, - variant, - data - ) values ( - $publicKey, - $variant, - $data - );` - ) - .run({ - publicKey: publicKeyHex, - variant: 'UserConfig', - data: userDump, - }); - - // TODO Cleanup logging - console.log( - '===================== configDumpInfo', - configDumpInfo, - '=======================' - ); - } else { - console.log( - '===================== userConfigWrapperDump not found =======================' - ); - } - } - // endregion - - // region Private Conversations - db.prepare( - `UPDATE ${CONVERSATIONS_TABLE} SET - expirationType = $expirationType - WHERE type = 'private' AND expirationType = 'off' AND expireTimer > 0;` - ).run({ expirationType: 'deleteAfterRead' }); - - // TODO add to Contact Wrapper - // if (privateConversationsInfo.changes) {} - // endregion - - // region Groups - db.prepare( - `UPDATE ${CONVERSATIONS_TABLE} SET - expirationType = $expirationType - WHERE type = 'group' AND id LIKE '05%' AND expireTimer > 0;` - ).run({ expirationType: 'deleteAfterSend' }); - // endregion - - // Message changes - db.prepare(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN expirationType TEXT;`).run(); - } catch (e) { - console.error( - `Failed to migrate to disappearing messages v2. Might just not have a logged in user yet? `, - e.message, - e.stack, - e - ); - // if we get an exception here, most likely no users are logged in yet. We can just continue the transaction and the wrappers will be created when a user creates a new account. - } - - writeSessionSchemaVersion(targetVersion, db); - })(); - - console.log(`updateToSessionSchemaVersion${targetVersion}: success!`); -} - export function printTableColumns(table: string, db: BetterSqlite3.Database) { console.info(db.pragma(`table_info('${table}');`)); } -function writeSessionSchemaVersion(newVersion: number, db: BetterSqlite3.Database) { +export function writeSessionSchemaVersion(newVersion: number, db: BetterSqlite3.Database) { db.prepare( `INSERT INTO loki_schema( version diff --git a/ts/session/utils/libsession/libsession_utils_contacts.ts b/ts/session/utils/libsession/libsession_utils_contacts.ts index fe5629d84..36926c551 100644 --- a/ts/session/utils/libsession/libsession_utils_contacts.ts +++ b/ts/session/utils/libsession/libsession_utils_contacts.ts @@ -54,7 +54,9 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise 0 - // ? expirationTimerSeconds - // : 0, - // expirationMode: 'off', //FIXME WILL add expirationMode here }; if ( diff --git a/ts/util/expiringMessages.ts b/ts/util/expiringMessages.ts index f177bae7b..3811ff4bc 100644 --- a/ts/util/expiringMessages.ts +++ b/ts/util/expiringMessages.ts @@ -18,6 +18,7 @@ export const DisappearingMessageMode = ['deleteAfterRead', 'deleteAfterSend']; export type DisappearingMessageType = typeof DisappearingMessageMode[number] | null; // TODO legacy messages support will be removed in a future release +// TODO NOTE do we need to remove 'legacy' from here? export const DisappearingMessageConversationSetting = ['off', ...DisappearingMessageMode, 'legacy']; export type DisappearingMessageConversationType = typeof DisappearingMessageConversationSetting[number]; // TODO we should make this type a bit more hardcoded than being just resolved as a string diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 86df36333..1a0677c8f 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -120,13 +120,13 @@ export const UserConfigWrapperActions: UserConfigWrapperActionsCalls = { callLibSessionWorker(['UserConfig', 'setEnableBlindedMsgRequest', enable]) as Promise< ReturnType >, - getExpiry: async () => - callLibSessionWorker(['UserConfig', 'getExpiry']) as Promise< - ReturnType + getNoteToSelfExpiry: async () => + callLibSessionWorker(['UserConfig', 'getNoteToSelfExpiry']) as Promise< + ReturnType >, - setExpiry: async (expirySeconds: number) => - callLibSessionWorker(['UserConfig', 'setExpiry', expirySeconds]) as Promise< - ReturnType + setNoteToSelfExpiry: async (expirySeconds: number) => + callLibSessionWorker(['UserConfig', 'setNoteToSelfExpiry', expirySeconds]) as Promise< + ReturnType >, };