From b259d1844307399716e8134bca6d61d4fb2df4d5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 16 Nov 2023 10:52:43 +1100 Subject: [PATCH] fix: add still broken promote handling with set_sig_keys --- package.json | 3 +- protos/SignalService.proto | 4 +- ts/components/MemberListItem.tsx | 47 ++++-- ts/components/basic/Flex.tsx | 2 + .../message-item/GroupUpdateMessage.tsx | 10 +- ts/models/conversation.ts | 7 +- ts/receiver/configMessage.ts | 2 +- ts/receiver/contentMessage.ts | 13 +- ts/receiver/dataMessage.ts | 10 +- ts/receiver/groupv2/handleGroupV2Message.ts | 4 +- ts/receiver/receiver.ts | 26 ++- .../apis/open_group_api/sogsv3/sogsApiV3.ts | 2 +- ts/session/apis/snode_api/retrieveRequest.ts | 17 +- .../snode_api/signature/groupSignature.ts | 26 ++- ts/session/apis/snode_api/swarmPolling.ts | 66 ++++---- .../SwarmPollingGroupConfig.ts | 28 ++-- ts/session/apis/snode_api/types.ts | 4 +- ts/session/crypto/MessageEncrypter.ts | 5 +- ts/session/group/closed-group.ts | 4 +- ts/session/sending/MessageQueue.ts | 3 + ts/session/sending/PendingMessageCache.ts | 2 +- ts/session/utils/Promise.ts | 7 - ts/session/utils/job_runners/JobRunner.ts | 8 + ts/session/utils/job_runners/PersistedJob.ts | 10 +- .../utils/job_runners/jobs/GroupInviteJob.ts | 8 +- .../utils/job_runners/jobs/GroupPromoteJob.ts | 151 ++++++++++++++++++ .../utils/job_runners/jobs/JobRunnerType.ts | 3 +- ts/state/ducks/groups.ts | 56 ++++--- ts/state/selectors/conversations.ts | 2 +- ts/types/sqlSharedTypes.ts | 6 +- ts/webworker/worker_interface.ts | 2 +- .../browser/libsession_worker_interface.ts | 6 + yarn.lock | 5 + 33 files changed, 408 insertions(+), 141 deletions(-) create mode 100644 ts/session/utils/job_runners/jobs/GroupPromoteJob.ts diff --git a/package.json b/package.json index 6a8f22d9a..20c59c7ea 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,8 @@ "semver": "^7.5.4", "styled-components": "5.1.1", "uuid": "8.3.2", - "webrtc-adapter": "^4.1.1" + "webrtc-adapter": "^4.1.1", + "zod": "^3.22.4" }, "devDependencies": { "@commitlint/cli": "^17.7.1", diff --git a/protos/SignalService.proto b/protos/SignalService.proto index c6fcdada5..eb0af27df 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -80,7 +80,6 @@ message GroupUpdateDeleteMessage { required bytes adminSignature = 2; } - message GroupUpdateInfoChangeMessage { enum Type { NAME = 1; @@ -232,7 +231,8 @@ message DataMessage { optional ClosedGroupControlMessage closedGroupControlMessage = 104; optional string syncTarget = 105; optional bool blocksCommunityMessageRequests = 106; - optional GroupUpdateMessage groupUpdateMessage = 120;} + optional GroupUpdateMessage groupUpdateMessage = 120; +} message CallMessage { diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index f785430af..d66eebbd3 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -6,6 +6,7 @@ import { useConversationUsernameOrShorten } from '../hooks/useParamSelector'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob'; +import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob'; import { useMemberInviteFailed, useMemberInvitePending, @@ -14,7 +15,12 @@ import { } from '../state/selectors/groups'; import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { Flex } from './basic/Flex'; -import { SessionButton, SessionButtonShape, SessionButtonType } from './basic/SessionButton'; +import { + SessionButton, + SessionButtonColor, + SessionButtonShape, + SessionButtonType, +} from './basic/SessionButton'; import { SessionRadio } from './basic/SessionRadio'; const AvatarContainer = styled.div` @@ -92,7 +98,7 @@ type MemberListItemProps = { groupPk?: string; }; -const ResendInviteContainer = ({ +const ResendContainer = ({ displayGroupStatus, groupPk, pubkey, @@ -105,8 +111,14 @@ const ResendInviteContainer = ({ !UserUtils.isUsFromCache(pubkey) ) { return ( - + + ); } @@ -177,7 +189,28 @@ const ResendInviteButton = ({ buttonType={SessionButtonType.Solid} text={window.i18n('resend')} onClick={() => { - void GroupInvite.addGroupInviteJob({ groupPk, member: pubkey }); + void GroupInvite.addJob({ groupPk, member: pubkey }); + }} + /> + ); +}; + +const ResendPromoteButton = ({ + groupPk, + pubkey, +}: { + pubkey: PubkeyType; + groupPk: GroupPubkeyType; +}) => { + return ( + { + void GroupPromote.addJob({ groupPk, member: pubkey }); }} /> ); @@ -234,11 +267,7 @@ export const MemberListItem = ({ - + {!inMentions && ( diff --git a/ts/components/basic/Flex.tsx b/ts/components/basic/Flex.tsx index 650e66982..63dd034a4 100644 --- a/ts/components/basic/Flex.tsx +++ b/ts/components/basic/Flex.tsx @@ -27,6 +27,7 @@ export interface FlexProps { | 'inherit'; // Child Props flexGrow?: number; + gap?: string; flexShrink?: number; flexBasis?: number; // Common Layout Props @@ -52,6 +53,7 @@ export const Flex = styled.div` align-items: ${props => props.alignItems || 'stretch'}; margin: ${props => props.margin || '0'}; padding: ${props => props.padding || '0'}; + gap: ${props => props.gap || undefined}; width: ${props => props.width || 'auto'}; height: ${props => props.height || 'auto'}; max-width: ${props => props.maxWidth || 'none'}; diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index 5cfc34cc7..d077577ca 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { PubkeyType } from 'libsession_util_nodejs'; -import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector'; +import { useConversationsUsernameWithQuoteOrShortPk } from '../../../../hooks/useParamSelector'; import { arrayContainsUsOnly } from '../../../../models/message'; import { PreConditionFailed } from '../../../../session/utils/errors'; import { @@ -89,7 +89,7 @@ const ChangeItemJoined = (added: Array): string => { if (!added.length) { throw new Error('Group update add is missing contacts'); } - const names = useConversationsUsernameWithQuoteOrFullPubkey(added); + const names = useConversationsUsernameWithQuoteOrShortPk(added); const isGroupV2 = useSelectedIsGroupV2(); const us = useOurPkStr(); if (isGroupV2) { @@ -107,7 +107,7 @@ const ChangeItemKicked = (removed: Array): string => { if (!removed.length) { throw new Error('Group update removed is missing contacts'); } - const names = useConversationsUsernameWithQuoteOrFullPubkey(removed); + const names = useConversationsUsernameWithQuoteOrShortPk(removed); const isGroupV2 = useSelectedIsGroupV2(); const us = useOurPkStr(); if (isGroupV2) { @@ -130,7 +130,7 @@ const ChangeItemPromoted = (promoted: Array): string => { if (!promoted.length) { throw new Error('Group update promoted is missing contacts'); } - const names = useConversationsUsernameWithQuoteOrFullPubkey(promoted); + const names = useConversationsUsernameWithQuoteOrShortPk(promoted); const isGroupV2 = useSelectedIsGroupV2(); const us = useOurPkStr(); if (isGroupV2) { @@ -148,7 +148,7 @@ const ChangeItemLeft = (left: Array): string => { throw new Error('Group update remove is missing contacts'); } - const names = useConversationsUsernameWithQuoteOrFullPubkey(left); + const names = useConversationsUsernameWithQuoteOrShortPk(left); if (arrayContainsUsOnly(left)) { return window.i18n('youLeftTheGroup'); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2d0d4fc8e..5de2f69e7 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -759,6 +759,7 @@ export class ConversationModel extends Backbone.Model { expireTimer, serverTimestamp: this.isPublic() ? networkTimestamp : undefined, groupInvitation, + sent_at: networkTimestamp, // overriden later, but we need one to have the sorting done in the UI even when the sending is pending }); // We're offline! @@ -1781,9 +1782,9 @@ export class ConversationModel extends Backbone.Model { const { id } = message; const destination = this.id as string; - const sentAt = message.get('sent_at'); - if (sentAt) { - throw new Error('sendMessageJob() sent_at is already set.'); + const sentAt = message.get('sent_at'); // this is used to store the timestamp when we tried sending that message, it should be set by the caller + if (!sentAt) { + throw new Error('sendMessageJob() sent_at is not set.'); } const networkTimestamp = GetNetworkTime.now(); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 58a83d25b..08aace3f7 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -131,7 +131,7 @@ async function mergeUserConfigsWithIncomingUpdates( const needsPush = await GenericWrapperActions.needsPush(variant); const mergedTimestamps = sameVariant .filter(m => hashesMerged.includes(m.hash)) - .map(m => m.timestamp); + .map(m => m.storedAt); const latestEnvelopeTimestamp = Math.max(...mergedTimestamps); window.log.debug( diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 33f13b55e..2c755fb79 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -44,12 +44,13 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH return; } - const sentAtTimestamp = toNumber(envelope.timestamp); + const envelopeTimestamp = toNumber(envelope.timestamp); + // swarm messages already comes with a timestamp in milliseconds, so this sentAtTimestamp is correct. // the sogs messages do not come as milliseconds but just seconds, so we override it await innerHandleSwarmContentMessage({ envelope, - sentAtTimestamp, + envelopeTimestamp, contentDecrypted: decryptedForAll.decryptedContent, messageHash, }); @@ -400,10 +401,10 @@ export async function innerHandleSwarmContentMessage({ contentDecrypted, envelope, messageHash, - sentAtTimestamp, + envelopeTimestamp, }: { envelope: EnvelopePlus; - sentAtTimestamp: number; + envelopeTimestamp: number; contentDecrypted: ArrayBuffer; messageHash: string; }): Promise { @@ -437,7 +438,7 @@ export async function innerHandleSwarmContentMessage({ const isPrivateConversationMessage = !envelope.senderIdentity; if (isPrivateConversationMessage) { - if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope, content)) { + if (await shouldDropIncomingPrivateMessage(envelopeTimestamp, envelope, content)) { await IncomingMessageCache.removeFromCache(envelope); return; } @@ -468,7 +469,7 @@ export async function innerHandleSwarmContentMessage({ } await handleSwarmDataMessage( envelope, - sentAtTimestamp, + envelopeTimestamp, content.dataMessage as SignalService.DataMessage, messageHash, senderConversationModel diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 7f156cd91..1a2624e80 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -147,7 +147,7 @@ export function cleanIncomingDataMessage(rawDataMessage: SignalService.DataMessa */ export async function handleSwarmDataMessage( envelope: EnvelopePlus, - sentAtTimestamp: number, + envelopeTimestamp: number, rawDataMessage: SignalService.DataMessage, messageHash: string, senderConversationModel: ConversationModel @@ -158,7 +158,7 @@ export async function handleSwarmDataMessage( if (cleanDataMessage.groupUpdateMessage) { await GroupV2Receiver.handleGroupUpdateMessage({ - envelopeTimestamp: sentAtTimestamp, + envelopeTimestamp, updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage, source: envelope.source, senderIdentity: envelope.senderIdentity, @@ -252,19 +252,19 @@ export async function handleSwarmDataMessage( ? createSwarmMessageSentFromUs({ conversationId: convoIdToAddTheMessageTo, messageHash, - sentAt: sentAtTimestamp, + sentAt: envelopeTimestamp, }) : createSwarmMessageSentFromNotUs({ conversationId: convoIdToAddTheMessageTo, messageHash, sender: senderConversationModel.id, - sentAt: sentAtTimestamp, + sentAt: envelopeTimestamp, }); await handleSwarmMessage( msgModel, messageHash, - sentAtTimestamp, + envelopeTimestamp, cleanDataMessage, convoToAddMessageTo, // eslint-disable-next-line @typescript-eslint/no-misused-promises diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index c1a8bd607..72efa8b8d 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -59,7 +59,7 @@ async function handleGroupInviteMessage({ ); return; } - debugger; + const sigValid = await verifySig({ pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), signature: inviteMessage.adminSignature, @@ -120,7 +120,6 @@ async function handleGroupInviteMessage({ await UserSync.queueNewJobIfNeeded(); // TODO currently sending auto-accept of invite. needs to be removed once we get the Group message request logic - debugger; console.warn('currently sending auto accept invite response'); await getMessageQueue().sendToGroupV2({ message: new GroupUpdateInviteResponseMessage({ @@ -396,6 +395,7 @@ async function handleGroupUpdatePromoteMessage({ window.inboxStore.dispatch( groupInfoActions.markUsAsAdmin({ groupPk, + secret: groupKeypair.privateKey, }) ); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 8040dd9ad..f55b6b646 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -1,5 +1,5 @@ /* eslint-disable more/no-then */ -import _, { isEmpty, last } from 'lodash'; +import { isEmpty, last, toNumber } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { EnvelopePlus } from './types'; @@ -27,12 +27,12 @@ const incomingMessagePromises: Array> = []; export async function handleSwarmContentDecryptedWithTimeout({ envelope, messageHash, - sentAtTimestamp, + envelopeTimestamp, contentDecrypted, }: { envelope: EnvelopePlus; messageHash: string; - sentAtTimestamp: number; + envelopeTimestamp: number; contentDecrypted: ArrayBuffer; }) { let taskDone = false; @@ -54,7 +54,7 @@ export async function handleSwarmContentDecryptedWithTimeout({ envelope, messageHash, contentDecrypted, - sentAtTimestamp, + envelopeTimestamp, }); await IncomingMessageCache.removeFromCache(envelope); } catch (e) { @@ -297,16 +297,14 @@ async function handleDecryptedEnvelope({ contentDecrypted: ArrayBuffer; messageHash: string; }) { - if (envelope.content) { - const sentAtTimestamp = _.toNumber(envelope.timestamp); - - await innerHandleSwarmContentMessage({ - envelope, - sentAtTimestamp, - contentDecrypted, - messageHash, - }); - } else { + if (!envelope.content) { await IncomingMessageCache.removeFromCache(envelope); } + + return innerHandleSwarmContentMessage({ + envelope, + contentDecrypted, + messageHash, + envelopeTimestamp: toNumber(envelope.timestamp), + }); } diff --git a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts index 7f412259f..38c0f36d5 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts @@ -475,7 +475,7 @@ async function handleInboxOutboxMessages( await innerHandleSwarmContentMessage({ envelope: builtEnvelope, - sentAtTimestamp: postedAtInMs, + envelopeTimestamp: postedAtInMs, contentDecrypted: builtEnvelope.content, messageHash: '', }); diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 8fa70c5d1..50fccb03b 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -134,13 +134,15 @@ async function buildRetrieveRequest( ): Promise> { const isUs = pubkey === ourPubkey; const maxSizeMap = SnodeNamespace.maxSizeMap(namespaces); + const now = GetNetworkTime.now(); + const retrieveRequestsParams: Array = await Promise.all( namespaces.map(async (namespace, index) => { const foundMaxSize = maxSizeMap.find(m => m.namespace === namespace)?.maxSize; const retrieveParam = { pubkey, last_hash: lastHashes.at(index) || '', - timestamp: GetNetworkTime.now(), + timestamp: now, max_size: foundMaxSize, }; @@ -197,7 +199,7 @@ async function buildRetrieveRequest( params: { messages: configHashesToBump, expiry, - ...signResult, + ...omit(signResult, 'timestamp'), pubkey, }, }; @@ -262,13 +264,12 @@ async function retrieveNextMessages( `_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}` ); } - + if (!window.inboxStore?.getState().onionPaths.isOnline) { + window.inboxStore?.dispatch(updateIsOnline(true)); + } try { // we rely on the code of the first one to check for online status const bodyFirstResult = firstResult.body; - if (!window.inboxStore?.getState().onionPaths.isOnline) { - window.inboxStore?.dispatch(updateIsOnline(true)); - } GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t); @@ -280,9 +281,7 @@ async function retrieveNextMessages( })); } catch (e) { window?.log?.warn('exception while parsing json of nextMessage:', e); - if (!window.inboxStore?.getState().onionPaths.isOnline) { - window.inboxStore?.dispatch(updateIsOnline(true)); - } + throw new Error( `_retrieveNextMessages - exception while parsing json of nextMessage ${targetNode.ip}:${targetNode.port}: ${e?.message}` ); diff --git a/ts/session/apis/snode_api/signature/groupSignature.ts b/ts/session/apis/snode_api/signature/groupSignature.ts index c4c849389..3d2545188 100644 --- a/ts/session/apis/snode_api/signature/groupSignature.ts +++ b/ts/session/apis/snode_api/signature/groupSignature.ts @@ -12,6 +12,7 @@ import { } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { getSodiumRenderer } from '../../../crypto/MessageEncrypter'; import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; +import { GroupUpdatePromoteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdatePromoteMessage'; import { StringUtils, UserUtils } from '../../../utils'; import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String'; import { PreConditionFailed } from '../../../utils/errors'; @@ -38,7 +39,6 @@ async function getGroupInviteMessage({ if (UserUtils.isUsFromCache(member)) { throw new Error('getGroupInviteMessage: we cannot invite ourselves'); } - debugger; // Note: as the signature is built with the timestamp here, we cannot override the timestamp later on the sending pipeline const adminSignature = sodium.crypto_sign_detached( @@ -57,6 +57,29 @@ async function getGroupInviteMessage({ return invite; } +async function getGroupPromoteMessage({ + member, + secretKey, + groupPk, +}: { + member: PubkeyType; + secretKey: Uint8ArrayLen64; // len 64 + groupPk: GroupPubkeyType; +}) { + const createAtNetworkTimestamp = GetNetworkTime.now(); + + if (UserUtils.isUsFromCache(member)) { + throw new Error('getGroupPromoteMessage: we cannot promote ourselves'); + } + + const msg = new GroupUpdatePromoteMessage({ + groupPk, + createAtNetworkTimestamp, + groupIdentitySeed: secretKey.slice(0, 32), // the seed is the first 32 bytes of the secretkey + }); + return msg; +} + type ParamsShared = { groupPk: GroupPubkeyType; namespace: SnodeNamespacesGroup; @@ -275,6 +298,7 @@ async function getGroupSignatureByHashesParams({ export const SnodeGroupSignature = { generateUpdateExpiryGroupSignature, getGroupInviteMessage, + getGroupPromoteMessage, getSnodeGroupSignature, getGroupSignatureByHashesParams, signDataWithAdminSecret, diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 90a3f7c21..eccd290bd 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -2,6 +2,8 @@ /* eslint-disable more/no-then */ /* eslint-disable @typescript-eslint/no-misused-promises */ import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { z } from 'zod'; + import { compact, concat, @@ -9,6 +11,7 @@ import { flatten, isArray, last, + omit, sample, toNumber, uniqBy, @@ -23,7 +26,6 @@ import * as snodePool from './snodePool'; import { ConversationModel } from '../../../models/conversation'; import { ConversationTypeEnum } from '../../../models/conversationAttributes'; -import { EnvelopePlus } from '../../../receiver/types'; import { updateIsOnline } from '../../../state/ducks/onion'; import { assertUnreachable } from '../../../types/sqlSharedTypes'; import { @@ -35,6 +37,7 @@ import { DURATION, SWARM_POLLING_TIMEOUT } from '../../constants'; import { ConvoHub } from '../../conversations'; import { ed25519Str } from '../../onions/onionPath'; import { StringUtils, UserUtils } from '../../utils'; +import { sleepFor } from '../../utils/Promise'; import { PreConditionFailed } from '../../utils/errors'; import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces'; @@ -313,6 +316,7 @@ export class SwarmPolling { return; } if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) { + await sleepFor(100); await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey); } } @@ -671,11 +675,30 @@ export class SwarmPolling { } } -function retrieveItemWithNamespace(results: Array) { +// zod schema for retrieve items as returned by the snodes +const retrieveItemSchema = z.object({ + hash: z.string(), + data: z.string(), + expiration: z.number(), + timestamp: z.number(), +}); + +function retrieveItemWithNamespace( + results: Array +): Array { return flatten( compact( results.map( - result => result.messages.messages?.map(r => ({ ...r, namespace: result.namespace })) + result => + result.messages.messages?.map(r => { + // throws if the result is not expected + const parsedItem = retrieveItemSchema.parse(r); + return { + ...omit(parsedItem, 'timestamp'), + namespace: result.namespace, + storedAt: parsedItem.timestamp, + }; + }) ) ) ); @@ -698,7 +721,6 @@ function filterMessagesPerTypeOfConvo( ); const confMessages = retrieveItemWithNamespace(userConfs); - const otherMessages = retrieveItemWithNamespace(userOthers); return { confMessages, otherMessages: uniqBy(otherMessages, x => x.hash) }; @@ -719,7 +741,6 @@ function filterMessagesPerTypeOfConvo( ); const groupConfMessages = retrieveItemWithNamespace(groupConfs); - const groupOtherMessages = retrieveItemWithNamespace(groupOthers); return { @@ -733,11 +754,7 @@ function filterMessagesPerTypeOfConvo( } } -async function decryptForGroupV2(retrieveResult: { - groupPk: string; - content: Uint8Array; - sentTimestamp: number; -}): Promise { +async function decryptForGroupV2(retrieveResult: { groupPk: string; content: Uint8Array }) { window?.log?.info('received closed group message v2'); try { const groupPk = retrieveResult.groupPk; @@ -746,28 +763,22 @@ async function decryptForGroupV2(retrieveResult: { } const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, retrieveResult.content); - const envelopePlus: EnvelopePlus = { + // just try to parse what we have, it should be a protobuf content decrypted already + const parsedEnvelope = SignalService.Envelope.decode(new Uint8Array(decrypted.plaintext)); + + // not doing anything, just enforcing that the content is indeed a protobuf object of type Content, or throws + SignalService.Content.decode(parsedEnvelope.content); + + // the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message + return { id: v4(), senderIdentity: decrypted.pubkeyHex, receivedAt: Date.now(), - content: decrypted.plaintext, + content: parsedEnvelope.content, source: groupPk, type: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE, - timestamp: retrieveResult.sentTimestamp, + timestamp: parsedEnvelope.timestamp, }; - try { - // just try to parse what we have, it should be a protobuf content decrypted already - const parsedEnvelope = SignalService.Envelope.decode(new Uint8Array(decrypted.plaintext)); - - SignalService.Content.decode(parsedEnvelope.content); - envelopePlus.content = parsedEnvelope.content; - } catch (e) { - throw new Error('content got from libsession does not look to be envelope+decryptedContent'); - } - - // the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message - - return envelopePlus; } catch (e) { window.log.warn('failed to decrypt message with error: ', e.message); return null; @@ -785,7 +796,6 @@ async function handleMessagesForGroupV2( const envelopePlus = await decryptForGroupV2({ content: retrieveResult, groupPk, - sentTimestamp: msg.timestamp, }); if (!envelopePlus) { throw new Error('decryptForGroupV2 returned empty envelope'); @@ -797,7 +807,7 @@ async function handleMessagesForGroupV2( envelope: envelopePlus, contentDecrypted: envelopePlus.content, messageHash: msg.hash, - sentAtTimestamp: toNumber(envelopePlus.timestamp), + envelopeTimestamp: toNumber(envelopePlus.timestamp), }); } catch (e) { window.log.warn('failed to handle groupv2 otherMessage because of: ', e.message); diff --git a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts index d7d16a6ef..5f460acbd 100644 --- a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts +++ b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts @@ -8,32 +8,37 @@ import { SnodeNamespaces } from '../namespaces'; import { RetrieveMessageItemWithNamespace } from '../types'; async function handleGroupSharedConfigMessages( - groupConfigMessagesMerged: Array, + groupConfigMessages: Array, groupPk: GroupPubkeyType ) { try { window.log.info( - `received groupConfigMessagesMerged count: ${ - groupConfigMessagesMerged.length - } for groupPk:${ed25519Str(groupPk)}` + `received groupConfigMessages count: ${groupConfigMessages.length} for groupPk:${ed25519Str( + groupPk + )}` ); - const infos = groupConfigMessagesMerged + + if (groupConfigMessages.find(m => !m.storedAt)) { + debugger; + throw new Error('all incoming group config message should have a timestamp'); + } + const infos = groupConfigMessages .filter(m => m.namespace === SnodeNamespaces.ClosedGroupInfo) .map(info => { return { data: fromBase64ToArray(info.data), hash: info.hash }; }); - const members = groupConfigMessagesMerged + const members = groupConfigMessages .filter(m => m.namespace === SnodeNamespaces.ClosedGroupMembers) .map(info => { return { data: fromBase64ToArray(info.data), hash: info.hash }; }); - const keys = groupConfigMessagesMerged + const keys = groupConfigMessages .filter(m => m.namespace === SnodeNamespaces.ClosedGroupKeys) .map(info => { return { data: fromBase64ToArray(info.data), hash: info.hash, - timestampMs: info.timestamp, + timestampMs: info.storedAt, }; }); const toMerge = { @@ -42,6 +47,11 @@ async function handleGroupSharedConfigMessages( groupMember: members, }; + window.log.info( + `received keys: ${toMerge.groupKeys.length},infos: ${toMerge.groupInfo.length},members: ${ + toMerge.groupMember.length + } for groupPk:${ed25519Str(groupPk)}` + ); // do the merge with our current state await MetaGroupWrapperActions.metaMerge(groupPk, toMerge); // save updated dumps to the DB right away @@ -55,7 +65,7 @@ async function handleGroupSharedConfigMessages( ); } catch (e) { window.log.warn( - `handleGroupSharedConfigMessages of ${groupConfigMessagesMerged.length} failed with ${e.message}` + `handleGroupSharedConfigMessages of ${groupConfigMessages.length} failed with ${e.message}` ); // not rethrowing } diff --git a/ts/session/apis/snode_api/types.ts b/ts/session/apis/snode_api/types.ts index d7fd39145..dec8dece2 100644 --- a/ts/session/apis/snode_api/types.ts +++ b/ts/session/apis/snode_api/types.ts @@ -3,8 +3,8 @@ import { SnodeNamespaces } from './namespaces'; export type RetrieveMessageItem = { hash: string; expiration: number; - data: string; // base64 encrypted content of the emssage - timestamp: number; + data: string; // base64 encrypted content of the message + storedAt: number; // **not** the envelope timestamp, but when the message was effectively stored on the snode }; export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & { diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index f0deb80f4..3cca8a2fe 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,4 +1,5 @@ import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { isEmpty } from 'lodash'; import { MessageEncrypter, concatUInt8Array, getSodiumRenderer } from '.'; import { Data } from '../../data/data'; import { SignalService } from '../../protobuf'; @@ -116,14 +117,14 @@ export async function encryptUsingSessionProtocol( ); const signature = sodium.crypto_sign_detached(verificationData, userED25519SecretKeyBytes); - if (!signature || signature.length === 0) { + if (isEmpty(signature)) { throw new Error("Couldn't sign message"); } const plaintextWithMetadata = concatUInt8Array(plaintext, userED25519PubKeyBytes, signature); const ciphertext = sodium.crypto_box_seal(plaintextWithMetadata, recipientX25519PublicKey); - if (!ciphertext) { + if (isEmpty(ciphertext)) { throw new Error("Couldn't encrypt message."); } return ciphertext; diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index d95a6ee9a..7f7dddf28 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -147,9 +147,7 @@ async function addUpdateMessage( if (diff.newName) { groupUpdate.name = diff.newName; - } - - if (diff.joiningMembers) { + } else if (diff.joiningMembers) { groupUpdate.joined = diff.joiningMembers; } else if (diff.leavingMembers) { groupUpdate.left = diff.leavingMembers; diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 002f27b0b..1e938fb55 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -39,6 +39,7 @@ import { GroupUpdateMemberChangeMessage } from '../messages/outgoing/controlMess import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage'; import { GroupUpdateDeleteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateDeleteMessage'; import { GroupUpdateInviteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; +import { GroupUpdatePromoteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdatePromoteMessage'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; type ClosedGroupMessageType = @@ -269,6 +270,7 @@ export class MessageQueue { | CallMessage | ClosedGroupMemberLeftMessage | GroupUpdateInviteMessage + | GroupUpdatePromoteMessage | GroupUpdateDeleteMessage; namespace: SnodeNamespaces; }): Promise { @@ -283,6 +285,7 @@ export class MessageQueue { ); return effectiveTimestamp; } catch (error) { + window.log.error('failed to send message with: ', error.message); if (rawMessage) { await MessageSentHandler.handleMessageSentFailure(rawMessage, error); } diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 153a635f0..917a02dfb 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -136,7 +136,7 @@ export class PendingMessageCache { !isNumber(message.networkTimestampCreated) || message.networkTimestampCreated <= 0 ) { - throw new Error('networkTimestampCreated is emptyo <=0'); + throw new Error('networkTimestampCreated is empty <=0'); } const plainTextBuffer = from_hex(message.plainTextBufferHex); // if a plaintextBufferHex is unset or not hex, this throws and we remove that message entirely diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index 087568e2b..c9de5b730 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -203,13 +203,6 @@ export async function timeout(promise: Promise, timeoutMs: number): Promis return Promise.race([timeoutPromise, promise]); } -export async function delay(timeoutMs: number = 2000): Promise { - return new Promise(resolve => { - setTimeout(() => { - resolve(true); - }, timeoutMs); - }); -} export const sleepFor = async (ms: number, showLog = false) => { if (showLog) { diff --git a/ts/session/utils/job_runners/JobRunner.ts b/ts/session/utils/job_runners/JobRunner.ts index f5c3b5ae1..60cb65b4c 100644 --- a/ts/session/utils/job_runners/JobRunner.ts +++ b/ts/session/utils/job_runners/JobRunner.ts @@ -6,6 +6,7 @@ import { persistedJobFromData } from './JobDeserialization'; import { AvatarDownloadPersistedData, GroupInvitePersistedData, + GroupPromotePersistedData, GroupSyncPersistedData, PersistedJob, RunJobResult, @@ -359,14 +360,21 @@ const avatarDownloadRunner = new PersistedJobRunner 'AvatarDownloadJob', null ); + const groupInviteJobRunner = new PersistedJobRunner( 'GroupInviteJob', null ); +const groupPromoteJobRunner = new PersistedJobRunner( + 'GroupPromoteJob', + null +); + export const runners = { userSyncRunner, groupSyncRunner, avatarDownloadRunner, groupInviteJobRunner, + groupPromoteJobRunner, }; diff --git a/ts/session/utils/job_runners/PersistedJob.ts b/ts/session/utils/job_runners/PersistedJob.ts index c496d7184..4d031c63c 100644 --- a/ts/session/utils/job_runners/PersistedJob.ts +++ b/ts/session/utils/job_runners/PersistedJob.ts @@ -6,6 +6,7 @@ export type PersistedJobType = | 'GroupSyncJobType' | 'AvatarDownloadJobType' | 'GroupInviteJobType' + | 'GroupPromoteJobType' | 'FakeSleepForJobType' | 'FakeSleepForJobMultiType'; @@ -40,6 +41,12 @@ export interface GroupInvitePersistedData extends PersistedJobData { member: PubkeyType; } +export interface GroupPromotePersistedData extends PersistedJobData { + jobType: 'GroupPromoteJobType'; + groupPk: GroupPubkeyType; + member: PubkeyType; +} + export interface UserSyncPersistedData extends PersistedJobData { jobType: 'UserSyncJobType'; } @@ -53,7 +60,8 @@ export type TypeOfPersistedData = | FakeSleepJobData | FakeSleepForMultiJobData | GroupSyncPersistedData - | GroupInvitePersistedData; + | GroupInvitePersistedData + | GroupPromotePersistedData; export type AddJobCheckReturn = 'skipAddSameJobPresent' | 'sameJobDataAlreadyInQueue' | null; diff --git a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts index 85c7d41d2..4fb1636a5 100644 --- a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts @@ -26,7 +26,7 @@ type JobExtraArgs = { member: PubkeyType; }; -export function shouldAddGroupInviteJob(args: JobExtraArgs) { +export function shouldAddJob(args: JobExtraArgs) { if (UserUtils.isUsFromCache(args.member)) { return false; } @@ -42,8 +42,8 @@ const invitesFailed = new Map< } >(); -async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) { - if (shouldAddGroupInviteJob({ groupPk, member })) { +async function addJob({ groupPk, member }: JobExtraArgs) { + if (shouldAddJob({ groupPk, member })) { const groupInviteJob = new GroupInviteJob({ groupPk, member, @@ -190,7 +190,7 @@ class GroupInviteJob extends PersistedJob { export const GroupInvite = { GroupInviteJob, - addGroupInviteJob, + addJob, }; function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) { let thisGroupFailure = invitesFailed.get(groupPk); diff --git a/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts b/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts new file mode 100644 index 000000000..a8e9f1710 --- /dev/null +++ b/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts @@ -0,0 +1,151 @@ +import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; +import { isNumber } from 'lodash'; +import { v4 } from 'uuid'; +import { UserUtils } from '../..'; +import { + MetaGroupWrapperActions, + UserGroupsWrapperActions, +} from '../../../../webworker/workers/browser/libsession_worker_interface'; +import { SnodeNamespaces } from '../../../apis/snode_api/namespaces'; +import { SnodeGroupSignature } from '../../../apis/snode_api/signature/groupSignature'; +import { getMessageQueue } from '../../../sending'; +import { PubKey } from '../../../types'; +import { runners } from '../JobRunner'; +import { + AddJobCheckReturn, + GroupPromotePersistedData, + PersistedJob, + RunJobResult, +} from '../PersistedJob'; + +const defaultMsBetweenRetries = 10000; +const defaultMaxAttemps = 1; + +type JobExtraArgs = { + groupPk: GroupPubkeyType; + member: PubkeyType; +}; + +export function shouldAddJob(args: JobExtraArgs) { + if (UserUtils.isUsFromCache(args.member)) { + return false; + } + + return true; +} + +async function addJob({ groupPk, member }: JobExtraArgs) { + if (shouldAddJob({ groupPk, member })) { + const groupPromoteJob = new GroupPromoteJob({ + groupPk, + member, + nextAttemptTimestamp: Date.now(), + }); + window.log.debug(`addGroupPromoteJob: adding group promote for ${groupPk}:${member} `); + await runners.groupPromoteJobRunner.addJob(groupPromoteJob); + } +} + +class GroupPromoteJob extends PersistedJob { + constructor({ + groupPk, + member, + nextAttemptTimestamp, + maxAttempts, + currentRetry, + identifier, + }: Pick & + Partial< + Pick< + GroupPromotePersistedData, + | 'nextAttemptTimestamp' + | 'identifier' + | 'maxAttempts' + | 'delayBetweenRetries' + | 'currentRetry' + > + >) { + super({ + jobType: 'GroupPromoteJobType', + identifier: identifier || v4(), + member, + groupPk, + delayBetweenRetries: defaultMsBetweenRetries, + maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttemps, + nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries, + currentRetry: isNumber(currentRetry) ? currentRetry : 0, + }); + } + + public async run(): Promise { + const { groupPk, member, jobType, identifier } = this.persistedData; + + window.log.info( + `running job ${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" ` + ); + const group = await UserGroupsWrapperActions.getGroup(groupPk); + if (!group || !group.secretKey || !group.name) { + window.log.warn(`GroupPromoteJob: Did not find group in wrapper or no valid info in wrapper`); + return RunJobResult.PermanentFailure; + } + + if (UserUtils.isUsFromCache(member)) { + return RunJobResult.Success; + } + let failed = true; + try { + const message = await SnodeGroupSignature.getGroupPromoteMessage({ + member, + secretKey: group.secretKey, + groupPk, + }); + + const storedAt = await getMessageQueue().sendToPubKeyNonDurably({ + message, + namespace: SnodeNamespaces.Default, + pubkey: PubKey.cast(member), + }); + if (storedAt !== null) { + failed = false; + } + } finally { + try { + await MetaGroupWrapperActions.memberSetPromoted(groupPk, member, failed); + } catch (e) { + window.log.warn('GroupPromoteJob memberSetPromoted failed with', e.message); + } + } + // return true so this job is marked as a success and we don't need to retry it + return RunJobResult.Success; + } + + public serializeJob(): GroupPromotePersistedData { + return super.serializeBase(); + } + + public nonRunningJobsToRemove(_jobs: Array) { + return []; + } + + public addJobCheck(jobs: Array): AddJobCheckReturn { + // avoid adding the same job if the exact same one is already planned + const hasSameJob = jobs.some(j => { + return j.groupPk === this.persistedData.groupPk && j.member === this.persistedData.member; + }); + + if (hasSameJob) { + return 'skipAddSameJobPresent'; + } + + return null; + } + + public getJobTimeoutMs(): number { + return 15000; + } +} + +export const GroupPromote = { + GroupPromoteJob, + addJob, +}; diff --git a/ts/session/utils/job_runners/jobs/JobRunnerType.ts b/ts/session/utils/job_runners/jobs/JobRunnerType.ts index a9ac9aba7..ecd573ef1 100644 --- a/ts/session/utils/job_runners/jobs/JobRunnerType.ts +++ b/ts/session/utils/job_runners/jobs/JobRunnerType.ts @@ -4,4 +4,5 @@ export type JobRunnerType = | 'FakeSleepForJob' | 'FakeSleepForMultiJob' | 'AvatarDownloadJob' - | 'GroupInviteJob'; + | 'GroupInviteJob' + | 'GroupPromoteJob'; diff --git a/ts/state/ducks/groups.ts b/ts/state/ducks/groups.ts index 190e69f24..710be201c 100644 --- a/ts/state/ducks/groups.ts +++ b/ts/state/ducks/groups.ts @@ -5,6 +5,7 @@ import { GroupMemberGet, GroupPubkeyType, PubkeyType, + Uint8ArrayLen64, UserGroupsGet, WithGroupPubkey, } from 'libsession_util_nodejs'; @@ -17,7 +18,6 @@ import { SignalService } from '../../protobuf'; import { getMessageQueue } from '../../session'; import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; -import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces'; import { RevokeChanges, SnodeAPIRevoke } from '../../session/apis/snode_api/revokeSubaccount'; import { SnodeGroupSignature } from '../../session/apis/snode_api/signature/groupSignature'; import { ConvoHub } from '../../session/conversations'; @@ -76,9 +76,10 @@ type GroupDetailsUpdate = { async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) { const us = UserUtils.getOurPubKeyStrFromCache(); - const inGroup = await MetaGroupWrapperActions.memberGet(groupPk, us); - const haveAdminkey = await UserGroupsWrapperActions.getGroup(groupPk); - if (!haveAdminkey || inGroup?.promoted) { + + const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us); + const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk); + if (isEmpty(inUserGroup?.secretKey) || !usInGroup?.promoted) { throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`); } } @@ -185,7 +186,7 @@ const initNewGroupInWrapper = createAsyncThunk( // can update the groupwrapper with a failed state if a message fails to be sent. for (let index = 0; index < membersFromWrapper.length; index++) { const member = membersFromWrapper[index]; - await GroupInvite.addGroupInviteJob({ member: member.pubkeyHex, groupPk }); + await GroupInvite.addJob({ member: member.pubkeyHex, groupPk }); } await openConversationWithMessages({ conversationKey: groupPk, messageId: null }); @@ -460,6 +461,10 @@ async function handleWithoutHistoryMembers({ const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member); await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false); } + + if (!isEmpty(withoutHistory)) { + await MetaGroupWrapperActions.keyRekey(groupPk); + } } async function handleRemoveMembers({ @@ -471,6 +476,7 @@ async function handleRemoveMembers({ if (!fromCurrentDevice) { return; } + await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, removed); const createAtNetworkTimestamp = GetNetworkTime.now(); @@ -489,15 +495,14 @@ async function handleRemoveMembers({ 'TODO: poll from namespace -11, handle messages and sig for it, batch request handle 401/403, but 200 ok for this -11 namespace' ); - const sentStatus = await getMessageQueue().sendToPubKeyNonDurably({ - pubkey: PubKey.cast(m), - message: deleteMessage, - namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages, - }); - if (!sentStatus) { - window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m); - throw new Error('Failed to send a GroupUpdateDeleteMessage to a member removed'); - } + // const sentStatus = await getMessageQueue().sendToPubKeyNonDurably({ + // pubkey: PubKey.cast(m), + // message: deleteMessage, + // namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages, + // }); + // if (!sentStatus) { + // window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m); + // } }) ); } @@ -591,11 +596,11 @@ async function handleMemberChangeFromUIOrNot({ // schedule send invite details, auth signature, etc. to the new users for (let index = 0; index < withoutHistory.length; index++) { const member = withoutHistory[index]; - await GroupInvite.addGroupInviteJob({ groupPk, member }); + await GroupInvite.addJob({ groupPk, member }); } for (let index = 0; index < withHistory.length; index++) { const member = withHistory[index]; - await GroupInvite.addGroupInviteJob({ groupPk, member }); + await GroupInvite.addJob({ groupPk, member }); } const sodium = await getSodiumRenderer(); @@ -755,8 +760,10 @@ const markUsAsAdmin = createAsyncThunk( async ( { groupPk, + secret, }: { groupPk: GroupPubkeyType; + secret: Uint8ArrayLen64; }, payloadCreator ): Promise => { @@ -764,6 +771,12 @@ const markUsAsAdmin = createAsyncThunk( if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) { throw new PreConditionFailed('markUsAsAdmin group not present in redux slice'); } + if (secret.length !== 64) { + throw new PreConditionFailed('markUsAsAdmin secret needs to be 64'); + } + console.warn('before setSigKeys ', groupPk, stringify(secret)); + await MetaGroupWrapperActions.setSigKeys(groupPk, secret); + console.warn('after setSigKeys'); const us = UserUtils.getOurPubKeyStrFromCache(); if (state.groups.members[groupPk].find(m => m.pubkeyHex === us)?.admin) { @@ -801,10 +814,15 @@ const inviteResponseReceived = createAsyncThunk( if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) { throw new PreConditionFailed('inviteResponseReceived group but not present in redux slice'); } - await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived'); + try { + await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived'); - await MetaGroupWrapperActions.memberSetAccepted(groupPk, member); - await GroupSync.queueNewJobIfNeeded(groupPk); + await MetaGroupWrapperActions.memberSetAccepted(groupPk, member); + await GroupSync.queueNewJobIfNeeded(groupPk); + } catch (e) { + window.log.info('inviteResponseReceived failed with', e.message); + // only admins can do the steps above, but we don't want to throw if we are not an admin + } return { groupPk, diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 0a4c55870..d60c5a6a2 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-syntax */ import { createSelector } from '@reduxjs/toolkit'; -import { filter, isEmpty, isNumber, pick, sortBy, toNumber, isFinite } from 'lodash'; +import { filter, isEmpty, isFinite, isNumber, pick, sortBy, toNumber } from 'lodash'; import { ConversationLookupType, diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts index 8dc30380a..87de18d4f 100644 --- a/ts/types/sqlSharedTypes.ts +++ b/ts/types/sqlSharedTypes.ts @@ -290,7 +290,7 @@ export function toFixedUint8ArrayOfLength( } export function stringify(obj: unknown) { - return JSON.stringify(obj, (_key, value) => - value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value - ); + return JSON.stringify(obj, (_key, value) => { + return value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value; + }); } diff --git a/ts/webworker/worker_interface.ts b/ts/webworker/worker_interface.ts index 84f29845a..6c3836f54 100644 --- a/ts/webworker/worker_interface.ts +++ b/ts/webworker/worker_interface.ts @@ -106,7 +106,7 @@ export class WorkerInterface { reject: (error: any) => { this._removeJob(id); const end = Date.now(); - window.log.info( + window.log.debug( `Worker job ${id} (${fnName}) failed in ${end - start}ms with ${error.message}` ); return reject(error); diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index cf489d9a7..5c6e6fbd2 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -14,6 +14,7 @@ import { ProfilePicture, PubkeyType, Uint8ArrayLen100, + Uint8ArrayLen64, UserConfigWrapperActionsCalls, UserGroupsSet, UserGroupsWrapperActionsCalls, @@ -540,6 +541,11 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = { 'swarmVerifySubAccount', signingValue, ]) as Promise>, + setSigKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => { + return callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'setSigKeys', secret]) as Promise< + ReturnType + >; + }, }; export const callLibSessionWorker = async ( diff --git a/yarn.lock b/yarn.lock index f9c7f8bea..418d42219 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7870,3 +7870,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==