feat: add contacts & user profile handling of incoming messages

pull/2620/head
Audric Ackermann 2 years ago
parent 141c22ed43
commit d1cefd4729

@ -42,11 +42,11 @@ message SharedConfigMessage {
enum Kind { enum Kind {
USER_PROFILE = 1; USER_PROFILE = 1;
CONTACTS = 2; CONTACTS = 2;
CONVERSATION_INFO = 3; // CONVERSATION_INFO = 3;
LEGACY_CLOSED_GROUPS = 4; // LEGACY_CLOSED_GROUPS = 4;
CLOSED_GROUP_INFO = 5; // CLOSED_GROUP_INFO = 5;
CLOSED_GROUP_MEMBERS = 6; // CLOSED_GROUP_MEMBERS = 6;
ENCRYPTION_KEYS = 7; // ENCRYPTION_KEYS = 7;
} }
required Kind kind = 1; required Kind kind = 1;

@ -43,7 +43,7 @@ export const SessionNicknameDialog = (props: Props) => {
throw new Error('Cant save without conversation id'); throw new Error('Cant save without conversation id');
} }
const conversation = getConversationController().get(conversationId); const conversation = getConversationController().get(conversationId);
await conversation.setNickname(nickname); await conversation.setNickname(nickname, true);
onClickClose(); onClickClose();
}; };

