You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/node/migration/helpers/v31.ts

367 lines
12 KiB
TypeScript

/* eslint-disable no-unused-expressions */
import * as BetterSqlite3 from '@signalapp/better-sqlite3';
import {
ContactInfoSet,
ContactsConfigWrapperNode,
ConvoInfoVolatileWrapperNode,
LegacyGroupInfo,
LegacyGroupMemberInfo,
UserGroupsWrapperNode,
} from 'libsession_util_nodejs';
import { isEmpty, isEqual, isFinite, isNumber } from 'lodash';
import { from_hex } from 'libsodium-wrappers-sumo';
import { MESSAGES_TABLE, toSqliteBoolean } from '../../database_utility';
import {
CONVERSATION_PRIORITIES,
ConversationAttributes,
} from '../../../models/conversationAttributes';
import { maybeArrayJSONtoArray } from '../../../types/sqlSharedTypes';
import { checkTargetMigration, hasDebugEnvVariable } from '../utils';
import { sqlNode } from '../../sql';
import { HexKeyPair } from '../../../receiver/keypairs';
import { fromHexToArray } from '../../../session/utils/String';
const targetVersion = 31;
/**
* This function returns a contactInfo for the wrapper to understand from the DB values.
* Created in this file so we can reuse it during the migration (node side), and from the renderer side
*/
function getContactInfoFromDBValues({
id,
dbApproved,
dbApprovedMe,
dbBlocked,
dbName,
dbNickname,
priority,
dbProfileUrl,
dbProfileKey,
dbCreatedAtSeconds,
}: {
id: string;
dbApproved: boolean;
dbApprovedMe: boolean;
dbBlocked: boolean;
dbNickname: string | undefined;
dbName: string | undefined;
priority: number;
dbProfileUrl: string | undefined;
dbProfileKey: string | undefined;
dbCreatedAtSeconds: number;
}): ContactInfoSet {
const wrapperContact: ContactInfoSet = {
id,
approved: !!dbApproved,
approvedMe: !!dbApprovedMe,
blocked: !!dbBlocked,
priority,
nickname: dbNickname,
name: dbName,
createdAtSeconds: dbCreatedAtSeconds,
};
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<string>,
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,
version: number
) {
checkTargetMigration(version, targetVersion);
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),
});
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),
})
);
} 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`
);
}
}
/**
* This function returns a CommunityInfo for the wrapper to understand from the DB values.
* It is created in this file so we can reuse it during the migration (node side), and from the renderer side
*/
function getCommunityInfoFromDBValues({
priority,
fullUrl,
}: {
priority: number;
fullUrl: string;
}) {
const community = {
fullUrl,
priority: priority || 0,
};
return community;
}
function insertCommunityIntoWrapper(
community: { id: string; priority: number },
userGroupConfigWrapper: UserGroupsWrapperNode,
volatileConfigWrapper: ConvoInfoVolatileWrapperNode,
db: BetterSqlite3.Database,
version: number
) {
checkTargetMigration(version, targetVersion);
const priority = community.priority;
const convoId = community.id; // the id of a conversation has the prefix, the serverUrl and the roomToken already present, but not the pubkey
const roomDetails = sqlNode.getV2OpenGroupRoom(convoId, db);
// hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community);
if (
!roomDetails ||
isEmpty(roomDetails) ||
isEmpty(roomDetails.serverUrl) ||
isEmpty(roomDetails.roomId) ||
isEmpty(roomDetails.serverPublicKey)
) {
console.info(
'insertCommunityIntoWrapper did not find corresponding room details',
convoId,
roomDetails
);
return;
}
hasDebugEnvVariable ??
console.info(
`building fullUrl from serverUrl:"${roomDetails.serverUrl}" roomId:"${roomDetails.roomId}" pubkey:"${roomDetails.serverPublicKey}"`
);
const fullUrl = userGroupConfigWrapper.buildFullUrlFromDetails(
roomDetails.serverUrl,
roomDetails.roomId,
roomDetails.serverPublicKey
);
const wrapperComm = getCommunityInfoFromDBValues({
fullUrl,
priority,
});
try {
hasDebugEnvVariable && console.info('Inserting community into group wrapper: ', wrapperComm);
userGroupConfigWrapper.setCommunityByFullUrl(wrapperComm.fullUrl, wrapperComm.priority);
const rows = db
.prepare(
`
SELECT MAX(COALESCE(serverTimestamp, 0)) AS max_sent_at
FROM ${MESSAGES_TABLE} WHERE
conversationId = $conversationId AND
unread = $unread;
`
)
.get({
conversationId: convoId,
unread: toSqliteBoolean(false), // we want to find the message read with the higher serverTimestamp timestamp
});
const maxRead = rows?.max_sent_at;
const lastRead = isNumber(maxRead) && isFinite(maxRead) ? maxRead : 0;
hasDebugEnvVariable &&
console.info(
`Inserting community into volatile wrapper: ${wrapperComm.fullUrl} :${lastRead}`
);
volatileConfigWrapper.setCommunityByFullUrl(wrapperComm.fullUrl, lastRead, false);
} catch (e) {
console.error(
`userGroupConfigWrapper.set during migration failed with ${e.message} for fullUrl: "${wrapperComm.fullUrl}". Skipping community entirely`
);
}
}
function getLegacyGroupInfoFromDBValues({
id,
priority,
members: maybeMembers,
displayNameInProfile,
encPubkeyHex,
encSeckeyHex,
groupAdmins: maybeAdmins,
lastJoinedTimestamp,
}: Pick<
ConversationAttributes,
'id' | 'priority' | 'displayNameInProfile' | 'lastJoinedTimestamp'
> & {
encPubkeyHex: string;
encSeckeyHex: string;
members: string | Array<string>;
groupAdmins: string | Array<string>;
}) {
const admins: Array<string> = maybeArrayJSONtoArray(maybeAdmins);
const members: Array<string> = maybeArrayJSONtoArray(maybeMembers);
const wrappedMembers: Array<LegacyGroupMemberInfo> = (members || []).map(m => {
return {
isAdmin: admins.includes(m),
pubkeyHex: m,
};
});
const legacyGroup: LegacyGroupInfo = {
pubkeyHex: id,
name: displayNameInProfile || '',
priority: priority || 0,
members: wrappedMembers,
encPubkey: !isEmpty(encPubkeyHex) ? from_hex(encPubkeyHex) : new Uint8Array(),
encSeckey: !isEmpty(encSeckeyHex) ? from_hex(encSeckeyHex) : new Uint8Array(),
joinedAtSeconds: Math.floor(lastJoinedTimestamp / 1000),
};
return legacyGroup;
}
function insertLegacyGroupIntoWrapper(
legacyGroup: Pick<
ConversationAttributes,
'id' | 'priority' | 'displayNameInProfile' | 'lastJoinedTimestamp'
> & { members: string; groupAdmins: string }, // members and groupAdmins are still stringified here
userGroupConfigWrapper: UserGroupsWrapperNode,
volatileInfoConfigWrapper: ConvoInfoVolatileWrapperNode,
db: BetterSqlite3.Database,
version: number
) {
checkTargetMigration(version, targetVersion);
const { priority, id, groupAdmins, members, displayNameInProfile, lastJoinedTimestamp } =
legacyGroup;
const latestEncryptionKeyPairHex = sqlNode.getLatestClosedGroupEncryptionKeyPair(
legacyGroup.id,
db
) as HexKeyPair | undefined;
const wrapperLegacyGroup = getLegacyGroupInfoFromDBValues({
id,
priority,
groupAdmins,
members,
displayNameInProfile,
encPubkeyHex: latestEncryptionKeyPairHex?.publicHex || '',
encSeckeyHex: latestEncryptionKeyPairHex?.privateHex || '',
lastJoinedTimestamp,
});
try {
hasDebugEnvVariable &&
console.info('Inserting legacy group into wrapper: ', wrapperLegacyGroup);
userGroupConfigWrapper.setLegacyGroup(wrapperLegacyGroup);
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: 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 legacy group into volatile wrapper maxread: ${id} :${lastRead}`);
volatileInfoConfigWrapper.setLegacyGroup(id, lastRead, false);
} catch (e) {
console.error(
`userGroupConfigWrapper.set during migration failed with ${e.message} for legacyGroup.id: "${legacyGroup.id}". Skipping that legacy group entirely`
);
}
}
export const V31 = {
insertContactIntoContactWrapper,
insertCommunityIntoWrapper,
insertLegacyGroupIntoWrapper,
};