feat: add contacts & user profile handling of incoming messages

pull/2620/head
Audric Ackermann 1 year ago
parent 141c22ed43
commit d1cefd4729

@ -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;

@ -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();
};

@ -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);
};

@ -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<GetByVariantAndPubkeyConfigDump> = (
variant: SharedConfigSupportedVariant,
variant: ConfigWrapperObjectTypes,
pubkey: string
) => {
return channels.getConfigDumpByVariantAndPubkey(variant, pubkey);
@ -23,4 +24,18 @@ const saveConfigDump: AsyncWrapper<SaveConfigDump> = (dump: ConfigDumpRow) => {
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) {
const conversation = getConversationController().get(conversationId);
await conversation.setNickname(null);
await conversation.setNickname(null, true);
}
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()) {
window.log.info('cannot setNickname to a non private conversation.');
return;
@ -1425,7 +1425,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
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<ConversationAttributes> {
* @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;
}
/**

@ -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);
})();

@ -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;
},
};
/**

@ -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<SignalService.ConfigurationMessage>
type IncomingConfResult = {
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(
`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
);

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

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

@ -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
);

@ -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();
}

@ -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<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) => {
const options = isGroup ? { conversationId: pkStr } : {};
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;
};
/**
* 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<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 = {
displayName: 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.
* 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<ConfigDumpRow>;
export type GetByPubkeyConfigDump = (pubkey: string) => Array<ConfigDumpRow>;
export type SaveConfigDump = (dump: ConfigDumpRow) => void;
export type GetAllDumps = () => Array<ConfigDumpRow>;
export type ConfigDumpDataNode = {
getConfigDumpByVariantAndPubkey: GetByVariantAndPubkeyConfigDump;
getConfigDumpsByPubkey: GetByPubkeyConfigDump;
saveConfigDump: SaveConfigDump;
getAllDumpsWithData: GetAllDumps;
getAllDumpsWithoutData: GetAllDumps;
};

@ -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);
}

@ -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;

@ -9,7 +9,7 @@ const internalCallLibSessionWorker = async ([
config,
fnName,
...args
]: LibSessionWorkerFunctions): Promise<any> => {
]: LibSessionWorkerFunctions): Promise<unknown> => {
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<any> => {
export const callLibSessionWorker = async (
callToMake: LibSessionWorkerFunctions
): Promise<unknown> => {
return internalCallLibSessionWorker(callToMake);
};

@ -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<EntryUserConfig | EntryContactsConfig> = 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<any>) {
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<any>, 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;
}

Loading…
Cancel
Save