@ -47,12 +47,12 @@ import { switchThemeTo } from '../../themes/switchTheme';
import { ThemeStateType } from '../../themes/constants/colors'; import { ThemeStateType } from '../../themes/constants/colors';
import { isDarkTheme } from '../../state/selectors/theme'; import { isDarkTheme } from '../../state/selectors/theme';
import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; 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 { SharedConfigMessage } from '../../session/messages/outgoing/controlMessage/SharedConfigMessage';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import Long from 'long'; import Long from 'long';
import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
import { initializeLibSessionUtilWrappers } from '../../session/utils/libsession/libsession_utils';
const Section = (props: { type: SectionType }) => { const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber); 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 * This function is called only once: on app startup with a logged in user
*/ */
const doAppStartUp = () => { const doAppStartUp = async () => {
// init the messageQueue. In the constructor, we add all not send messages await initializeLibSessionUtilWrappers();
// this call does nothing except calling the constructor, which will continue sending message in the pipeline
void getMessageQueue().processAllPending();
void setupTheme(); void setupTheme();
// this generates the key to encrypt attachments locally // this generates the key to encrypt attachments locally
void Data.generateAttachmentKeyIfEmpty(); await 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);
// trigger a sync message if needed for our other devices // trigger a sync message if needed for our other devices
void triggerSyncIfNeeded(); void triggerSyncIfNeeded();
@ -224,19 +217,22 @@ const doAppStartUp = () => {
void loadDefaultRooms(); void loadDefaultRooms();
// TODO make this a job of the JobRunner
debounce(triggerAvatarReUploadIfNeeded, 200); debounce(triggerAvatarReUploadIfNeeded, 200);
setTimeout(async () => { // init the messageQueue. In the constructor, we add all not send messages
const keypair = await UserUtils.getUserED25519KeyPairBytes(); // this call does nothing except calling the constructor, which will continue sending message in the pipeline
if (!keypair) { void getMessageQueue().processAllPending();
throw new Error('edkeypair not found for current user');
} /* 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]); global.setTimeout(() => {
console.warn(`getName result:"${await callLibSessionWorker(['UserConfig', 'getName'])}"`); // init the messageQueue. In the constructor, we add all not send messages
console.warn('setName'); // this call does nothing except calling the constructor, which will continue sending message in the pipeline
await callLibSessionWorker(['UserConfig', 'setName', 'MyName']); void getMessageQueue().processAllPending();
console.warn(`getName result:"${await callLibSessionWorker(['UserConfig', 'getName'])}"`);
}, 3000); }, 3000);
}; };

@ -1,15 +1,16 @@
import { import {
AsyncWrapper, AsyncWrapper,
ConfigDumpRow, ConfigDumpRow,
GetAllDumps,
GetByPubkeyConfigDump, GetByPubkeyConfigDump,
GetByVariantAndPubkeyConfigDump, GetByVariantAndPubkeyConfigDump,
SaveConfigDump, SaveConfigDump,
SharedConfigSupportedVariant,
} from '../../types/sqlSharedTypes'; } from '../../types/sqlSharedTypes';
import { ConfigWrapperObjectTypes } from '../../webworker/workers/browser/libsession_worker_functions';
import { channels } from '../channels'; import { channels } from '../channels';
const getByVariantAndPubkey: AsyncWrapper<GetByVariantAndPubkeyConfigDump> = ( const getByVariantAndPubkey: AsyncWrapper<GetByVariantAndPubkeyConfigDump> = (
variant: SharedConfigSupportedVariant, variant: ConfigWrapperObjectTypes,
pubkey: string pubkey: string
) => { ) => {
return channels.getConfigDumpByVariantAndPubkey(variant, pubkey); return channels.getConfigDumpByVariantAndPubkey(variant, pubkey);
@ -23,4 +24,18 @@ const saveConfigDump: AsyncWrapper<SaveConfigDump> = (dump: ConfigDumpRow) => {
return channels.saveConfigDump(dump); return channels.saveConfigDump(dump);
}; };
export const ConfigDumpData = { getByVariantAndPubkey, getByPubkey, saveConfigDump }; const getAllDumpsWithData: AsyncWrapper<GetAllDumps> = () => {
return channels.getAllDumpsWithData();
};
const getAllDumpsWithoutData: AsyncWrapper<GetAllDumps> = () => {
return channels.getAllDumpsWithoutData();
};
export const ConfigDumpData = {
getByVariantAndPubkey,
getByPubkey,
saveConfigDump,
getAllDumpsWithData,
getAllDumpsWithoutData,
};

@ -298,7 +298,7 @@ export async function setNotificationForConvoId(
} }
export async function clearNickNameByConvoId(conversationId: string) { export async function clearNickNameByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId); const conversation = getConversationController().get(conversationId);
await conversation.setNickname(null); await conversation.setNickname(null, true);
} }
export function showChangeNickNameByConvoId(conversationId: string) { export function showChangeNickNameByConvoId(conversationId: string) {

@ -1406,7 +1406,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
} }
public async setNickname(nickname: string | null) { public async setNickname(nickname: string | null, shouldCommit = false) {
if (!this.isPrivate()) { if (!this.isPrivate()) {
window.log.info('cannot setNickname to a non private conversation.'); window.log.info('cannot setNickname to a non private conversation.');
return; return;
@ -1425,7 +1425,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.set({ nickname: trimmed, displayNameInProfile: realUserName }); this.set({ nickname: trimmed, displayNameInProfile: realUserName });
} }
await this.commit(); if (shouldCommit) {
await this.commit();
}
} }
public async setSessionProfile(newProfile: { public async setSessionProfile(newProfile: {
@ -1482,7 +1484,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
* @returns `nickname` so the nickname we forced for that user. For a group, this returns `undefined` * @returns `nickname` so the nickname we forced for that user. For a group, this returns `undefined`
*/ */
public getNickname(): string | undefined { public getNickname(): string | undefined {
return this.isPrivate() ? this.get('nickname') : undefined; return this.isPrivate() ? this.get('nickname') || undefined : undefined;
} }
/** /**

@ -1233,7 +1233,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`); console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
/** /**
* Create a table to store our sharedConfigMessage dumps * Create a table to store our sharedConfigMessage dumps
**/ */
db.transaction(() => { db.transaction(() => {
db.exec(`CREATE TABLE configDump( db.exec(`CREATE TABLE configDump(
variant TEXT NOT NULL, variant TEXT NOT NULL,
@ -1241,6 +1241,13 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
data BLOB, data BLOB,
combinedMessageHashes TEXT); 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; throw null;
writeSessionSchemaVersion(targetVersion, db); writeSessionSchemaVersion(targetVersion, db);
})(); })();

@ -46,12 +46,7 @@ import {
toSqliteBoolean, toSqliteBoolean,
} from './database_utility'; } from './database_utility';
import { import { ConfigDumpDataNode, ConfigDumpRow, UpdateLastHashType } from '../types/sqlSharedTypes';
ConfigDumpDataNode,
ConfigDumpRow,
SharedConfigSupportedVariant,
UpdateLastHashType,
} from '../types/sqlSharedTypes';
import { OpenGroupV2Room } from '../data/opengroups'; import { OpenGroupV2Room } from '../data/opengroups';
import { import {
@ -67,6 +62,7 @@ import {
initDbInstanceWith, initDbInstanceWith,
isInstanceInitialized, isInstanceInitialized,
} from './sqlInstance'; } from './sqlInstance';
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
// tslint:disable: no-console function-name non-literal-fs-path // tslint:disable: no-console function-name non-literal-fs-path
@ -2031,7 +2027,7 @@ function removeV2OpenGroupRoom(conversationId: string) {
*/ */
const configDumpData: ConfigDumpDataNode = { const configDumpData: ConfigDumpDataNode = {
getConfigDumpByVariantAndPubkey: (variant: SharedConfigSupportedVariant, pubkey: string) => { getConfigDumpByVariantAndPubkey: (variant: ConfigWrapperObjectTypes, pubkey: string) => {
const rows = assertGlobalInstance() const rows = assertGlobalInstance()
.prepare('SELECT * from configDump WHERE variant = $variant AND pubkey = $pubkey;') .prepare('SELECT * from configDump WHERE variant = $variant AND pubkey = $pubkey;')
.get({ .get({
@ -2084,6 +2080,32 @@ const configDumpData: ConfigDumpDataNode = {
data, 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;
},
}; };
/** /**

@ -1,4 +1,4 @@
import _ from 'lodash'; import _, { groupBy, isArray, isEmpty } from 'lodash';
import { Data, hasSyncedInitialConfigurationItem } from '../data/data'; import { Data, hasSyncedInitialConfigurationItem } from '../data/data';
import { import {
joinOpenGroupV2WithUIEvents, joinOpenGroupV2WithUIEvents,
@ -18,22 +18,230 @@ import { ConversationInteraction } from '../interactions';
import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage'; import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage';
import { appendFetchAvatarAndProfileJob, updateOurProfileSync } from './userProfileImageUpdates'; import { appendFetchAvatarAndProfileJob, updateOurProfileSync } from './userProfileImageUpdates';
import { ConversationTypeEnum } from '../models/conversationAttributes'; 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( type IncomingConfResult = {
configMessages: Array<SignalService.ConfigurationMessage> needsPush: boolean;
needsDump: boolean;
messageHashes: Array<string>;
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<Array<IncomingMessage<SignalService.SharedConfigMessage>>>
) { ) {
if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { const kindMessageMap: Map<SignalService.SharedConfigMessage.Kind, IncomingConfResult> = 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<SignalService.SharedConfigMessage.Kind, IncomingConfResult>
) {
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<IncomingMessage<SignalService.SharedConfigMessage>>
) {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
if (
!window.sessionFeatureFlags.useSharedUtilForUserConfig ||
!configMessages ||
!isArray(configMessages) ||
configMessages.length === 0
) {
return;
} }
window?.log?.info( window?.log?.info(
`Handling our profileUdpates via libsession_util. count: ${configMessages.length}` `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( async function handleOurProfileUpdate(
sentAt: number | Long, sentAt: number | Long,
configMessage: SignalService.ConfigurationMessage configMessage: SignalService.ConfigurationMessage
) { ) {
// this call won't be needed with the new sharedUtilLibrary
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
return;
}
const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp(); const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp();
if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) { if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) {
window?.log?.info( window?.log?.info(
@ -197,7 +405,7 @@ const handleContactFromConfig = async (
} }
void appendFetchAvatarAndProfileJob( void appendFetchAvatarAndProfileJob(
contactConvo, contactConvo.id,
profileInDataMessage, profileInDataMessage,
contactReceived.profileKey contactReceived.profileKey
); );

@ -688,7 +688,7 @@ async function handleMessageRequestResponse(
if (messageRequestResponse.profile && !isEmpty(messageRequestResponse.profile)) { if (messageRequestResponse.profile && !isEmpty(messageRequestResponse.profile)) {
void appendFetchAvatarAndProfileJob( void appendFetchAvatarAndProfileJob(
conversationToApprove, conversationToApprove.id,
messageRequestResponse.profile, messageRequestResponse.profile,
messageRequestResponse.profileKey messageRequestResponse.profileKey
); );

@ -218,7 +218,7 @@ export async function handleSwarmDataMessage(
) { ) {
// do not await this // do not await this
void appendFetchAvatarAndProfileJob( void appendFetchAvatarAndProfileJob(
senderConversationModel, senderConversationModel.id,
cleanDataMessage.profile, cleanDataMessage.profile,
cleanDataMessage.profileKey cleanDataMessage.profileKey
); );

@ -394,7 +394,7 @@ export async function handleMessageJob(
// as our profile is shared accross our devices with a ConfigurationMessage // as our profile is shared accross our devices with a ConfigurationMessage
if (messageModel.isIncoming() && regularDataMessage.profile) { if (messageModel.isIncoming() && regularDataMessage.profile) {
void appendFetchAvatarAndProfileJob( void appendFetchAvatarAndProfileJob(
sendingDeviceConversation, sendingDeviceConversation.id,
regularDataMessage.profile, regularDataMessage.profile,
regularDataMessage.profileKey regularDataMessage.profileKey
); );

@ -10,7 +10,6 @@ import { processNewAttachment } from '../types/MessageAttachment';
import { MIME } from '../types'; import { MIME } from '../types';
import { autoScaleForIncomingAvatar } from '../util/attachmentsUtil'; import { autoScaleForIncomingAvatar } from '../util/attachmentsUtil';
import { decryptProfile } from '../util/crypto/profileEncrypter'; import { decryptProfile } from '../util/crypto/profileEncrypter';
import { ConversationModel } from '../models/conversation';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
@ -25,26 +24,21 @@ queue.on('reject', error => {
}); });
export async function appendFetchAvatarAndProfileJob( export async function appendFetchAvatarAndProfileJob(
conversation: ConversationModel, conversationId: string,
profileInDataMessage: SignalService.DataMessage.ILokiProfile, 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'); window?.log?.warn('[profileupdate] Cannot update profile with empty convoid');
return; return;
} }
const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversation.id}`; const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversationId}`;
if (hasAlreadyOneAtaTimeMatching(oneAtaTimeStr)) { 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; return;
} }
// window.log.info(`[profileupdate] queuing fetching avatar for ${conversation.id}`);
const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => { const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => {
return createOrUpdateProfile(conversation, profileInDataMessage, profileKey); return createOrUpdateProfile(conversationId, profileInDataMessage, profileKey);
}); });
queue.enqueue(async () => task); queue.enqueue(async () => task);
@ -56,7 +50,7 @@ export async function appendFetchAvatarAndProfileJob(
*/ */
export async function updateOurProfileSync( export async function updateOurProfileSync(
profileInDataMessage: SignalService.DataMessage.ILokiProfile, profileInDataMessage: SignalService.DataMessage.ILokiProfile,
profileKey?: Uint8Array | null // was any profileKey?: Uint8Array | null
) { ) {
const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache()); const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
if (!ourConvo?.id) { if (!ourConvo?.id) {
@ -65,7 +59,7 @@ export async function updateOurProfileSync(
} }
const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${ourConvo.id}`; const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${ourConvo.id}`;
return allowOnlyOneAtATime(oneAtaTimeStr, async () => { 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. * Creates a new profile from the profile provided. Creates the profile if it doesn't exist.
*/ */
async function createOrUpdateProfile( async function createOrUpdateProfile(
conversation: ConversationModel, conversationId: string,
profileInDataMessage: SignalService.DataMessage.ILokiProfile, profileInDataMessage: SignalService.DataMessage.ILokiProfile,
profileKey?: Uint8Array | null profileKey?: Uint8Array | null
) { ) {
const conversation = getConversationController().get(conversationId);
if (!conversation) {
return;
}
if (!conversation.isPrivate()) { if (!conversation.isPrivate()) {
window.log.warn('createOrUpdateProfile can only be used for private convos'); window.log.warn('createOrUpdateProfile can only be used for private convos');
return; return;
@ -143,6 +141,22 @@ async function createOrUpdateProfile(
conversation.set({ avatarInProfile: undefined }); 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) { if (changes) {
await conversation.commit(); await conversation.commit();
} }

@ -17,6 +17,8 @@ import pRetry from 'p-retry';
import { SnodeAPIRetrieve } from './retrieveRequest'; import { SnodeAPIRetrieve } from './retrieveRequest';
import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { SnodeNamespace, SnodeNamespaces } from './namespaces';
import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types';
import { ConfigMessageHandler } from '../../../receiver/configMessage';
import { IncomingMessage } from '../../messages/incoming/IncomingMessage';
// Some websocket nonsense // Some websocket nonsense
export function processMessage(message: string, options: any = {}, messageHash: string) { export function processMessage(message: string, options: any = {}, messageHash: string) {
@ -271,11 +273,27 @@ export class SwarmPolling {
} }
perfStart(`handleSeenMessages-${pkStr}`); perfStart(`handleSeenMessages-${pkStr}`);
const newMessages = await this.handleSeenMessages(messages); const newMessages = await this.handleSeenMessages(messages);
perfEnd(`handleSeenMessages-${pkStr}`, 'handleSeenMessages'); perfEnd(`handleSeenMessages-${pkStr}`, 'handleSeenMessages');
// try {
// if (
// window.sessionFeatureFlags.useSharedUtilForUserConfig &&
// userConfigMessagesMerged.length
// ) {
// const asIncomingMessages = userConfigMessagesMerged.map(msg => {
// const incomingMessage: IncomingMessage<SignalService.SharedConfigMessage> = {
// 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) => { newMessages.forEach((m: RetrieveMessageItem) => {
const options = isGroup ? { conversationId: pkStr } : {}; const options = isGroup ? { conversationId: pkStr } : {};
processMessage(m.data, options, m.hash); processMessage(m.data, options, m.hash);

@ -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<T extends IncomingMessageAvailableTypes> {
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;
}
}

@ -21,6 +21,16 @@ export type JobEventListener = {
onJobStarted: (job: SerializedPersistedJob) => void; 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 { export class PersistedJobRunner {
private isInit = false; private isInit = false;
private jobsScheduled: Array<Persistedjob> = []; private jobsScheduled: Array<Persistedjob> = [];

@ -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<ConfigWrapperObjectTypes>();
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<ConfigWrapperObjectTypes> = ['UserConfig', 'ContactsConfig']; // 'conversations'
const missingRequiredVariants: Array<ConfigWrapperObjectTypes> = difference(requiredVariants, [
...userVariantsBuildWithoutErrors.values(),
]);
for (let index = 0; index < missingRequiredVariants.length; index++) {
const missingVariant = missingRequiredVariants[index];
await callLibSessionWorker([missingVariant, 'init', privateKeyEd25519, null]);
}
}

@ -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<Attachment>;
id: string;
received_at: number;
// Optional
body?: string;
errors?: Array<any>;
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 = { export type LokiProfile = {
displayName: string; displayName: string;
avatarPointer?: string; avatarPointer?: string;

@ -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. * 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` * 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; namespace: number;
}; };
/**
* Shared config dump types
*/
export type SharedConfigSupportedVariant = 'user-profile' | 'contacts';
export type ConfigDumpRow = { 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 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 data: Uint8Array; // the blob returned by libsession.dump() call
combinedMessageHashes?: string; // array of lastHashes to keep track of, stringified combinedMessageHashes?: string; // array of lastHashes to keep track of, stringified
@ -35,14 +31,17 @@ export type ConfigDumpRow = {
}; };
export type GetByVariantAndPubkeyConfigDump = ( export type GetByVariantAndPubkeyConfigDump = (
variant: SharedConfigSupportedVariant, variant: ConfigWrapperObjectTypes,
pubkey: string pubkey: string
) => Array<ConfigDumpRow>; ) => Array<ConfigDumpRow>;
export type GetByPubkeyConfigDump = (pubkey: string) => Array<ConfigDumpRow>; export type GetByPubkeyConfigDump = (pubkey: string) => Array<ConfigDumpRow>;
export type SaveConfigDump = (dump: ConfigDumpRow) => void; export type SaveConfigDump = (dump: ConfigDumpRow) => void;
export type GetAllDumps = () => Array<ConfigDumpRow>;
export type ConfigDumpDataNode = { export type ConfigDumpDataNode = {
getConfigDumpByVariantAndPubkey: GetByVariantAndPubkeyConfigDump; getConfigDumpByVariantAndPubkey: GetByVariantAndPubkeyConfigDump;
getConfigDumpsByPubkey: GetByPubkeyConfigDump; getConfigDumpsByPubkey: GetByPubkeyConfigDump;
saveConfigDump: SaveConfigDump; saveConfigDump: SaveConfigDump;
getAllDumpsWithData: GetAllDumps;
getAllDumpsWithoutData: GetAllDumps;
}; };

@ -126,6 +126,9 @@ export function getLastProfileUpdateTimestamp() {
} }
export async function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) { export async function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) {
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
return;
}
await put('last_profile_update_timestamp', lastUpdateTimestamp); await put('last_profile_update_timestamp', lastUpdateTimestamp);
} }

@ -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 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 ContactsConfig = 'ContactsConfig';
type ClosedGroupConfig = `${ClosedGroupConfigPrefix}${string}`;
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;
type UserConfigFunctions =
/**Those are the actions inherited from BaseConfigWrapper to UserConfigWrapper */ | [UserConfig, ...BaseConfigActions]
type UserConfigInheritedActions = [UserConfig, ...BaseConfigActions]; | [UserConfig, ...UserConfigActionsType];
type UserConfigActions = [UserConfig,...UserConfigActionsType] | [UserConfig, 'init']; type ContactsConfigFunctions =
| [ContactsConfig, ...BaseConfigActions]
| [ContactsConfig, ...ContactsConfigActionsType];
/**Those are the actions inherited from BaseConfigWrapper to ClosedGroupConfigWrapper */ /**Those are the actions inherited from BaseConfigWrapper to ClosedGroupConfigWrapper */
type ClosedGroupConfigFromBase = [ClosedGroupConfig, ...BaseConfigActions]; // type ClosedGroupConfigFromBase = [ClosedGroupConfig, ...BaseConfigActions];
// type ClosedGroupConfigFunctions = ClosedGroupConfigFromBase;
//| ClosedGroupConfigFunctions;
type UserConfigFunctions = UserConfigInheritedActions | UserConfigActions;
type ClosedGroupConfigFunctions = ClosedGroupConfigFromBase;
export type LibSessionWorkerFunctions = UserConfigFunctions | ClosedGroupConfigFunctions; export type LibSessionWorkerFunctions = UserConfigFunctions | ContactsConfigFunctions;

@ -9,7 +9,7 @@ const internalCallLibSessionWorker = async ([
config, config,
fnName, fnName,
...args ...args
]: LibSessionWorkerFunctions): Promise<any> => { ]: LibSessionWorkerFunctions): Promise<unknown> => {
if (!libsessionWorkerInterface) { if (!libsessionWorkerInterface) {
const libsessionWorkerPath = join( const libsessionWorkerPath = join(
getAppRootPath(), getAppRootPath(),
@ -26,6 +26,8 @@ const internalCallLibSessionWorker = async ([
return libsessionWorkerInterface?.callWorker(config, fnName, ...args); return libsessionWorkerInterface?.callWorker(config, fnName, ...args);
}; };
export const callLibSessionWorker = async (callToMake: LibSessionWorkerFunctions): Promise<any> => { export const callLibSessionWorker = async (
callToMake: LibSessionWorkerFunctions
): Promise<unknown> => {
return internalCallLibSessionWorker(callToMake); return internalCallLibSessionWorker(callToMake);
}; };

@ -1,51 +1,79 @@
import _, { isEmpty, isNull } from 'lodash'; 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 { ConfigWrapperObjectTypes } from '../../browser/libsession_worker_functions';
// import { default as sodiumWrappers } from 'libsodium-wrappers-sumo';
/* eslint-disable no-console */ /* eslint-disable no-console */
/* eslint-disable strict */ /* 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() { // const configWrappers: Array<EntryUserConfig | EntryContactsConfig> = new Array();
// await sodiumWrappers.ready;
// return sodiumWrappers; type UserWrapperType = 'UserConfig' | 'ContactsConfig';
// }
async function getCorrespondingWrapper(config: ConfigWrapperObjectTypes) { function getUserWrapper(type: UserWrapperType): BaseConfigWrapper | undefined {
if (config !== 'UserConfig') { switch (type) {
throw new Error(`Invalid config: ${config}`); 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) { function isUInt8Array(value: any) {
return value.constructor === Uint8Array; return value.constructor === Uint8Array;
} }
function initUserConfigWrapper(options: Array<any>) { function assertUserWrapperType(wrapperType: ConfigWrapperObjectTypes): UserWrapperType {
if (userConfig) { if (wrapperType !== 'ContactsConfig' && wrapperType !== 'UserConfig') {
throw new Error('UserConfig already init'); 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<any>, wrapperType: UserWrapperType): BaseConfigWrapper {
const wrapper = getUserWrapper(wrapperType);
if (wrapper) {
throw new Error(`${wrapperType} already init`);
} }
if (options.length !== 2) { if (options.length !== 2) {
throw new Error('UserConfig init needs two arguments'); throw new Error(`${wrapperType} init needs two arguments`);
} }
const [edSecretKey, dump] = options; const [edSecretKey, dump] = options;
if (isEmpty(edSecretKey) || !isUInt8Array(edSecretKey)) { 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)) { 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 // tslint:disable: function-name no-console
@ -55,7 +83,7 @@ onmessage = async (e: { data: [number, ConfigWrapperObjectTypes, string, ...any]
try { try {
if (action === 'init') { if (action === 'init') {
initUserConfigWrapper(args); initUserWrapper(args, config);
postMessage([jobId, null, null]); postMessage([jobId, null, null]);
return; return;
} }

Loading…
Cancel
Save