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==