fix: add still broken promote handling with set_sig_keys

pull/2963/head
Audric Ackermann 2 years ago
parent bbb376fc2a
commit b259d18443

@ -130,7 +130,8 @@
"semver": "^7.5.4", "semver": "^7.5.4",
"styled-components": "5.1.1", "styled-components": "5.1.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"webrtc-adapter": "^4.1.1" "webrtc-adapter": "^4.1.1",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.7.1", "@commitlint/cli": "^17.7.1",

@ -80,7 +80,6 @@ message GroupUpdateDeleteMessage {
required bytes adminSignature = 2; required bytes adminSignature = 2;
} }
message GroupUpdateInfoChangeMessage { message GroupUpdateInfoChangeMessage {
enum Type { enum Type {
NAME = 1; NAME = 1;
@ -232,7 +231,8 @@ message DataMessage {
optional ClosedGroupControlMessage closedGroupControlMessage = 104; optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105; optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106; optional bool blocksCommunityMessageRequests = 106;
optional GroupUpdateMessage groupUpdateMessage = 120;} optional GroupUpdateMessage groupUpdateMessage = 120;
}
message CallMessage { message CallMessage {

@ -6,6 +6,7 @@ import { useConversationUsernameOrShorten } from '../hooks/useParamSelector';
import { PubKey } from '../session/types'; import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob'; import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob';
import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob';
import { import {
useMemberInviteFailed, useMemberInviteFailed,
useMemberInvitePending, useMemberInvitePending,
@ -14,7 +15,12 @@ import {
} from '../state/selectors/groups'; } from '../state/selectors/groups';
import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar';
import { Flex } from './basic/Flex'; 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'; import { SessionRadio } from './basic/SessionRadio';
const AvatarContainer = styled.div` const AvatarContainer = styled.div`
@ -92,7 +98,7 @@ type MemberListItemProps = {
groupPk?: string; groupPk?: string;
}; };
const ResendInviteContainer = ({ const ResendContainer = ({
displayGroupStatus, displayGroupStatus,
groupPk, groupPk,
pubkey, pubkey,
@ -105,8 +111,14 @@ const ResendInviteContainer = ({
!UserUtils.isUsFromCache(pubkey) !UserUtils.isUsFromCache(pubkey)
) { ) {
return ( return (
<Flex container={true} margin="0 0 0 auto" padding="0 var(--margins-lg)"> <Flex
container={true}
margin="0 0 0 auto"
padding="0 var(--margins-lg)"
gap="var(--margins-sm)"
>
<ResendInviteButton groupPk={groupPk} pubkey={pubkey} /> <ResendInviteButton groupPk={groupPk} pubkey={pubkey} />
<ResendPromoteButton groupPk={groupPk} pubkey={pubkey} />
</Flex> </Flex>
); );
} }
@ -177,7 +189,28 @@ const ResendInviteButton = ({
buttonType={SessionButtonType.Solid} buttonType={SessionButtonType.Solid}
text={window.i18n('resend')} text={window.i18n('resend')}
onClick={() => { onClick={() => {
void GroupInvite.addGroupInviteJob({ groupPk, member: pubkey }); void GroupInvite.addJob({ groupPk, member: pubkey });
}}
/>
);
};
const ResendPromoteButton = ({
groupPk,
pubkey,
}: {
pubkey: PubkeyType;
groupPk: GroupPubkeyType;
}) => {
return (
<SessionButton
dataTestId="resend-promote-button"
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
buttonColor={SessionButtonColor.Danger}
text="ReSEND PRomote"
onClick={() => {
void GroupPromote.addJob({ groupPk, member: pubkey });
}} }}
/> />
); );
@ -234,11 +267,7 @@ export const MemberListItem = ({
</Flex> </Flex>
</StyledInfo> </StyledInfo>
<ResendInviteContainer <ResendContainer pubkey={pubkey} displayGroupStatus={displayGroupStatus} groupPk={groupPk} />
pubkey={pubkey}
displayGroupStatus={displayGroupStatus}
groupPk={groupPk}
/>
{!inMentions && ( {!inMentions && (
<StyledCheckContainer> <StyledCheckContainer>

@ -27,6 +27,7 @@ export interface FlexProps {
| 'inherit'; | 'inherit';
// Child Props // Child Props
flexGrow?: number; flexGrow?: number;
gap?: string;
flexShrink?: number; flexShrink?: number;
flexBasis?: number; flexBasis?: number;
// Common Layout Props // Common Layout Props
@ -52,6 +53,7 @@ export const Flex = styled.div<FlexProps>`
align-items: ${props => props.alignItems || 'stretch'}; align-items: ${props => props.alignItems || 'stretch'};
margin: ${props => props.margin || '0'}; margin: ${props => props.margin || '0'};
padding: ${props => props.padding || '0'}; padding: ${props => props.padding || '0'};
gap: ${props => props.gap || undefined};
width: ${props => props.width || 'auto'}; width: ${props => props.width || 'auto'};
height: ${props => props.height || 'auto'}; height: ${props => props.height || 'auto'};
max-width: ${props => props.maxWidth || 'none'}; max-width: ${props => props.maxWidth || 'none'};

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { PubkeyType } from 'libsession_util_nodejs'; import { PubkeyType } from 'libsession_util_nodejs';
import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector'; import { useConversationsUsernameWithQuoteOrShortPk } from '../../../../hooks/useParamSelector';
import { arrayContainsUsOnly } from '../../../../models/message'; import { arrayContainsUsOnly } from '../../../../models/message';
import { PreConditionFailed } from '../../../../session/utils/errors'; import { PreConditionFailed } from '../../../../session/utils/errors';
import { import {
@ -89,7 +89,7 @@ const ChangeItemJoined = (added: Array<PubkeyType>): string => {
if (!added.length) { if (!added.length) {
throw new Error('Group update add is missing contacts'); throw new Error('Group update add is missing contacts');
} }
const names = useConversationsUsernameWithQuoteOrFullPubkey(added); const names = useConversationsUsernameWithQuoteOrShortPk(added);
const isGroupV2 = useSelectedIsGroupV2(); const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr(); const us = useOurPkStr();
if (isGroupV2) { if (isGroupV2) {
@ -107,7 +107,7 @@ const ChangeItemKicked = (removed: Array<PubkeyType>): string => {
if (!removed.length) { if (!removed.length) {
throw new Error('Group update removed is missing contacts'); throw new Error('Group update removed is missing contacts');
} }
const names = useConversationsUsernameWithQuoteOrFullPubkey(removed); const names = useConversationsUsernameWithQuoteOrShortPk(removed);
const isGroupV2 = useSelectedIsGroupV2(); const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr(); const us = useOurPkStr();
if (isGroupV2) { if (isGroupV2) {
@ -130,7 +130,7 @@ const ChangeItemPromoted = (promoted: Array<PubkeyType>): string => {
if (!promoted.length) { if (!promoted.length) {
throw new Error('Group update promoted is missing contacts'); throw new Error('Group update promoted is missing contacts');
} }
const names = useConversationsUsernameWithQuoteOrFullPubkey(promoted); const names = useConversationsUsernameWithQuoteOrShortPk(promoted);
const isGroupV2 = useSelectedIsGroupV2(); const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr(); const us = useOurPkStr();
if (isGroupV2) { if (isGroupV2) {
@ -148,7 +148,7 @@ const ChangeItemLeft = (left: Array<PubkeyType>): string => {
throw new Error('Group update remove is missing contacts'); throw new Error('Group update remove is missing contacts');
} }
const names = useConversationsUsernameWithQuoteOrFullPubkey(left); const names = useConversationsUsernameWithQuoteOrShortPk(left);
if (arrayContainsUsOnly(left)) { if (arrayContainsUsOnly(left)) {
return window.i18n('youLeftTheGroup'); return window.i18n('youLeftTheGroup');

@ -759,6 +759,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
expireTimer, expireTimer,
serverTimestamp: this.isPublic() ? networkTimestamp : undefined, serverTimestamp: this.isPublic() ? networkTimestamp : undefined,
groupInvitation, 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! // We're offline!
@ -1781,9 +1782,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const { id } = message; const { id } = message;
const destination = this.id as string; const destination = this.id as string;
const sentAt = message.get('sent_at'); 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) { if (!sentAt) {
throw new Error('sendMessageJob() sent_at is already set.'); throw new Error('sendMessageJob() sent_at is not set.');
} }
const networkTimestamp = GetNetworkTime.now(); const networkTimestamp = GetNetworkTime.now();

@ -131,7 +131,7 @@ async function mergeUserConfigsWithIncomingUpdates(
const needsPush = await GenericWrapperActions.needsPush(variant); const needsPush = await GenericWrapperActions.needsPush(variant);
const mergedTimestamps = sameVariant const mergedTimestamps = sameVariant
.filter(m => hashesMerged.includes(m.hash)) .filter(m => hashesMerged.includes(m.hash))
.map(m => m.timestamp); .map(m => m.storedAt);
const latestEnvelopeTimestamp = Math.max(...mergedTimestamps); const latestEnvelopeTimestamp = Math.max(...mergedTimestamps);
window.log.debug( window.log.debug(

@ -44,12 +44,13 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH
return; 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. // 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 // the sogs messages do not come as milliseconds but just seconds, so we override it
await innerHandleSwarmContentMessage({ await innerHandleSwarmContentMessage({
envelope, envelope,
sentAtTimestamp, envelopeTimestamp,
contentDecrypted: decryptedForAll.decryptedContent, contentDecrypted: decryptedForAll.decryptedContent,
messageHash, messageHash,
}); });
@ -400,10 +401,10 @@ export async function innerHandleSwarmContentMessage({
contentDecrypted, contentDecrypted,
envelope, envelope,
messageHash, messageHash,
sentAtTimestamp, envelopeTimestamp,
}: { }: {
envelope: EnvelopePlus; envelope: EnvelopePlus;
sentAtTimestamp: number; envelopeTimestamp: number;
contentDecrypted: ArrayBuffer; contentDecrypted: ArrayBuffer;
messageHash: string; messageHash: string;
}): Promise<void> { }): Promise<void> {
@ -437,7 +438,7 @@ export async function innerHandleSwarmContentMessage({
const isPrivateConversationMessage = !envelope.senderIdentity; const isPrivateConversationMessage = !envelope.senderIdentity;
if (isPrivateConversationMessage) { if (isPrivateConversationMessage) {
if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope, content)) { if (await shouldDropIncomingPrivateMessage(envelopeTimestamp, envelope, content)) {
await IncomingMessageCache.removeFromCache(envelope); await IncomingMessageCache.removeFromCache(envelope);
return; return;
} }
@ -468,7 +469,7 @@ export async function innerHandleSwarmContentMessage({
} }
await handleSwarmDataMessage( await handleSwarmDataMessage(
envelope, envelope,
sentAtTimestamp, envelopeTimestamp,
content.dataMessage as SignalService.DataMessage, content.dataMessage as SignalService.DataMessage,
messageHash, messageHash,
senderConversationModel senderConversationModel

@ -147,7 +147,7 @@ export function cleanIncomingDataMessage(rawDataMessage: SignalService.DataMessa
*/ */
export async function handleSwarmDataMessage( export async function handleSwarmDataMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,
sentAtTimestamp: number, envelopeTimestamp: number,
rawDataMessage: SignalService.DataMessage, rawDataMessage: SignalService.DataMessage,
messageHash: string, messageHash: string,
senderConversationModel: ConversationModel senderConversationModel: ConversationModel
@ -158,7 +158,7 @@ export async function handleSwarmDataMessage(
if (cleanDataMessage.groupUpdateMessage) { if (cleanDataMessage.groupUpdateMessage) {
await GroupV2Receiver.handleGroupUpdateMessage({ await GroupV2Receiver.handleGroupUpdateMessage({
envelopeTimestamp: sentAtTimestamp, envelopeTimestamp,
updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage, updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage,
source: envelope.source, source: envelope.source,
senderIdentity: envelope.senderIdentity, senderIdentity: envelope.senderIdentity,
@ -252,19 +252,19 @@ export async function handleSwarmDataMessage(
? createSwarmMessageSentFromUs({ ? createSwarmMessageSentFromUs({
conversationId: convoIdToAddTheMessageTo, conversationId: convoIdToAddTheMessageTo,
messageHash, messageHash,
sentAt: sentAtTimestamp, sentAt: envelopeTimestamp,
}) })
: createSwarmMessageSentFromNotUs({ : createSwarmMessageSentFromNotUs({
conversationId: convoIdToAddTheMessageTo, conversationId: convoIdToAddTheMessageTo,
messageHash, messageHash,
sender: senderConversationModel.id, sender: senderConversationModel.id,
sentAt: sentAtTimestamp, sentAt: envelopeTimestamp,
}); });
await handleSwarmMessage( await handleSwarmMessage(
msgModel, msgModel,
messageHash, messageHash,
sentAtTimestamp, envelopeTimestamp,
cleanDataMessage, cleanDataMessage,
convoToAddMessageTo, convoToAddMessageTo,
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises

@ -59,7 +59,7 @@ async function handleGroupInviteMessage({
); );
return; return;
} }
debugger;
const sigValid = await verifySig({ const sigValid = await verifySig({
pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId),
signature: inviteMessage.adminSignature, signature: inviteMessage.adminSignature,
@ -120,7 +120,6 @@ async function handleGroupInviteMessage({
await UserSync.queueNewJobIfNeeded(); await UserSync.queueNewJobIfNeeded();
// TODO currently sending auto-accept of invite. needs to be removed once we get the Group message request logic // 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'); console.warn('currently sending auto accept invite response');
await getMessageQueue().sendToGroupV2({ await getMessageQueue().sendToGroupV2({
message: new GroupUpdateInviteResponseMessage({ message: new GroupUpdateInviteResponseMessage({
@ -396,6 +395,7 @@ async function handleGroupUpdatePromoteMessage({
window.inboxStore.dispatch( window.inboxStore.dispatch(
groupInfoActions.markUsAsAdmin({ groupInfoActions.markUsAsAdmin({
groupPk, groupPk,
secret: groupKeypair.privateKey,
}) })
); );

@ -1,5 +1,5 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
import _, { isEmpty, last } from 'lodash'; import { isEmpty, last, toNumber } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
@ -27,12 +27,12 @@ const incomingMessagePromises: Array<Promise<any>> = [];
export async function handleSwarmContentDecryptedWithTimeout({ export async function handleSwarmContentDecryptedWithTimeout({
envelope, envelope,
messageHash, messageHash,
sentAtTimestamp, envelopeTimestamp,
contentDecrypted, contentDecrypted,
}: { }: {
envelope: EnvelopePlus; envelope: EnvelopePlus;
messageHash: string; messageHash: string;
sentAtTimestamp: number; envelopeTimestamp: number;
contentDecrypted: ArrayBuffer; contentDecrypted: ArrayBuffer;
}) { }) {
let taskDone = false; let taskDone = false;
@ -54,7 +54,7 @@ export async function handleSwarmContentDecryptedWithTimeout({
envelope, envelope,
messageHash, messageHash,
contentDecrypted, contentDecrypted,
sentAtTimestamp, envelopeTimestamp,
}); });
await IncomingMessageCache.removeFromCache(envelope); await IncomingMessageCache.removeFromCache(envelope);
} catch (e) { } catch (e) {
@ -297,16 +297,14 @@ async function handleDecryptedEnvelope({
contentDecrypted: ArrayBuffer; contentDecrypted: ArrayBuffer;
messageHash: string; messageHash: string;
}) { }) {
if (envelope.content) { if (!envelope.content) {
const sentAtTimestamp = _.toNumber(envelope.timestamp); await IncomingMessageCache.removeFromCache(envelope);
}
await innerHandleSwarmContentMessage({ return innerHandleSwarmContentMessage({
envelope, envelope,
sentAtTimestamp,
contentDecrypted, contentDecrypted,
messageHash, messageHash,
envelopeTimestamp: toNumber(envelope.timestamp),
}); });
} else {
await IncomingMessageCache.removeFromCache(envelope);
}
} }

@ -475,7 +475,7 @@ async function handleInboxOutboxMessages(
await innerHandleSwarmContentMessage({ await innerHandleSwarmContentMessage({
envelope: builtEnvelope, envelope: builtEnvelope,
sentAtTimestamp: postedAtInMs, envelopeTimestamp: postedAtInMs,
contentDecrypted: builtEnvelope.content, contentDecrypted: builtEnvelope.content,
messageHash: '', messageHash: '',
}); });

@ -134,13 +134,15 @@ async function buildRetrieveRequest(
): Promise<Array<RetrieveSubRequestType>> { ): Promise<Array<RetrieveSubRequestType>> {
const isUs = pubkey === ourPubkey; const isUs = pubkey === ourPubkey;
const maxSizeMap = SnodeNamespace.maxSizeMap(namespaces); const maxSizeMap = SnodeNamespace.maxSizeMap(namespaces);
const now = GetNetworkTime.now();
const retrieveRequestsParams: Array<RetrieveSubRequestType> = await Promise.all( const retrieveRequestsParams: Array<RetrieveSubRequestType> = await Promise.all(
namespaces.map(async (namespace, index) => { namespaces.map(async (namespace, index) => {
const foundMaxSize = maxSizeMap.find(m => m.namespace === namespace)?.maxSize; const foundMaxSize = maxSizeMap.find(m => m.namespace === namespace)?.maxSize;
const retrieveParam = { const retrieveParam = {
pubkey, pubkey,
last_hash: lastHashes.at(index) || '', last_hash: lastHashes.at(index) || '',
timestamp: GetNetworkTime.now(), timestamp: now,
max_size: foundMaxSize, max_size: foundMaxSize,
}; };
@ -197,7 +199,7 @@ async function buildRetrieveRequest(
params: { params: {
messages: configHashesToBump, messages: configHashesToBump,
expiry, expiry,
...signResult, ...omit(signResult, 'timestamp'),
pubkey, pubkey,
}, },
}; };
@ -262,13 +264,12 @@ async function retrieveNextMessages(
`_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}` `_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}`
); );
} }
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) { if (!window.inboxStore?.getState().onionPaths.isOnline) {
window.inboxStore?.dispatch(updateIsOnline(true)); window.inboxStore?.dispatch(updateIsOnline(true));
} }
try {
// we rely on the code of the first one to check for online status
const bodyFirstResult = firstResult.body;
GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t); GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t);
@ -280,9 +281,7 @@ async function retrieveNextMessages(
})); }));
} catch (e) { } catch (e) {
window?.log?.warn('exception while parsing json of nextMessage:', 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( throw new Error(
`_retrieveNextMessages - exception while parsing json of nextMessage ${targetNode.ip}:${targetNode.port}: ${e?.message}` `_retrieveNextMessages - exception while parsing json of nextMessage ${targetNode.ip}:${targetNode.port}: ${e?.message}`
); );

@ -12,6 +12,7 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface'; } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { getSodiumRenderer } from '../../../crypto/MessageEncrypter'; import { getSodiumRenderer } from '../../../crypto/MessageEncrypter';
import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; 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 { StringUtils, UserUtils } from '../../../utils';
import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String'; import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String';
import { PreConditionFailed } from '../../../utils/errors'; import { PreConditionFailed } from '../../../utils/errors';
@ -38,7 +39,6 @@ async function getGroupInviteMessage({
if (UserUtils.isUsFromCache(member)) { if (UserUtils.isUsFromCache(member)) {
throw new Error('getGroupInviteMessage: we cannot invite ourselves'); 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 // 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( const adminSignature = sodium.crypto_sign_detached(
@ -57,6 +57,29 @@ async function getGroupInviteMessage({
return invite; 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 = { type ParamsShared = {
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
namespace: SnodeNamespacesGroup; namespace: SnodeNamespacesGroup;
@ -275,6 +298,7 @@ async function getGroupSignatureByHashesParams({
export const SnodeGroupSignature = { export const SnodeGroupSignature = {
generateUpdateExpiryGroupSignature, generateUpdateExpiryGroupSignature,
getGroupInviteMessage, getGroupInviteMessage,
getGroupPromoteMessage,
getSnodeGroupSignature, getSnodeGroupSignature,
getGroupSignatureByHashesParams, getGroupSignatureByHashesParams,
signDataWithAdminSecret, signDataWithAdminSecret,

@ -2,6 +2,8 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/no-misused-promises */
import { GroupPubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType } from 'libsession_util_nodejs';
import { z } from 'zod';
import { import {
compact, compact,
concat, concat,
@ -9,6 +11,7 @@ import {
flatten, flatten,
isArray, isArray,
last, last,
omit,
sample, sample,
toNumber, toNumber,
uniqBy, uniqBy,
@ -23,7 +26,6 @@ import * as snodePool from './snodePool';
import { ConversationModel } from '../../../models/conversation'; import { ConversationModel } from '../../../models/conversation';
import { ConversationTypeEnum } from '../../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../../models/conversationAttributes';
import { EnvelopePlus } from '../../../receiver/types';
import { updateIsOnline } from '../../../state/ducks/onion'; import { updateIsOnline } from '../../../state/ducks/onion';
import { assertUnreachable } from '../../../types/sqlSharedTypes'; import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { import {
@ -35,6 +37,7 @@ import { DURATION, SWARM_POLLING_TIMEOUT } from '../../constants';
import { ConvoHub } from '../../conversations'; import { ConvoHub } from '../../conversations';
import { ed25519Str } from '../../onions/onionPath'; import { ed25519Str } from '../../onions/onionPath';
import { StringUtils, UserUtils } from '../../utils'; import { StringUtils, UserUtils } from '../../utils';
import { sleepFor } from '../../utils/Promise';
import { PreConditionFailed } from '../../utils/errors'; import { PreConditionFailed } from '../../utils/errors';
import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; import { LibSessionUtil } from '../../utils/libsession/libsession_utils';
import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces'; import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces';
@ -313,6 +316,7 @@ export class SwarmPolling {
return; return;
} }
if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) { if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) {
await sleepFor(100);
await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey); await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey);
} }
} }
@ -671,11 +675,30 @@ export class SwarmPolling {
} }
} }
function retrieveItemWithNamespace(results: Array<RetrieveRequestResult>) { // 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<RetrieveRequestResult>
): Array<RetrieveMessageItemWithNamespace> {
return flatten( return flatten(
compact( compact(
results.map( 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<T extends ConversationTypeEnum>(
); );
const confMessages = retrieveItemWithNamespace(userConfs); const confMessages = retrieveItemWithNamespace(userConfs);
const otherMessages = retrieveItemWithNamespace(userOthers); const otherMessages = retrieveItemWithNamespace(userOthers);
return { confMessages, otherMessages: uniqBy(otherMessages, x => x.hash) }; return { confMessages, otherMessages: uniqBy(otherMessages, x => x.hash) };
@ -719,7 +741,6 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
); );
const groupConfMessages = retrieveItemWithNamespace(groupConfs); const groupConfMessages = retrieveItemWithNamespace(groupConfs);
const groupOtherMessages = retrieveItemWithNamespace(groupOthers); const groupOtherMessages = retrieveItemWithNamespace(groupOthers);
return { return {
@ -733,11 +754,7 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
} }
} }
async function decryptForGroupV2(retrieveResult: { async function decryptForGroupV2(retrieveResult: { groupPk: string; content: Uint8Array }) {
groupPk: string;
content: Uint8Array;
sentTimestamp: number;
}): Promise<EnvelopePlus | null> {
window?.log?.info('received closed group message v2'); window?.log?.info('received closed group message v2');
try { try {
const groupPk = retrieveResult.groupPk; const groupPk = retrieveResult.groupPk;
@ -746,28 +763,22 @@ async function decryptForGroupV2(retrieveResult: {
} }
const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, retrieveResult.content); const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, retrieveResult.content);
const envelopePlus: EnvelopePlus = {
id: v4(),
senderIdentity: decrypted.pubkeyHex,
receivedAt: Date.now(),
content: decrypted.plaintext,
source: groupPk,
type: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,
timestamp: retrieveResult.sentTimestamp,
};
try {
// just try to parse what we have, it should be a protobuf content decrypted already // just try to parse what we have, it should be a protobuf content decrypted already
const parsedEnvelope = SignalService.Envelope.decode(new Uint8Array(decrypted.plaintext)); 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); 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 // the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message
return {
return envelopePlus; id: v4(),
senderIdentity: decrypted.pubkeyHex,
receivedAt: Date.now(),
content: parsedEnvelope.content,
source: groupPk,
type: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,
timestamp: parsedEnvelope.timestamp,
};
} catch (e) { } catch (e) {
window.log.warn('failed to decrypt message with error: ', e.message); window.log.warn('failed to decrypt message with error: ', e.message);
return null; return null;
@ -785,7 +796,6 @@ async function handleMessagesForGroupV2(
const envelopePlus = await decryptForGroupV2({ const envelopePlus = await decryptForGroupV2({
content: retrieveResult, content: retrieveResult,
groupPk, groupPk,
sentTimestamp: msg.timestamp,
}); });
if (!envelopePlus) { if (!envelopePlus) {
throw new Error('decryptForGroupV2 returned empty envelope'); throw new Error('decryptForGroupV2 returned empty envelope');
@ -797,7 +807,7 @@ async function handleMessagesForGroupV2(
envelope: envelopePlus, envelope: envelopePlus,
contentDecrypted: envelopePlus.content, contentDecrypted: envelopePlus.content,
messageHash: msg.hash, messageHash: msg.hash,
sentAtTimestamp: toNumber(envelopePlus.timestamp), envelopeTimestamp: toNumber(envelopePlus.timestamp),
}); });
} catch (e) { } catch (e) {
window.log.warn('failed to handle groupv2 otherMessage because of: ', e.message); window.log.warn('failed to handle groupv2 otherMessage because of: ', e.message);

@ -8,32 +8,37 @@ import { SnodeNamespaces } from '../namespaces';
import { RetrieveMessageItemWithNamespace } from '../types'; import { RetrieveMessageItemWithNamespace } from '../types';
async function handleGroupSharedConfigMessages( async function handleGroupSharedConfigMessages(
groupConfigMessagesMerged: Array<RetrieveMessageItemWithNamespace>, groupConfigMessages: Array<RetrieveMessageItemWithNamespace>,
groupPk: GroupPubkeyType groupPk: GroupPubkeyType
) { ) {
try { try {
window.log.info( window.log.info(
`received groupConfigMessagesMerged count: ${ `received groupConfigMessages count: ${groupConfigMessages.length} for groupPk:${ed25519Str(
groupConfigMessagesMerged.length groupPk
} 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) .filter(m => m.namespace === SnodeNamespaces.ClosedGroupInfo)
.map(info => { .map(info => {
return { data: fromBase64ToArray(info.data), hash: info.hash }; return { data: fromBase64ToArray(info.data), hash: info.hash };
}); });
const members = groupConfigMessagesMerged const members = groupConfigMessages
.filter(m => m.namespace === SnodeNamespaces.ClosedGroupMembers) .filter(m => m.namespace === SnodeNamespaces.ClosedGroupMembers)
.map(info => { .map(info => {
return { data: fromBase64ToArray(info.data), hash: info.hash }; return { data: fromBase64ToArray(info.data), hash: info.hash };
}); });
const keys = groupConfigMessagesMerged const keys = groupConfigMessages
.filter(m => m.namespace === SnodeNamespaces.ClosedGroupKeys) .filter(m => m.namespace === SnodeNamespaces.ClosedGroupKeys)
.map(info => { .map(info => {
return { return {
data: fromBase64ToArray(info.data), data: fromBase64ToArray(info.data),
hash: info.hash, hash: info.hash,
timestampMs: info.timestamp, timestampMs: info.storedAt,
}; };
}); });
const toMerge = { const toMerge = {
@ -42,6 +47,11 @@ async function handleGroupSharedConfigMessages(
groupMember: members, 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 // do the merge with our current state
await MetaGroupWrapperActions.metaMerge(groupPk, toMerge); await MetaGroupWrapperActions.metaMerge(groupPk, toMerge);
// save updated dumps to the DB right away // save updated dumps to the DB right away
@ -55,7 +65,7 @@ async function handleGroupSharedConfigMessages(
); );
} catch (e) { } catch (e) {
window.log.warn( window.log.warn(
`handleGroupSharedConfigMessages of ${groupConfigMessagesMerged.length} failed with ${e.message}` `handleGroupSharedConfigMessages of ${groupConfigMessages.length} failed with ${e.message}`
); );
// not rethrowing // not rethrowing
} }

@ -3,8 +3,8 @@ import { SnodeNamespaces } from './namespaces';
export type RetrieveMessageItem = { export type RetrieveMessageItem = {
hash: string; hash: string;
expiration: number; expiration: number;
data: string; // base64 encrypted content of the emssage data: string; // base64 encrypted content of the message
timestamp: number; storedAt: number; // **not** the envelope timestamp, but when the message was effectively stored on the snode
}; };
export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & { export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & {

@ -1,4 +1,5 @@
import { GroupPubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { MessageEncrypter, concatUInt8Array, getSodiumRenderer } from '.'; import { MessageEncrypter, concatUInt8Array, getSodiumRenderer } from '.';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
@ -116,14 +117,14 @@ export async function encryptUsingSessionProtocol(
); );
const signature = sodium.crypto_sign_detached(verificationData, userED25519SecretKeyBytes); const signature = sodium.crypto_sign_detached(verificationData, userED25519SecretKeyBytes);
if (!signature || signature.length === 0) { if (isEmpty(signature)) {
throw new Error("Couldn't sign message"); throw new Error("Couldn't sign message");
} }
const plaintextWithMetadata = concatUInt8Array(plaintext, userED25519PubKeyBytes, signature); const plaintextWithMetadata = concatUInt8Array(plaintext, userED25519PubKeyBytes, signature);
const ciphertext = sodium.crypto_box_seal(plaintextWithMetadata, recipientX25519PublicKey); const ciphertext = sodium.crypto_box_seal(plaintextWithMetadata, recipientX25519PublicKey);
if (!ciphertext) { if (isEmpty(ciphertext)) {
throw new Error("Couldn't encrypt message."); throw new Error("Couldn't encrypt message.");
} }
return ciphertext; return ciphertext;

@ -147,9 +147,7 @@ async function addUpdateMessage(
if (diff.newName) { if (diff.newName) {
groupUpdate.name = diff.newName; groupUpdate.name = diff.newName;
} } else if (diff.joiningMembers) {
if (diff.joiningMembers) {
groupUpdate.joined = diff.joiningMembers; groupUpdate.joined = diff.joiningMembers;
} else if (diff.leavingMembers) { } else if (diff.leavingMembers) {
groupUpdate.left = diff.leavingMembers; groupUpdate.left = diff.leavingMembers;

@ -39,6 +39,7 @@ import { GroupUpdateMemberChangeMessage } from '../messages/outgoing/controlMess
import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage'; import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage';
import { GroupUpdateDeleteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateDeleteMessage'; import { GroupUpdateDeleteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateDeleteMessage';
import { GroupUpdateInviteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; 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'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
type ClosedGroupMessageType = type ClosedGroupMessageType =
@ -269,6 +270,7 @@ export class MessageQueue {
| CallMessage | CallMessage
| ClosedGroupMemberLeftMessage | ClosedGroupMemberLeftMessage
| GroupUpdateInviteMessage | GroupUpdateInviteMessage
| GroupUpdatePromoteMessage
| GroupUpdateDeleteMessage; | GroupUpdateDeleteMessage;
namespace: SnodeNamespaces; namespace: SnodeNamespaces;
}): Promise<number | null> { }): Promise<number | null> {
@ -283,6 +285,7 @@ export class MessageQueue {
); );
return effectiveTimestamp; return effectiveTimestamp;
} catch (error) { } catch (error) {
window.log.error('failed to send message with: ', error.message);
if (rawMessage) { if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error); await MessageSentHandler.handleMessageSentFailure(rawMessage, error);
} }

@ -136,7 +136,7 @@ export class PendingMessageCache {
!isNumber(message.networkTimestampCreated) || !isNumber(message.networkTimestampCreated) ||
message.networkTimestampCreated <= 0 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 const plainTextBuffer = from_hex(message.plainTextBufferHex); // if a plaintextBufferHex is unset or not hex, this throws and we remove that message entirely

@ -203,13 +203,6 @@ export async function timeout<T>(promise: Promise<T>, timeoutMs: number): Promis
return Promise.race([timeoutPromise, promise]); return Promise.race([timeoutPromise, promise]);
} }
export async function delay(timeoutMs: number = 2000): Promise<boolean> {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, timeoutMs);
});
}
export const sleepFor = async (ms: number, showLog = false) => { export const sleepFor = async (ms: number, showLog = false) => {
if (showLog) { if (showLog) {

@ -6,6 +6,7 @@ import { persistedJobFromData } from './JobDeserialization';
import { import {
AvatarDownloadPersistedData, AvatarDownloadPersistedData,
GroupInvitePersistedData, GroupInvitePersistedData,
GroupPromotePersistedData,
GroupSyncPersistedData, GroupSyncPersistedData,
PersistedJob, PersistedJob,
RunJobResult, RunJobResult,
@ -359,14 +360,21 @@ const avatarDownloadRunner = new PersistedJobRunner<AvatarDownloadPersistedData>
'AvatarDownloadJob', 'AvatarDownloadJob',
null null
); );
const groupInviteJobRunner = new PersistedJobRunner<GroupInvitePersistedData>( const groupInviteJobRunner = new PersistedJobRunner<GroupInvitePersistedData>(
'GroupInviteJob', 'GroupInviteJob',
null null
); );
const groupPromoteJobRunner = new PersistedJobRunner<GroupPromotePersistedData>(
'GroupPromoteJob',
null
);
export const runners = { export const runners = {
userSyncRunner, userSyncRunner,
groupSyncRunner, groupSyncRunner,
avatarDownloadRunner, avatarDownloadRunner,
groupInviteJobRunner, groupInviteJobRunner,
groupPromoteJobRunner,
}; };

@ -6,6 +6,7 @@ export type PersistedJobType =
| 'GroupSyncJobType' | 'GroupSyncJobType'
| 'AvatarDownloadJobType' | 'AvatarDownloadJobType'
| 'GroupInviteJobType' | 'GroupInviteJobType'
| 'GroupPromoteJobType'
| 'FakeSleepForJobType' | 'FakeSleepForJobType'
| 'FakeSleepForJobMultiType'; | 'FakeSleepForJobMultiType';
@ -40,6 +41,12 @@ export interface GroupInvitePersistedData extends PersistedJobData {
member: PubkeyType; member: PubkeyType;
} }
export interface GroupPromotePersistedData extends PersistedJobData {
jobType: 'GroupPromoteJobType';
groupPk: GroupPubkeyType;
member: PubkeyType;
}
export interface UserSyncPersistedData extends PersistedJobData { export interface UserSyncPersistedData extends PersistedJobData {
jobType: 'UserSyncJobType'; jobType: 'UserSyncJobType';
} }
@ -53,7 +60,8 @@ export type TypeOfPersistedData =
| FakeSleepJobData | FakeSleepJobData
| FakeSleepForMultiJobData | FakeSleepForMultiJobData
| GroupSyncPersistedData | GroupSyncPersistedData
| GroupInvitePersistedData; | GroupInvitePersistedData
| GroupPromotePersistedData;
export type AddJobCheckReturn = 'skipAddSameJobPresent' | 'sameJobDataAlreadyInQueue' | null; export type AddJobCheckReturn = 'skipAddSameJobPresent' | 'sameJobDataAlreadyInQueue' | null;

@ -26,7 +26,7 @@ type JobExtraArgs = {
member: PubkeyType; member: PubkeyType;
}; };
export function shouldAddGroupInviteJob(args: JobExtraArgs) { export function shouldAddJob(args: JobExtraArgs) {
if (UserUtils.isUsFromCache(args.member)) { if (UserUtils.isUsFromCache(args.member)) {
return false; return false;
} }
@ -42,8 +42,8 @@ const invitesFailed = new Map<
} }
>(); >();
async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) { async function addJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddGroupInviteJob({ groupPk, member })) { if (shouldAddJob({ groupPk, member })) {
const groupInviteJob = new GroupInviteJob({ const groupInviteJob = new GroupInviteJob({
groupPk, groupPk,
member, member,
@ -190,7 +190,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
export const GroupInvite = { export const GroupInvite = {
GroupInviteJob, GroupInviteJob,
addGroupInviteJob, addJob,
}; };
function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) { function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) {
let thisGroupFailure = invitesFailed.get(groupPk); let thisGroupFailure = invitesFailed.get(groupPk);

@ -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<GroupPromotePersistedData> {
constructor({
groupPk,
member,
nextAttemptTimestamp,
maxAttempts,
currentRetry,
identifier,
}: Pick<GroupPromotePersistedData, 'groupPk' | 'member'> &
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<RunJobResult> {
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<GroupPromotePersistedData>) {
return [];
}
public addJobCheck(jobs: Array<GroupPromotePersistedData>): 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,
};

@ -4,4 +4,5 @@ export type JobRunnerType =
| 'FakeSleepForJob' | 'FakeSleepForJob'
| 'FakeSleepForMultiJob' | 'FakeSleepForMultiJob'
| 'AvatarDownloadJob' | 'AvatarDownloadJob'
| 'GroupInviteJob'; | 'GroupInviteJob'
| 'GroupPromoteJob';

@ -5,6 +5,7 @@ import {
GroupMemberGet, GroupMemberGet,
GroupPubkeyType, GroupPubkeyType,
PubkeyType, PubkeyType,
Uint8ArrayLen64,
UserGroupsGet, UserGroupsGet,
WithGroupPubkey, WithGroupPubkey,
} from 'libsession_util_nodejs'; } from 'libsession_util_nodejs';
@ -17,7 +18,6 @@ import { SignalService } from '../../protobuf';
import { getMessageQueue } from '../../session'; import { getMessageQueue } from '../../session';
import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; 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 { RevokeChanges, SnodeAPIRevoke } from '../../session/apis/snode_api/revokeSubaccount';
import { SnodeGroupSignature } from '../../session/apis/snode_api/signature/groupSignature'; import { SnodeGroupSignature } from '../../session/apis/snode_api/signature/groupSignature';
import { ConvoHub } from '../../session/conversations'; import { ConvoHub } from '../../session/conversations';
@ -76,9 +76,10 @@ type GroupDetailsUpdate = {
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) { async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) {
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
const inGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
const haveAdminkey = await UserGroupsWrapperActions.getGroup(groupPk); const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
if (!haveAdminkey || inGroup?.promoted) { const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
if (isEmpty(inUserGroup?.secretKey) || !usInGroup?.promoted) {
throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`); 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. // can update the groupwrapper with a failed state if a message fails to be sent.
for (let index = 0; index < membersFromWrapper.length; index++) { for (let index = 0; index < membersFromWrapper.length; index++) {
const member = membersFromWrapper[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 }); await openConversationWithMessages({ conversationKey: groupPk, messageId: null });
@ -460,6 +461,10 @@ async function handleWithoutHistoryMembers({
const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member); const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member);
await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false); await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false);
} }
if (!isEmpty(withoutHistory)) {
await MetaGroupWrapperActions.keyRekey(groupPk);
}
} }
async function handleRemoveMembers({ async function handleRemoveMembers({
@ -471,6 +476,7 @@ async function handleRemoveMembers({
if (!fromCurrentDevice) { if (!fromCurrentDevice) {
return; return;
} }
await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, removed); await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, removed);
const createAtNetworkTimestamp = GetNetworkTime.now(); 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' '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({ // const sentStatus = await getMessageQueue().sendToPubKeyNonDurably({
pubkey: PubKey.cast(m), // pubkey: PubKey.cast(m),
message: deleteMessage, // message: deleteMessage,
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages, // namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
}); // });
if (!sentStatus) { // if (!sentStatus) {
window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m); // 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'); // }
}
}) })
); );
} }
@ -591,11 +596,11 @@ async function handleMemberChangeFromUIOrNot({
// schedule send invite details, auth signature, etc. to the new users // schedule send invite details, auth signature, etc. to the new users
for (let index = 0; index < withoutHistory.length; index++) { for (let index = 0; index < withoutHistory.length; index++) {
const member = withoutHistory[index]; const member = withoutHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member }); await GroupInvite.addJob({ groupPk, member });
} }
for (let index = 0; index < withHistory.length; index++) { for (let index = 0; index < withHistory.length; index++) {
const member = withHistory[index]; const member = withHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member }); await GroupInvite.addJob({ groupPk, member });
} }
const sodium = await getSodiumRenderer(); const sodium = await getSodiumRenderer();
@ -755,8 +760,10 @@ const markUsAsAdmin = createAsyncThunk(
async ( async (
{ {
groupPk, groupPk,
secret,
}: { }: {
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
secret: Uint8ArrayLen64;
}, },
payloadCreator payloadCreator
): Promise<GroupDetailsUpdate> => { ): Promise<GroupDetailsUpdate> => {
@ -764,6 +771,12 @@ const markUsAsAdmin = createAsyncThunk(
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) { if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('markUsAsAdmin group not present in redux slice'); 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(); const us = UserUtils.getOurPubKeyStrFromCache();
if (state.groups.members[groupPk].find(m => m.pubkeyHex === us)?.admin) { 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]) { if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('inviteResponseReceived group but not present in redux slice'); throw new PreConditionFailed('inviteResponseReceived group but not present in redux slice');
} }
try {
await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived'); await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived');
await MetaGroupWrapperActions.memberSetAccepted(groupPk, member); await MetaGroupWrapperActions.memberSetAccepted(groupPk, member);
await GroupSync.queueNewJobIfNeeded(groupPk); 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 { return {
groupPk, groupPk,

@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
import { createSelector } from '@reduxjs/toolkit'; 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 { import {
ConversationLookupType, ConversationLookupType,

@ -290,7 +290,7 @@ export function toFixedUint8ArrayOfLength<T extends number>(
} }
export function stringify(obj: unknown) { export function stringify(obj: unknown) {
return JSON.stringify(obj, (_key, value) => return JSON.stringify(obj, (_key, value) => {
value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value return value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value;
); });
} }

@ -106,7 +106,7 @@ export class WorkerInterface {
reject: (error: any) => { reject: (error: any) => {
this._removeJob(id); this._removeJob(id);
const end = Date.now(); const end = Date.now();
window.log.info( window.log.debug(
`Worker job ${id} (${fnName}) failed in ${end - start}ms with ${error.message}` `Worker job ${id} (${fnName}) failed in ${end - start}ms with ${error.message}`
); );
return reject(error); return reject(error);

@ -14,6 +14,7 @@ import {
ProfilePicture, ProfilePicture,
PubkeyType, PubkeyType,
Uint8ArrayLen100, Uint8ArrayLen100,
Uint8ArrayLen64,
UserConfigWrapperActionsCalls, UserConfigWrapperActionsCalls,
UserGroupsSet, UserGroupsSet,
UserGroupsWrapperActionsCalls, UserGroupsWrapperActionsCalls,
@ -540,6 +541,11 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
'swarmVerifySubAccount', 'swarmVerifySubAccount',
signingValue, signingValue,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['swarmVerifySubAccount']>>, ]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['swarmVerifySubAccount']>>,
setSigKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
return callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'setSigKeys', secret]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['setSigKeys']>
>;
},
}; };
export const callLibSessionWorker = async ( export const callLibSessionWorker = async (

@ -7870,3 +7870,8 @@ yocto-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 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==

Loading…
Cancel
Save