diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 6734fa9a2..d913fbef0 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -42,11 +42,11 @@ message SharedConfigMessage { enum Kind { USER_PROFILE = 1; CONTACTS = 2; - CONVERSATION_INFO = 3; - LEGACY_CLOSED_GROUPS = 4; - CLOSED_GROUP_INFO = 5; - CLOSED_GROUP_MEMBERS = 6; - ENCRYPTION_KEYS = 7; + // CONVERSATION_INFO = 3; + // LEGACY_CLOSED_GROUPS = 4; + // CLOSED_GROUP_INFO = 5; + // CLOSED_GROUP_MEMBERS = 6; + // ENCRYPTION_KEYS = 7; } required Kind kind = 1; diff --git a/ts/components/dialog/SessionNicknameDialog.tsx b/ts/components/dialog/SessionNicknameDialog.tsx index ed82ffc24..b4a26c0bf 100644 --- a/ts/components/dialog/SessionNicknameDialog.tsx +++ b/ts/components/dialog/SessionNicknameDialog.tsx @@ -43,7 +43,7 @@ export const SessionNicknameDialog = (props: Props) => { throw new Error('Cant save without conversation id'); } const conversation = getConversationController().get(conversationId); - await conversation.setNickname(nickname); + await conversation.setNickname(nickname, true); onClickClose(); }; diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 2edebaa29..96b4b6882 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -47,12 +47,12 @@ import { switchThemeTo } from '../../themes/switchTheme'; import { ThemeStateType } from '../../themes/constants/colors'; import { isDarkTheme } from '../../state/selectors/theme'; import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; -import { callLibSessionWorker } from '../../webworker/workers/browser/libsession_worker_interface'; import { SharedConfigMessage } from '../../session/messages/outgoing/controlMessage/SharedConfigMessage'; import { SignalService } from '../../protobuf'; import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; import Long from 'long'; import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces'; +import { initializeLibSessionUtilWrappers } from '../../session/utils/libsession/libsession_utils'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); @@ -204,19 +204,12 @@ const triggerAvatarReUploadIfNeeded = async () => { /** * This function is called only once: on app startup with a logged in user */ -const doAppStartUp = () => { - // init the messageQueue. In the constructor, we add all not send messages - // this call does nothing except calling the constructor, which will continue sending message in the pipeline - void getMessageQueue().processAllPending(); +const doAppStartUp = async () => { + await initializeLibSessionUtilWrappers(); void setupTheme(); // this generates the key to encrypt attachments locally - void Data.generateAttachmentKeyIfEmpty(); - - /* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */ - global.setTimeout(() => { - void getOpenGroupManager().startPolling(); - }, 5000); + await Data.generateAttachmentKeyIfEmpty(); // trigger a sync message if needed for our other devices void triggerSyncIfNeeded(); @@ -224,19 +217,22 @@ const doAppStartUp = () => { void loadDefaultRooms(); + // TODO make this a job of the JobRunner debounce(triggerAvatarReUploadIfNeeded, 200); - setTimeout(async () => { - const keypair = await UserUtils.getUserED25519KeyPairBytes(); - if (!keypair) { - throw new Error('edkeypair not found for current user'); - } + // init the messageQueue. In the constructor, we add all not send messages + // this call does nothing except calling the constructor, which will continue sending message in the pipeline + void getMessageQueue().processAllPending(); + + /* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */ + global.setTimeout(() => { + void getOpenGroupManager().startPolling(); + }, 10000); - await callLibSessionWorker(['UserConfig', 'init', keypair.privKeyBytes, null]); - console.warn(`getName result:"${await callLibSessionWorker(['UserConfig', 'getName'])}"`); - console.warn('setName'); - await callLibSessionWorker(['UserConfig', 'setName', 'MyName']); - console.warn(`getName result:"${await callLibSessionWorker(['UserConfig', 'getName'])}"`); + global.setTimeout(() => { + // init the messageQueue. In the constructor, we add all not send messages + // this call does nothing except calling the constructor, which will continue sending message in the pipeline + void getMessageQueue().processAllPending(); }, 3000); }; diff --git a/ts/data/configDump/configDump.ts b/ts/data/configDump/configDump.ts index 1c9a8d1dc..cd5ce1a06 100644 --- a/ts/data/configDump/configDump.ts +++ b/ts/data/configDump/configDump.ts @@ -1,15 +1,16 @@ import { AsyncWrapper, ConfigDumpRow, + GetAllDumps, GetByPubkeyConfigDump, GetByVariantAndPubkeyConfigDump, SaveConfigDump, - SharedConfigSupportedVariant, } from '../../types/sqlSharedTypes'; +import { ConfigWrapperObjectTypes } from '../../webworker/workers/browser/libsession_worker_functions'; import { channels } from '../channels'; const getByVariantAndPubkey: AsyncWrapper = ( - variant: SharedConfigSupportedVariant, + variant: ConfigWrapperObjectTypes, pubkey: string ) => { return channels.getConfigDumpByVariantAndPubkey(variant, pubkey); @@ -23,4 +24,18 @@ const saveConfigDump: AsyncWrapper = (dump: ConfigDumpRow) => { return channels.saveConfigDump(dump); }; -export const ConfigDumpData = { getByVariantAndPubkey, getByPubkey, saveConfigDump }; +const getAllDumpsWithData: AsyncWrapper = () => { + return channels.getAllDumpsWithData(); +}; + +const getAllDumpsWithoutData: AsyncWrapper = () => { + return channels.getAllDumpsWithoutData(); +}; + +export const ConfigDumpData = { + getByVariantAndPubkey, + getByPubkey, + saveConfigDump, + getAllDumpsWithData, + getAllDumpsWithoutData, +}; diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 8715fb5e7..90d32275e 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -298,7 +298,7 @@ export async function setNotificationForConvoId( } export async function clearNickNameByConvoId(conversationId: string) { const conversation = getConversationController().get(conversationId); - await conversation.setNickname(null); + await conversation.setNickname(null, true); } export function showChangeNickNameByConvoId(conversationId: string) { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 33d95689e..1c6ad5df8 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1406,7 +1406,7 @@ export class ConversationModel extends Backbone.Model { } } - public async setNickname(nickname: string | null) { + public async setNickname(nickname: string | null, shouldCommit = false) { if (!this.isPrivate()) { window.log.info('cannot setNickname to a non private conversation.'); return; @@ -1425,7 +1425,9 @@ export class ConversationModel extends Backbone.Model { this.set({ nickname: trimmed, displayNameInProfile: realUserName }); } - await this.commit(); + if (shouldCommit) { + await this.commit(); + } } public async setSessionProfile(newProfile: { @@ -1482,7 +1484,7 @@ export class ConversationModel extends Backbone.Model { * @returns `nickname` so the nickname we forced for that user. For a group, this returns `undefined` */ public getNickname(): string | undefined { - return this.isPrivate() ? this.get('nickname') : undefined; + return this.isPrivate() ? this.get('nickname') || undefined : undefined; } /** diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 8a45af40d..69df8c3fa 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -1233,7 +1233,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`); /** * Create a table to store our sharedConfigMessage dumps - **/ + */ db.transaction(() => { db.exec(`CREATE TABLE configDump( variant TEXT NOT NULL, @@ -1241,6 +1241,13 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite data BLOB, combinedMessageHashes TEXT); `); + + db.exec(`ALTER TABLE conversations + ADD COLUMN lastReadTimestampMs INTEGER; + ; + `); + + // we need to populate those fields with the current state of the conversation so let's throw null until this is done throw null; writeSessionSchemaVersion(targetVersion, db); })(); diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 0c80bce7c..2c09883f3 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -46,12 +46,7 @@ import { toSqliteBoolean, } from './database_utility'; -import { - ConfigDumpDataNode, - ConfigDumpRow, - SharedConfigSupportedVariant, - UpdateLastHashType, -} from '../types/sqlSharedTypes'; +import { ConfigDumpDataNode, ConfigDumpRow, UpdateLastHashType } from '../types/sqlSharedTypes'; import { OpenGroupV2Room } from '../data/opengroups'; import { @@ -67,6 +62,7 @@ import { initDbInstanceWith, isInstanceInitialized, } from './sqlInstance'; +import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions'; // tslint:disable: no-console function-name non-literal-fs-path @@ -2031,7 +2027,7 @@ function removeV2OpenGroupRoom(conversationId: string) { */ const configDumpData: ConfigDumpDataNode = { - getConfigDumpByVariantAndPubkey: (variant: SharedConfigSupportedVariant, pubkey: string) => { + getConfigDumpByVariantAndPubkey: (variant: ConfigWrapperObjectTypes, pubkey: string) => { const rows = assertGlobalInstance() .prepare('SELECT * from configDump WHERE variant = $variant AND pubkey = $pubkey;') .get({ @@ -2084,6 +2080,32 @@ const configDumpData: ConfigDumpDataNode = { data, }); }, + + getAllDumpsWithData: () => { + const rows = assertGlobalInstance() + .prepare('SELECT variant, publicKey, combinedMessageHashes, data from configDump;') + .get(); + + if (!rows) { + return []; + } + throw new Error(`getAllDumpsWithData: rows: ${JSON.stringify(rows)} `); + + return rows; + }, + + getAllDumpsWithoutData: () => { + const rows = assertGlobalInstance() + .prepare('SELECT variant, publicKey, combinedMessageHashes from configDump;') + .get(); + + if (!rows) { + return []; + } + throw new Error(`getAllDumpsWithoutData: rows: ${JSON.stringify(rows)} `); + + return rows; + }, }; /** diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 45f67056f..173776b6d 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _, { groupBy, isArray, isEmpty } from 'lodash'; import { Data, hasSyncedInitialConfigurationItem } from '../data/data'; import { joinOpenGroupV2WithUIEvents, @@ -18,22 +18,230 @@ import { ConversationInteraction } from '../interactions'; import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage'; import { appendFetchAvatarAndProfileJob, updateOurProfileSync } from './userProfileImageUpdates'; import { ConversationTypeEnum } from '../models/conversationAttributes'; +import { callLibSessionWorker } from '../webworker/workers/browser/libsession_worker_interface'; +import { IncomingMessage } from '../session/messages/incoming/IncomingMessage'; +import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions'; +import { Dictionary } from '@reduxjs/toolkit'; +import { ContactInfo, ProfilePicture } from 'session_util_wrapper'; -export async function handleConfigMessagesViaLibSession( - configMessages: Array +type IncomingConfResult = { + needsPush: boolean; + needsDump: boolean; + messageHashes: Array; + latestSentTimestamp: number; +}; + +function protobufSharedConfigTypeToWrapper( + kind: SignalService.SharedConfigMessage.Kind +): ConfigWrapperObjectTypes | null { + switch (kind) { + case SignalService.SharedConfigMessage.Kind.USER_PROFILE: + return 'UserConfig'; + case SignalService.SharedConfigMessage.Kind.CONTACTS: + return 'ContactsConfig'; + default: + return null; + } +} + +async function mergeConfigsWithIncomingUpdates( + groupedByKind: Dictionary>> ) { - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + const kindMessageMap: Map = new Map(); + // do the merging on all wrappers sequentially instead of with a promise.all() + const allKinds = (Object.keys(groupedByKind) as unknown) as Array< + SignalService.SharedConfigMessage.Kind + >; + for (let index = 0; index < allKinds.length; index++) { + const kind = allKinds[index]; + // see comment above "groupedByKind = groupBy" about why this is needed + const castedKind = (kind as unknown) as SignalService.SharedConfigMessage.Kind; + const currentKindMessages = groupedByKind[castedKind]; + if (!currentKindMessages) { + continue; + } + const toMerge = currentKindMessages.map(m => m.message.data); + + const wrapperId = protobufSharedConfigTypeToWrapper(castedKind); + if (!wrapperId) { + throw new Error(`Invalid castedKind: ${castedKind}`); + } + + await callLibSessionWorker([wrapperId, 'merge', toMerge]); + const needsPush = ((await callLibSessionWorker([wrapperId, 'needsPush'])) || false) as boolean; + const needsDump = ((await callLibSessionWorker([wrapperId, 'needsDump'])) || false) as boolean; + const messageHashes = currentKindMessages.map(m => m.messageHash); + const latestSentTimestamp = Math.max(...currentKindMessages.map(m => m.envelopeTimestamp)); + + const incomingConfResult: IncomingConfResult = { + latestSentTimestamp, + messageHashes, + needsDump, + needsPush, + }; + kindMessageMap.set(kind, incomingConfResult); + } + + return kindMessageMap; +} + +async function handleUserProfileUpdate(result: IncomingConfResult) { + if (result.needsDump) { + return; + } + + const updatedUserName = (await callLibSessionWorker(['UserConfig', 'getName'])) as + | string + | undefined; + const updatedProfilePicture = (await callLibSessionWorker([ + 'UserConfig', + 'getProfilePicture', + ])) as ProfilePicture; + + // fetch our own conversation + const userPublicKey = UserUtils.getOurPubKeyStrFromCache(); + if (!userPublicKey) { + return; + } + + const picUpdate = !isEmpty(updatedProfilePicture.key) && !isEmpty(updatedProfilePicture.url); + + // trigger an update of our profileName and picture if there is one. + // this call checks for differences between updating anything + void updateOurProfileSync( + { displayName: updatedUserName, profilePicture: picUpdate ? updatedProfilePicture.url : null }, + picUpdate ? updatedProfilePicture.key : null + ); +} + +async function handleContactsUpdate(result: IncomingConfResult) { + if (result.needsDump) { + return; + } + + const allContacts = (await callLibSessionWorker(['ContactsConfig', 'getAll'])) as Array< + ContactInfo + >; + + for (let index = 0; index < allContacts.length; index++) { + const wrapperConvo = allContacts[index]; + + if (wrapperConvo.id && getConversationController().get(wrapperConvo.id)) { + const existingConvo = getConversationController().get(wrapperConvo.id); + let changes = false; + + // Note: the isApproved and didApproveMe flags are irreversible so they should only be updated when getting set to true + if ( + existingConvo.get('isApproved') !== undefined && + wrapperConvo.approved !== undefined && + existingConvo.get('isApproved') !== wrapperConvo.approved + ) { + await existingConvo.setIsApproved(wrapperConvo.approved, false); + changes = true; + } + + if ( + existingConvo.get('didApproveMe') !== undefined && + wrapperConvo.approvedMe !== undefined && + existingConvo.get('didApproveMe') !== wrapperConvo.approvedMe + ) { + await existingConvo.setDidApproveMe(wrapperConvo.approvedMe, false); + changes = true; + } + + const convoBlocked = wrapperConvo.blocked || false; + if (convoBlocked !== existingConvo.isBlocked()) { + if (existingConvo.isPrivate()) { + await BlockedNumberController.setBlocked(wrapperConvo.id, convoBlocked); + } else { + await BlockedNumberController.setGroupBlocked(wrapperConvo.id, convoBlocked); + } + } + + if (wrapperConvo.nickname !== existingConvo.getNickname()) { + await existingConvo.setNickname(wrapperConvo.nickname || null, false); + changes = true; + } + // make sure to write the changes to the database now as the `appendFetchAvatarAndProfileJob` call below might take some time before getting run + if (changes) { + await existingConvo.commit(); + } + + // we still need to handle the the `name` and the `profilePicture` but those are currently made asynchronously + void appendFetchAvatarAndProfileJob( + existingConvo.id, + { + displayName: wrapperConvo.name, + profilePicture: wrapperConvo.profilePicture?.url || null, + }, + wrapperConvo.profilePicture?.key || null + ); + } + } +} + +async function processMergingResults( + results: Map +) { + const keys = [...results.keys()]; + + for (let index = 0; index < keys.length; index++) { + const kind = keys[index]; + const result = results.get(kind); + + if (!result) { + continue; + } + + try { + switch (kind) { + case SignalService.SharedConfigMessage.Kind.USER_PROFILE: + await handleUserProfileUpdate(result); + break; + case SignalService.SharedConfigMessage.Kind.CONTACTS: + await handleContactsUpdate(result); + break; + } + } catch (e) { + throw e; + } + } +} + +async function handleConfigMessagesViaLibSession( + configMessages: Array> +) { + // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent + + if ( + !window.sessionFeatureFlags.useSharedUtilForUserConfig || + !configMessages || + !isArray(configMessages) || + configMessages.length === 0 + ) { + return; } window?.log?.info( `Handling our profileUdpates via libsession_util. count: ${configMessages.length}` ); + + // lodash does not have a way to give the type of the keys as generic parameter so this can only be a string: Array<> + const groupedByKind = groupBy(configMessages, m => m.message.kind); + + const kindMessagesMap = await mergeConfigsWithIncomingUpdates(groupedByKind); + + await processMergingResults(kindMessagesMap); } async function handleOurProfileUpdate( sentAt: number | Long, configMessage: SignalService.ConfigurationMessage ) { + // this call won't be needed with the new sharedUtilLibrary + if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { + return; + } const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp(); if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) { window?.log?.info( @@ -197,7 +405,7 @@ const handleContactFromConfig = async ( } void appendFetchAvatarAndProfileJob( - contactConvo, + contactConvo.id, profileInDataMessage, contactReceived.profileKey ); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 400ddd918..855972c8a 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -688,7 +688,7 @@ async function handleMessageRequestResponse( if (messageRequestResponse.profile && !isEmpty(messageRequestResponse.profile)) { void appendFetchAvatarAndProfileJob( - conversationToApprove, + conversationToApprove.id, messageRequestResponse.profile, messageRequestResponse.profileKey ); diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 402a9b6b0..ae5394e82 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -218,7 +218,7 @@ export async function handleSwarmDataMessage( ) { // do not await this void appendFetchAvatarAndProfileJob( - senderConversationModel, + senderConversationModel.id, cleanDataMessage.profile, cleanDataMessage.profileKey ); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index f1bb88927..b5bb6f505 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -394,7 +394,7 @@ export async function handleMessageJob( // as our profile is shared accross our devices with a ConfigurationMessage if (messageModel.isIncoming() && regularDataMessage.profile) { void appendFetchAvatarAndProfileJob( - sendingDeviceConversation, + sendingDeviceConversation.id, regularDataMessage.profile, regularDataMessage.profileKey ); diff --git a/ts/receiver/userProfileImageUpdates.ts b/ts/receiver/userProfileImageUpdates.ts index f1c581c41..1e2c47020 100644 --- a/ts/receiver/userProfileImageUpdates.ts +++ b/ts/receiver/userProfileImageUpdates.ts @@ -10,7 +10,6 @@ import { processNewAttachment } from '../types/MessageAttachment'; import { MIME } from '../types'; import { autoScaleForIncomingAvatar } from '../util/attachmentsUtil'; import { decryptProfile } from '../util/crypto/profileEncrypter'; -import { ConversationModel } from '../models/conversation'; import { SignalService } from '../protobuf'; import { getConversationController } from '../session/conversations'; import { UserUtils } from '../session/utils'; @@ -25,26 +24,21 @@ queue.on('reject', error => { }); export async function appendFetchAvatarAndProfileJob( - conversation: ConversationModel, + conversationId: string, profileInDataMessage: SignalService.DataMessage.ILokiProfile, - profileKey?: Uint8Array | null // was any + profileKey?: Uint8Array | null ) { - if (!conversation?.id) { + if (!conversationId) { window?.log?.warn('[profileupdate] Cannot update profile with empty convoid'); return; } - const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversation.id}`; + const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversationId}`; if (hasAlreadyOneAtaTimeMatching(oneAtaTimeStr)) { - // window.log.debug( - // '[profileupdate] not adding another task of "appendFetchAvatarAndProfileJob" as there is already one scheduled for the conversation: ', - // conversation.id - // ); return; } - // window.log.info(`[profileupdate] queuing fetching avatar for ${conversation.id}`); const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return createOrUpdateProfile(conversation, profileInDataMessage, profileKey); + return createOrUpdateProfile(conversationId, profileInDataMessage, profileKey); }); queue.enqueue(async () => task); @@ -56,7 +50,7 @@ export async function appendFetchAvatarAndProfileJob( */ export async function updateOurProfileSync( profileInDataMessage: SignalService.DataMessage.ILokiProfile, - profileKey?: Uint8Array | null // was any + profileKey?: Uint8Array | null ) { const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache()); if (!ourConvo?.id) { @@ -65,7 +59,7 @@ export async function updateOurProfileSync( } const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${ourConvo.id}`; return allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return createOrUpdateProfile(ourConvo, profileInDataMessage, profileKey); + return createOrUpdateProfile(ourConvo.id, profileInDataMessage, profileKey); }); } @@ -73,10 +67,14 @@ export async function updateOurProfileSync( * Creates a new profile from the profile provided. Creates the profile if it doesn't exist. */ async function createOrUpdateProfile( - conversation: ConversationModel, + conversationId: string, profileInDataMessage: SignalService.DataMessage.ILokiProfile, profileKey?: Uint8Array | null ) { + const conversation = getConversationController().get(conversationId); + if (!conversation) { + return; + } if (!conversation.isPrivate()) { window.log.warn('createOrUpdateProfile can only be used for private convos'); return; @@ -143,6 +141,22 @@ async function createOrUpdateProfile( conversation.set({ avatarInProfile: undefined }); } + if (conversation.id === UserUtils.getOurPubKeyStrFromCache()) { + // make sure the settings which should already set to `true` are + if ( + !conversation.get('isTrustedForAttachmentDownload') || + !conversation.get('isApproved') || + !conversation.get('didApproveMe') + ) { + conversation.set({ + isTrustedForAttachmentDownload: true, + isApproved: true, + didApproveMe: true, + }); + changes = true; + } + } + if (changes) { await conversation.commit(); } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 44a95217d..8bd59b418 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -17,6 +17,8 @@ import pRetry from 'p-retry'; import { SnodeAPIRetrieve } from './retrieveRequest'; import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; +import { ConfigMessageHandler } from '../../../receiver/configMessage'; +import { IncomingMessage } from '../../messages/incoming/IncomingMessage'; // Some websocket nonsense export function processMessage(message: string, options: any = {}, messageHash: string) { @@ -271,11 +273,27 @@ export class SwarmPolling { } perfStart(`handleSeenMessages-${pkStr}`); - const newMessages = await this.handleSeenMessages(messages); - perfEnd(`handleSeenMessages-${pkStr}`, 'handleSeenMessages'); + // try { + // if ( + // window.sessionFeatureFlags.useSharedUtilForUserConfig && + // userConfigMessagesMerged.length + // ) { + // const asIncomingMessages = userConfigMessagesMerged.map(msg => { + // const incomingMessage: IncomingMessage = { + // envelopeTimestamp: msg.timestamp, + // message: msg.data, + // messageHash: msg.hash, + // }; + // }); + // await ConfigMessageHandler.handleConfigMessagesViaLibSession(); + // } + // } catch (e) { + // console.error('shared util lib process messages failed with: ', e); + // } + newMessages.forEach((m: RetrieveMessageItem) => { const options = isGroup ? { conversationId: pkStr } : {}; processMessage(m.data, options, m.hash); diff --git a/ts/session/messages/incoming/IncomingMessage.ts b/ts/session/messages/incoming/IncomingMessage.ts new file mode 100644 index 000000000..cab939c2e --- /dev/null +++ b/ts/session/messages/incoming/IncomingMessage.ts @@ -0,0 +1,56 @@ +import Long from 'long'; +import { SignalService } from '../../../protobuf'; + +type IncomingMessageAvailableTypes = + | SignalService.DataMessage + | SignalService.CallMessage + | SignalService.ReceiptMessage + | SignalService.TypingMessage + | SignalService.ConfigurationMessage + | SignalService.DataExtractionNotification + | SignalService.Unsend + | SignalService.MessageRequestResponse + | SignalService.SharedConfigMessage; + +export class IncomingMessage { + public readonly envelopeTimestamp: number; + public readonly authorOrGroupPubkey: any; + public readonly authorInGroup: string | null; + public readonly messageHash: string; + public readonly message: T; + + /** + * + * - `messageHash` is the hash as retrieved from the `/receive` request + * - `envelopeTimestamp` is part of the message envelope and the what our sent timestamp must be. + * - `authorOrGroupPubkey`: + * * for a 1o1 message, the is the sender + * * for a message in a group, this is the pubkey of the group (as everyone + * in a group send message to the group pubkey) + * - `authorInGroup` is only set when this message is incoming from a closed group. This is the old `senderIdentity` and is the publicKey of the sender inside the message itself once decrypted. This is the real sender of a closed group message. + * - `message` is the data of the ContentMessage itself. + */ + constructor({ + envelopeTimestamp, + authorOrGroupPubkey, + authorInGroup, + message, + messageHash, + }: { + messageHash: string; + envelopeTimestamp: Long; + authorOrGroupPubkey: string; + authorInGroup: string | null; + message: T; + }) { + if (envelopeTimestamp > Long.fromNumber(Number.MAX_SAFE_INTEGER)) { + throw new Error('envelopeTimestamp as Long is > Number.MAX_SAFE_INTEGER'); + } + + this.envelopeTimestamp = envelopeTimestamp.toNumber(); + this.authorOrGroupPubkey = authorOrGroupPubkey; + this.authorInGroup = authorInGroup; + this.messageHash = messageHash; + this.message = message; + } +} diff --git a/ts/session/utils/job_runners/JobRunner.ts b/ts/session/utils/job_runners/JobRunner.ts index b75beebe4..a8a6f4a4a 100644 --- a/ts/session/utils/job_runners/JobRunner.ts +++ b/ts/session/utils/job_runners/JobRunner.ts @@ -21,6 +21,16 @@ export type JobEventListener = { onJobStarted: (job: SerializedPersistedJob) => void; }; +/** + * This class is used to plan jobs and make sure they are retried until the success. + * By having a specific type, we can find the logic to be run by that type of job. + * + * There are different type of jobs which can be scheduled, but we currently only use the SyncConfigurationJob. + * + * SyncConfigurationJob is a job which can only be planned once until it is a success. So in the queue on jobs, there can only be one SyncConfigurationJob at all times. + * + * + */ export class PersistedJobRunner { private isInit = false; private jobsScheduled: Array = []; diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts new file mode 100644 index 000000000..0d42013de --- /dev/null +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -0,0 +1,44 @@ +import { difference } from 'lodash'; +import { UserUtils } from '..'; +import { ConfigDumpData } from '../../../data/configDump/configDump'; +import { ConfigWrapperObjectTypes } from '../../../webworker/workers/browser/libsession_worker_functions'; +import { callLibSessionWorker } from '../../../webworker/workers/browser/libsession_worker_interface'; + +export async function initializeLibSessionUtilWrappers() { + const keypair = await UserUtils.getUserED25519KeyPairBytes(); + if (!keypair) { + throw new Error('edkeypair not found for current user'); + } + const privateKeyEd25519 = keypair.privKeyBytes; + const dumps = await ConfigDumpData.getAllDumpsWithData(); + + const userVariantsBuildWithoutErrors = new Set(); + + for (let index = 0; index < dumps.length; index++) { + const dump = dumps[index]; + try { + await callLibSessionWorker([ + dump.variant, + 'init', + privateKeyEd25519, + dump.data.length ? dump.data : null, + ]); + + userVariantsBuildWithoutErrors.add(dump.variant); + } catch (e) { + window.log.warn(`init of UserConfig failed with ${e.message} `); + throw new Error(`initializeLibSessionUtilWrappers failed with ${e.message}`); + } + } + + // TODO complete this list + const requiredVariants: Array = ['UserConfig', 'ContactsConfig']; // 'conversations' + const missingRequiredVariants: Array = difference(requiredVariants, [ + ...userVariantsBuildWithoutErrors.values(), + ]); + + for (let index = 0; index < missingRequiredVariants.length; index++) { + const missingVariant = missingRequiredVariants[index]; + await callLibSessionWorker([missingVariant, 'init', privateKeyEd25519, null]); + } +} diff --git a/ts/types/Message.ts b/ts/types/Message.ts index d21f6a037..ba0fddb27 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -1,42 +1,3 @@ -import { Attachment } from './Attachment'; - -export type Message = UserMessage; -export type UserMessage = IncomingMessage; - -export type IncomingMessage = Readonly< - { - type: 'incoming'; - // Required - attachments: Array; - id: string; - received_at: number; - - // Optional - body?: string; - errors?: Array; - expireTimer?: number; - flags?: number; - source?: string; - } & SharedMessageProperties & - ExpirationTimerUpdate ->; - -type SharedMessageProperties = Readonly<{ - conversationId: string; - sent_at: number; - timestamp: number; -}>; - -type ExpirationTimerUpdate = Partial< - Readonly<{ - expirationTimerUpdate: Readonly<{ - expireTimer: number; - fromSync: boolean; - source: string; - }>; - }> ->; - export type LokiProfile = { displayName: string; avatarPointer?: string; diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts index a64d56167..dc7248e10 100644 --- a/ts/types/sqlSharedTypes.ts +++ b/ts/types/sqlSharedTypes.ts @@ -1,3 +1,5 @@ +import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions'; + /** * This wrapper can be used to make a function type not async, asynced. * We use it in the typing of the database communication, because the data calls (renderer side) have essentially the same signature of the sql calls (node side), with an added `await` @@ -20,14 +22,8 @@ export type UpdateLastHashType = { namespace: number; }; -/** - * Shared config dump types - */ - -export type SharedConfigSupportedVariant = 'user-profile' | 'contacts'; - export type ConfigDumpRow = { - variant: SharedConfigSupportedVariant; // the variant this entry is about. (user-config, contacts, ...) + variant: ConfigWrapperObjectTypes; // the variant this entry is about. (user pr, contacts, ...) pubkey: string; // either our pubkey if a dump for our own swarm or the closed group pubkey data: Uint8Array; // the blob returned by libsession.dump() call combinedMessageHashes?: string; // array of lastHashes to keep track of, stringified @@ -35,14 +31,17 @@ export type ConfigDumpRow = { }; export type GetByVariantAndPubkeyConfigDump = ( - variant: SharedConfigSupportedVariant, + variant: ConfigWrapperObjectTypes, pubkey: string ) => Array; export type GetByPubkeyConfigDump = (pubkey: string) => Array; export type SaveConfigDump = (dump: ConfigDumpRow) => void; +export type GetAllDumps = () => Array; export type ConfigDumpDataNode = { getConfigDumpByVariantAndPubkey: GetByVariantAndPubkeyConfigDump; getConfigDumpsByPubkey: GetByPubkeyConfigDump; saveConfigDump: SaveConfigDump; + getAllDumpsWithData: GetAllDumps; + getAllDumpsWithoutData: GetAllDumps; }; diff --git a/ts/util/storage.ts b/ts/util/storage.ts index d67651cd5..853325dc6 100644 --- a/ts/util/storage.ts +++ b/ts/util/storage.ts @@ -126,6 +126,9 @@ export function getLastProfileUpdateTimestamp() { } export async function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) { + if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { + return; + } await put('last_profile_update_timestamp', lastUpdateTimestamp); } diff --git a/ts/webworker/workers/browser/libsession_worker_functions.d.ts b/ts/webworker/workers/browser/libsession_worker_functions.d.ts index 837f5f6a7..0ee7b79b1 100644 --- a/ts/webworker/workers/browser/libsession_worker_functions.d.ts +++ b/ts/webworker/workers/browser/libsession_worker_functions.d.ts @@ -1,21 +1,28 @@ -import { BaseConfigActions, BaseConfigWrapper, UserConfigActionsType } from 'session_util_wrapper'; +import { + BaseConfigActions, + BaseConfigWrapper, + ContactsConfigActionsType, + UserConfigActionsType, +} from 'session_util_wrapper'; type UserConfig = 'UserConfig'; // we can only have one of those wrapper for our current user (but we can have a few configs for it to be merged into one) -type ClosedGroupConfigPrefix = 'ClosedGroupConfig-03'; // we can have a bunch of those wrapper as we need to be able to send them to a different swarm for each group -type ClosedGroupConfig = `${ClosedGroupConfigPrefix}${string}`; +type ContactsConfig = 'ContactsConfig'; -export type ConfigWrapperObjectTypes = UserConfig | ClosedGroupConfig; +// type ClosedGroupConfigPrefix = 'ClosedGroupConfig-03'; // we can have a bunch of those wrapper as we need to be able to send them to a different swarm for each group +// type ClosedGroupConfig = `${ClosedGroupConfigPrefix}${string}`; +// | ClosedGroupConfig; +export type ConfigWrapperObjectTypes = UserConfig | ContactsConfig; - -/**Those are the actions inherited from BaseConfigWrapper to UserConfigWrapper */ -type UserConfigInheritedActions = [UserConfig, ...BaseConfigActions]; -type UserConfigActions = [UserConfig,...UserConfigActionsType] | [UserConfig, 'init']; +type UserConfigFunctions = + | [UserConfig, ...BaseConfigActions] + | [UserConfig, ...UserConfigActionsType]; +type ContactsConfigFunctions = + | [ContactsConfig, ...BaseConfigActions] + | [ContactsConfig, ...ContactsConfigActionsType]; /**Those are the actions inherited from BaseConfigWrapper to ClosedGroupConfigWrapper */ -type ClosedGroupConfigFromBase = [ClosedGroupConfig, ...BaseConfigActions]; - - -type UserConfigFunctions = UserConfigInheritedActions | UserConfigActions; -type ClosedGroupConfigFunctions = ClosedGroupConfigFromBase; +// type ClosedGroupConfigFromBase = [ClosedGroupConfig, ...BaseConfigActions]; +// type ClosedGroupConfigFunctions = ClosedGroupConfigFromBase; +//| ClosedGroupConfigFunctions; -export type LibSessionWorkerFunctions = UserConfigFunctions | ClosedGroupConfigFunctions; +export type LibSessionWorkerFunctions = UserConfigFunctions | ContactsConfigFunctions; diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 4ddafcbb2..0f40f676d 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -9,7 +9,7 @@ const internalCallLibSessionWorker = async ([ config, fnName, ...args -]: LibSessionWorkerFunctions): Promise => { +]: LibSessionWorkerFunctions): Promise => { if (!libsessionWorkerInterface) { const libsessionWorkerPath = join( getAppRootPath(), @@ -26,6 +26,8 @@ const internalCallLibSessionWorker = async ([ return libsessionWorkerInterface?.callWorker(config, fnName, ...args); }; -export const callLibSessionWorker = async (callToMake: LibSessionWorkerFunctions): Promise => { +export const callLibSessionWorker = async ( + callToMake: LibSessionWorkerFunctions +): Promise => { return internalCallLibSessionWorker(callToMake); }; diff --git a/ts/webworker/workers/node/libsession/libsession.worker.ts b/ts/webworker/workers/node/libsession/libsession.worker.ts index df55da608..0b31c9b42 100644 --- a/ts/webworker/workers/node/libsession/libsession.worker.ts +++ b/ts/webworker/workers/node/libsession/libsession.worker.ts @@ -1,51 +1,79 @@ import _, { isEmpty, isNull } from 'lodash'; -import { UserConfigWrapper } from 'session_util_wrapper'; +import { BaseConfigWrapper, ContactsConfigWrapper, UserConfigWrapper } from 'session_util_wrapper'; import { ConfigWrapperObjectTypes } from '../../browser/libsession_worker_functions'; -// import { default as sodiumWrappers } from 'libsodium-wrappers-sumo'; /* eslint-disable no-console */ /* eslint-disable strict */ -let userConfig: UserConfigWrapper; +// we can only have one of those so don't worry about storing them in a map for now +let userProfileWrapper: UserConfigWrapper | undefined; +let contactsConfigWrapper: ContactsConfigWrapper | undefined; -// async function getSodiumWorker() { -// await sodiumWrappers.ready; +// const configWrappers: Array = new Array(); -// return sodiumWrappers; -// } +type UserWrapperType = 'UserConfig' | 'ContactsConfig'; -async function getCorrespondingWrapper(config: ConfigWrapperObjectTypes) { - if (config !== 'UserConfig') { - throw new Error(`Invalid config: ${config}`); +function getUserWrapper(type: UserWrapperType): BaseConfigWrapper | undefined { + switch (type) { + case 'UserConfig': + return userProfileWrapper; + case 'ContactsConfig': + return contactsConfigWrapper; } - if (!userConfig) { - throw new Error('UserConfig is not init yet'); +} + +function getCorrespondingWrapper(wrapperType: ConfigWrapperObjectTypes): BaseConfigWrapper { + switch (wrapperType) { + case 'UserConfig': + case 'ContactsConfig': + const wrapper = getUserWrapper(wrapperType); + if (!wrapper) { + throw new Error(`${wrapperType} is not init yet`); + } + return wrapper; } - return userConfig; } function isUInt8Array(value: any) { return value.constructor === Uint8Array; } -function initUserConfigWrapper(options: Array) { - if (userConfig) { - throw new Error('UserConfig already init'); +function assertUserWrapperType(wrapperType: ConfigWrapperObjectTypes): UserWrapperType { + if (wrapperType !== 'ContactsConfig' && wrapperType !== 'UserConfig') { + throw new Error(`wrapperType "${wrapperType} is not of type User"`); + } + return wrapperType; +} + +/** + * This function can be used to initialize a wrapper which takes the private ed25519 key of the user and a dump as argument. + */ +function initUserWrapper(options: Array, wrapperType: UserWrapperType): BaseConfigWrapper { + const wrapper = getUserWrapper(wrapperType); + if (wrapper) { + throw new Error(`${wrapperType} already init`); } if (options.length !== 2) { - throw new Error('UserConfig init needs two arguments'); + throw new Error(`${wrapperType} init needs two arguments`); } const [edSecretKey, dump] = options; if (isEmpty(edSecretKey) || !isUInt8Array(edSecretKey)) { - throw new Error('UserConfig init needs a valid edSecretKey'); + throw new Error(`${wrapperType} init needs a valid edSecretKey`); } if (!isNull(dump) && !isUInt8Array(dump)) { - throw new Error('UserConfig init needs a valid dump'); + throw new Error('${wrapperType} init needs a valid dump'); + } + const userType = assertUserWrapperType(wrapperType); + switch (userType) { + case 'UserConfig': + userProfileWrapper = new UserConfigWrapper(edSecretKey, dump); + return userProfileWrapper; + case 'ContactsConfig': + contactsConfigWrapper = new ContactsConfigWrapper(edSecretKey, dump); + return contactsConfigWrapper; } - console.warn('UserConfigWrapper', UserConfigWrapper); - userConfig = new UserConfigWrapper(edSecretKey, dump); } // tslint:disable: function-name no-console @@ -55,7 +83,7 @@ onmessage = async (e: { data: [number, ConfigWrapperObjectTypes, string, ...any] try { if (action === 'init') { - initUserConfigWrapper(args); + initUserWrapper(args, config); postMessage([jobId, null, null]); return; }