fix: add setadmin on promote accepts

also sign/verify group update messages
pull/2963/head
Audric Ackermann 1 year ago
parent 16e7ee1cd6
commit 9595f09085

@ -90,12 +90,11 @@ message GroupUpdateInviteMessage {
required string name = 2; required string name = 2;
required bytes memberAuthData = 3; required bytes memberAuthData = 3;
// @required // @required
required bytes adminSignature = 6; required bytes adminSignature = 4;
} }
message GroupUpdateDeleteMessage { message GroupUpdateDeleteMessage {
// @required repeated string memberSessionIds = 1;
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required bytes adminSignature = 2; required bytes adminSignature = 2;
} }
@ -111,6 +110,7 @@ message GroupUpdateInfoChangeMessage {
required Type type = 1; required Type type = 1;
optional string updatedName = 2; optional string updatedName = 2;
optional uint32 updatedExpiration = 3; optional uint32 updatedExpiration = 3;
required bytes adminSignature = 4;
} }
message GroupUpdateMemberChangeMessage { message GroupUpdateMemberChangeMessage {
@ -123,6 +123,7 @@ message GroupUpdateMemberChangeMessage {
// @required // @required
required Type type = 1; required Type type = 1;
repeated string memberSessionIds = 2; repeated string memberSessionIds = 2;
required bytes adminSignature = 3;
} }
@ -143,8 +144,8 @@ message GroupUpdateInviteResponseMessage {
message GroupUpdateDeleteMemberContentMessage { message GroupUpdateDeleteMemberContentMessage {
repeated string memberSessionIds = 1; repeated string memberSessionIds = 1;
// @required repeated string messageHashes = 2;
required bytes adminSignature = 2; optional bytes adminSignature = 3;
} }
message GroupUpdateMessage { message GroupUpdateMessage {

@ -997,14 +997,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (!this.id) { if (!this.id) {
throw new Error('A message always needs an id'); throw new Error('A message always needs an id');
} }
console.warn('this.attributes', JSON.stringify(this.attributes));
perfStart(`messageCommit-${this.id}`);
// because the saving to db calls _cleanData which mutates the field for cleaning, we need to save a copy // because the saving to db calls _cleanData which mutates the field for cleaning, we need to save a copy
const id = await Data.saveMessage(cloneDeep(this.attributes)); const id = await Data.saveMessage(cloneDeep(this.attributes));
if (triggerUIUpdate) { if (triggerUIUpdate) {
this.dispatchMessageUpdate(); this.dispatchMessageUpdate();
} }
perfEnd(`messageCommit-${this.id}`, 'messageCommit');
return id; return id;
} }

@ -38,8 +38,6 @@ import { ECKeyPair } from './keypairs';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) { export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try { try {
console.warn('444 envelope.source', envelope.source);
console.warn('444 envelope.senderIdentity', envelope.senderIdentity);
const decryptedForAll = await decrypt(envelope); const decryptedForAll = await decrypt(envelope);
if (!decryptedForAll || !decryptedForAll.decryptedContent || isEmpty(decryptedForAll)) { if (!decryptedForAll || !decryptedForAll.decryptedContent || isEmpty(decryptedForAll)) {

@ -1,16 +1,22 @@
import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash'; import { isEmpty, isFinite, isNumber } from 'lodash';
import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings'; import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ConvoHub } from '../../session/conversations'; import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto';
import { ClosedGroup } from '../../session/group/closed-group'; import { ClosedGroup } from '../../session/group/closed-group';
import { ed25519Str } from '../../session/onions/onionPath'; import { ed25519Str } from '../../session/onions/onionPath';
import { PubKey } from '../../session/types'; import { PubKey } from '../../session/types';
import { UserUtils } from '../../session/utils'; import { UserUtils } from '../../session/utils';
import { stringToUint8Array } from '../../session/utils/String';
import { PreConditionFailed } from '../../session/utils/errors';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils'; import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { groupInfoActions } from '../../state/ducks/groups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
import { import {
MetaGroupWrapperActions, MetaGroupWrapperActions,
UserGroupsWrapperActions, UserGroupsWrapperActions,
@ -27,11 +33,7 @@ type GroupInviteDetails = {
} & WithEnvelopeTimestamp & } & WithEnvelopeTimestamp &
WithAuthor; WithAuthor;
type GroupMemberChangeDetails = { type GroupUpdateGeneric<T> = { change: T } & WithEnvelopeTimestamp & WithGroupPubkey & WithAuthor;
memberChangeDetails: SignalService.GroupUpdateMemberChangeMessage;
} & WithEnvelopeTimestamp &
WithGroupPubkey &
WithAuthor;
type GroupUpdateDetails = { type GroupUpdateDetails = {
updateMessage: SignalService.GroupUpdateMessage; updateMessage: SignalService.GroupUpdateMessage;
@ -43,22 +45,39 @@ async function handleGroupInviteMessage({
envelopeTimestamp, envelopeTimestamp,
}: GroupInviteDetails) { }: GroupInviteDetails) {
if (!PubKey.is03Pubkey(inviteMessage.groupSessionId)) { if (!PubKey.is03Pubkey(inviteMessage.groupSessionId)) {
// invite to a group which has not a 03 prefix, we can just drop it.
return; return;
} }
if (BlockedNumberController.isBlocked(author)) {
window.log.info(
`received invite to group ${ed25519Str(
inviteMessage.groupSessionId
)} by blocked user:${ed25519Str(author)}... dropping it`
);
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(inviteMessage.groupSessionId),
signature: inviteMessage.adminSignature,
data: stringToUint8Array(`INVITE${UserUtils.getOurPubKeyStrFromCache()}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group invite with invalid signature. dropping');
return;
}
window.log.debug( window.log.debug(
`received invite to group ${ed25519Str(inviteMessage.groupSessionId)} by user:${ed25519Str( `received invite to group ${ed25519Str(inviteMessage.groupSessionId)} by user:${ed25519Str(
author author
)}` )}`
); );
// TODO verify sig invite adminSignature
const convo = await ConvoHub.use().getOrCreateAndWait( const convo = await ConvoHub.use().getOrCreateAndWait(
inviteMessage.groupSessionId, inviteMessage.groupSessionId,
ConversationTypeEnum.GROUPV2 ConversationTypeEnum.GROUPV2
); );
convo.set({ convo.set({
active_at: envelopeTimestamp, active_at: envelopeTimestamp,
didApproveMe: true,
}); });
if (inviteMessage.name && isEmpty(convo.getRealSessionUsername())) { if (inviteMessage.name && isEmpty(convo.getRealSessionUsername())) {
@ -94,33 +113,107 @@ async function handleGroupInviteMessage({
).buffer, ).buffer,
}); });
await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache()); await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache());
await UserSync.queueNewJobIfNeeded();
// TODO use the pending so we actually don't start polling here unless it is not in the pending state. // TODO use the pending so we actually don't start polling here unless it is not in the pending state.
// once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages. // once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages.
getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId); getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId);
} }
async function handleGroupMemberChangeMessage({ async function verifySig({
memberChangeDetails, data,
pubKey,
signature,
}: {
data: Uint8Array;
signature: Uint8Array;
pubKey: Uint8Array;
}) {
const sodium = await getSodiumRenderer();
return sodium.crypto_sign_verify_detached(signature, data, pubKey);
}
async function handleGroupInfoChangeMessage({
change,
groupPk, groupPk,
envelopeTimestamp, envelopeTimestamp,
author, author,
}: GroupMemberChangeDetails) { }: GroupUpdateGeneric<SignalService.GroupUpdateInfoChangeMessage>) {
if (!PubKey.is03Pubkey(groupPk)) { const sigValid = await verifySig({
// invite to a group which has not a 03 prefix, we can just drop it. pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`INFO_CHANGE${change.type}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group info change with invalid signature. dropping');
return; return;
} }
// TODO verify sig invite adminSignature
const convo = ConvoHub.use().get(groupPk); const convo = ConvoHub.use().get(groupPk);
if (!convo) { if (!convo) {
return; return;
} }
switch (memberChangeDetails.type) { switch (change.type) {
case SignalService.GroupUpdateInfoChangeMessage.Type.NAME: {
await ClosedGroup.addUpdateMessage(
convo,
{ newName: change.updatedName },
author,
envelopeTimestamp
);
break;
}
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR: {
console.warn('Not implemented');
throw new Error('Not implemented');
}
case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES: {
if (
change.updatedExpiration &&
isNumber(change.updatedExpiration) &&
isFinite(change.updatedExpiration) &&
change.updatedExpiration >= 0
) {
await convo.updateExpireTimer(change.updatedExpiration, author, envelopeTimestamp);
}
break;
}
default:
return;
}
convo.set({
active_at: envelopeTimestamp,
});
}
async function handleGroupMemberChangeMessage({
change,
groupPk,
envelopeTimestamp,
author,
}: GroupUpdateGeneric<SignalService.GroupUpdateMemberChangeMessage>) {
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`MEMBER_CHANGE${change.type}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group member change with invalid signature. dropping');
return;
}
switch (change.type) {
case SignalService.GroupUpdateMemberChangeMessage.Type.ADDED: { case SignalService.GroupUpdateMemberChangeMessage.Type.ADDED: {
await ClosedGroup.addUpdateMessage( await ClosedGroup.addUpdateMessage(
convo, convo,
{ joiningMembers: memberChangeDetails.memberSessionIds }, { joiningMembers: change.memberSessionIds },
author, author,
envelopeTimestamp envelopeTimestamp
); );
@ -130,7 +223,7 @@ async function handleGroupMemberChangeMessage({
case SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED: { case SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED: {
await ClosedGroup.addUpdateMessage( await ClosedGroup.addUpdateMessage(
convo, convo,
{ kickedMembers: memberChangeDetails.memberSessionIds }, { kickedMembers: change.memberSessionIds },
author, author,
envelopeTimestamp envelopeTimestamp
); );
@ -139,7 +232,7 @@ async function handleGroupMemberChangeMessage({
case SignalService.GroupUpdateMemberChangeMessage.Type.PROMOTED: { case SignalService.GroupUpdateMemberChangeMessage.Type.PROMOTED: {
await ClosedGroup.addUpdateMessage( await ClosedGroup.addUpdateMessage(
convo, convo,
{ promotedMembers: memberChangeDetails.memberSessionIds }, { promotedMembers: change.memberSessionIds },
author, author,
envelopeTimestamp envelopeTimestamp
); );
@ -151,27 +244,187 @@ async function handleGroupMemberChangeMessage({
convo.set({ convo.set({
active_at: envelopeTimestamp, active_at: envelopeTimestamp,
didApproveMe: true,
}); });
} }
async function handleGroupUpdateMessage( async function handleGroupMemberLeftMessage({
groupPk,
envelopeTimestamp,
author,
}: GroupUpdateGeneric<SignalService.GroupUpdateMemberLeftMessage>) {
// No need to verify sig, the author is already verified with the libsession.decrypt()
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
await ClosedGroup.addUpdateMessage(
convo,
{ leavingMembers: [author] },
author,
envelopeTimestamp
);
convo.set({
active_at: envelopeTimestamp,
});
// TODO We should process this message type even if the sender is blocked
}
async function handleGroupDeleteMemberContentMessage({
groupPk,
envelopeTimestamp,
change,
}: GroupUpdateGeneric<SignalService.GroupUpdateDeleteMemberContentMessage>) {
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(
`DELETE_CONTENT${envelopeTimestamp}${change.memberSessionIds.join()}${change.messageHashes.join()}`
),
});
if (!sigValid) {
window.log.warn('received group member delete content with invalid signature. dropping');
return;
}
// TODO we should process this message type even if the sender is blocked
console.warn('Not implemented');
convo.set({
active_at: envelopeTimestamp,
});
throw new Error('Not implemented');
}
async function handleGroupUpdateDeleteMessage({
groupPk,
envelopeTimestamp,
change,
}: GroupUpdateGeneric<SignalService.GroupUpdateDeleteMessage>) {
// TODO verify sig?
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`DELETE${envelopeTimestamp}${change.memberSessionIds.join()}`),
});
if (!sigValid) {
window.log.warn('received group delete message with invalid signature. dropping');
return;
}
convo.set({
active_at: envelopeTimestamp,
});
console.warn('Not implemented');
throw new Error('Not implemented');
// TODO We should process this message type even if the sender is blocked
}
async function handleGroupUpdateInviteResponseMessage({
groupPk,
envelopeTimestamp,
}: GroupUpdateGeneric<SignalService.GroupUpdateInviteResponseMessage>) {
// no sig verify for this type of messages
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
convo.set({
active_at: envelopeTimestamp,
});
console.warn('Not implemented');
// TODO We should process this message type even if the sender is blocked
throw new Error('Not implemented');
}
async function handleGroupUpdatePromoteMessage({
change,
}: Omit<GroupUpdateGeneric<SignalService.GroupUpdatePromoteMessage>, 'groupPk'>) {
const seed = change.groupIdentitySeed;
const sodium = await getSodiumRenderer();
const groupKeypair = sodium.crypto_sign_seed_keypair(seed);
const groupPk = `03${HexString.toHexString(groupKeypair.publicKey)}` as GroupPubkeyType;
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
// no group update message here, another message is sent to the group's swarm for the update message.
// this message is just about the keys that we need to save, and accepting the promotion.
const found = await UserGroupsWrapperActions.getGroup(groupPk);
if (!found) {
// could have been removed by the user already so let's not force create it
window.log.info(
'received group promote message but that group is not in the usergroups wrapper'
);
return;
}
found.secretKey = groupKeypair.privateKey;
await UserGroupsWrapperActions.setGroup(found);
await UserSync.queueNewJobIfNeeded();
window.inboxStore.dispatch(
groupInfoActions.markUsAsAdmin({
groupPk,
})
);
// TODO we should process this even if the sender is blocked
}
async function handle1o1GroupUpdateMessage(
details: GroupUpdateDetails & WithUncheckedSource & WithUncheckedSenderIdentity details: GroupUpdateDetails & WithUncheckedSource & WithUncheckedSenderIdentity
) { ) {
if (details.updateMessage.inviteMessage) { // the message types below are received from our own swarm, so source is the sender, and senderIdentity is empty
// the invite message is received from our own swarm, so source is the sender, and senderIdentity is empty
const author = details.source; if (details.updateMessage.inviteMessage || details.updateMessage.promoteMessage) {
if (!PubKey.is05Pubkey(author)) { if (!PubKey.is05Pubkey(details.source)) {
window.log.warn('received group inviteMessage with invalid author'); window.log.warn('received group invite/promote with invalid author');
return; throw new PreConditionFailed('received group invite/promote with invalid author');
} }
if (details.updateMessage.inviteMessage) {
await handleGroupInviteMessage({ await handleGroupInviteMessage({
inviteMessage: details.updateMessage.inviteMessage as SignalService.GroupUpdateInviteMessage, inviteMessage: details.updateMessage
.inviteMessage as SignalService.GroupUpdateInviteMessage,
...details, ...details,
author, author: details.source,
}); });
} else if (details.updateMessage.promoteMessage) {
await handleGroupUpdatePromoteMessage({
change: details.updateMessage.promoteMessage as SignalService.GroupUpdatePromoteMessage,
...details,
author: details.source,
});
}
// returns true for all cases where this message was expected to be a 1o1 message, even if not processed
return true;
}
return false;
}
async function handleGroupUpdateMessage(
details: GroupUpdateDetails & WithUncheckedSource & WithUncheckedSenderIdentity
) {
const was1o1Message = await handle1o1GroupUpdateMessage(details);
if (was1o1Message) {
return; return;
} }
// other messages are received from the groups swarm, so source is the groupPk, and senderIdentity is the author // other messages are received from the groups swarm, so source is the groupPk, and senderIdentity is the author
const author = details.senderIdentity; const author = details.senderIdentity;
const groupPk = details.source; const groupPk = details.source;
@ -179,16 +432,57 @@ async function handleGroupUpdateMessage(
window.log.warn('received group update message with invalid author or groupPk'); window.log.warn('received group update message with invalid author or groupPk');
return; return;
} }
const detailsWithContext = { ...details, author, groupPk };
if (details.updateMessage.memberChangeMessage) { if (details.updateMessage.memberChangeMessage) {
await handleGroupMemberChangeMessage({ await handleGroupMemberChangeMessage({
memberChangeDetails: details.updateMessage change: details.updateMessage
.memberChangeMessage as SignalService.GroupUpdateMemberChangeMessage, .memberChangeMessage as SignalService.GroupUpdateMemberChangeMessage,
...details, ...detailsWithContext,
author, });
groupPk, return;
}
if (details.updateMessage.infoChangeMessage) {
await handleGroupInfoChangeMessage({
change: details.updateMessage.infoChangeMessage as SignalService.GroupUpdateInfoChangeMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.memberLeftMessage) {
await handleGroupMemberLeftMessage({
change: details.updateMessage.memberLeftMessage as SignalService.GroupUpdateMemberLeftMessage,
...detailsWithContext,
}); });
return; return;
} }
if (details.updateMessage.deleteMemberContent) {
await handleGroupDeleteMemberContentMessage({
change: details.updateMessage
.deleteMemberContent as SignalService.GroupUpdateDeleteMemberContentMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.deleteMessage) {
await handleGroupUpdateDeleteMessage({
change: details.updateMessage.deleteMessage as SignalService.GroupUpdateDeleteMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.inviteResponse) {
await handleGroupUpdateInviteResponseMessage({
change: details.updateMessage
.inviteResponse as SignalService.GroupUpdateInviteResponseMessage,
...detailsWithContext,
});
return;
}
window.log.warn('received group update of unknown type. Discarding...');
} }
export const GroupV2Receiver = { handleGroupUpdateMessage }; export const GroupV2Receiver = { handleGroupUpdateMessage };

@ -31,6 +31,11 @@ export enum SnodeNamespaces {
*/ */
UserGroups = 5, UserGroups = 5,
/**
* This is the namespace that revoked members can still poll messages from
*/
ClosedGroupRevokedRetrievableMessages = -11,
/** /**
* This is the namespace used to sync the closed group messages for each closed group * This is the namespace used to sync the closed group messages for each closed group
*/ */
@ -97,6 +102,7 @@ function isUserConfigNamespace(namespace: SnodeNamespaces): namespace is UserCon
case SnodeNamespaces.ClosedGroupMembers: case SnodeNamespaces.ClosedGroupMembers:
case SnodeNamespaces.ClosedGroupMessages: case SnodeNamespaces.ClosedGroupMessages:
case SnodeNamespaces.LegacyClosedGroup: case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
case SnodeNamespaces.Default: case SnodeNamespaces.Default:
// user messages is not hosting config based messages // user messages is not hosting config based messages
return false; return false;
@ -125,6 +131,7 @@ function isGroupConfigNamespace(
case SnodeNamespaces.ConvoInfoVolatile: case SnodeNamespaces.ConvoInfoVolatile:
case SnodeNamespaces.LegacyClosedGroup: case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.ClosedGroupMessages: case SnodeNamespaces.ClosedGroupMessages:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
return false; return false;
case SnodeNamespaces.ClosedGroupInfo: case SnodeNamespaces.ClosedGroupInfo:
case SnodeNamespaces.ClosedGroupKeys: case SnodeNamespaces.ClosedGroupKeys:
@ -153,6 +160,9 @@ function isGroupNamespace(namespace: SnodeNamespaces): namespace is SnodeNamespa
if (namespace === SnodeNamespaces.ClosedGroupMessages) { if (namespace === SnodeNamespaces.ClosedGroupMessages) {
return true; return true;
} }
if (namespace === SnodeNamespaces.ClosedGroupRevokedRetrievableMessages) {
return true;
}
switch (namespace) { switch (namespace) {
case SnodeNamespaces.Default: case SnodeNamespaces.Default:
case SnodeNamespaces.UserContacts: case SnodeNamespaces.UserContacts:
@ -185,6 +195,7 @@ function namespacePriority(namespace: SnodeNamespaces): 10 | 1 {
case SnodeNamespaces.ClosedGroupInfo: case SnodeNamespaces.ClosedGroupInfo:
case SnodeNamespaces.ClosedGroupMembers: case SnodeNamespaces.ClosedGroupMembers:
case SnodeNamespaces.ClosedGroupKeys: case SnodeNamespaces.ClosedGroupKeys:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
return 1; return 1;
default: default:

@ -1,11 +1,13 @@
import _, { isFinite, isNumber } from 'lodash'; import _, { isFinite, isNumber } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { PubkeyType } from 'libsession_util_nodejs';
import { getMessageQueue } from '..'; import { getMessageQueue } from '..';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { ConversationModel } from '../../models/conversation'; import { ConversationModel } from '../../models/conversation';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes'; import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageModel } from '../../models/message'; import { MessageModel } from '../../models/message';
import { MessageGroupUpdate } from '../../models/messageType';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { import {
addKeyPairToCacheAndDBIfNeeded, addKeyPairToCacheAndDBIfNeeded,
@ -37,10 +39,6 @@ export type GroupInfo = {
admins?: Array<string>; admins?: Array<string>;
}; };
export interface GroupDiff extends MemberChanges {
newName?: string;
}
export interface MemberChanges { export interface MemberChanges {
joiningMembers?: Array<string>; joiningMembers?: Array<string>;
leavingMembers?: Array<string>; leavingMembers?: Array<string>;
@ -48,6 +46,10 @@ export interface MemberChanges {
promotedMembers?: Array<string>; promotedMembers?: Array<string>;
} }
export interface GroupDiff extends MemberChanges {
newName?: string;
}
/** /**
* This function is only called when the local user makes a change to a group. * This function is only called when the local user makes a change to a group.
* So this function is not called on group updates from the network, even from another of our devices. * So this function is not called on group updates from the network, even from another of our devices.
@ -141,7 +143,7 @@ async function addUpdateMessage(
sender: string, sender: string,
sentAt: number sentAt: number
): Promise<MessageModel> { ): Promise<MessageModel> {
const groupUpdate: any = {}; const groupUpdate: MessageGroupUpdate = {};
if (diff.newName) { if (diff.newName) {
groupUpdate.name = diff.newName; groupUpdate.name = diff.newName;
@ -149,14 +151,14 @@ async function addUpdateMessage(
if (diff.joiningMembers) { if (diff.joiningMembers) {
groupUpdate.joined = diff.joiningMembers; groupUpdate.joined = diff.joiningMembers;
} } else if (diff.leavingMembers) {
if (diff.leavingMembers) {
groupUpdate.left = diff.leavingMembers; groupUpdate.left = diff.leavingMembers;
} } else if (diff.kickedMembers) {
if (diff.kickedMembers) {
groupUpdate.kicked = diff.kickedMembers; groupUpdate.kicked = diff.kickedMembers;
} else if (diff.promotedMembers) {
groupUpdate.promoted = diff.promotedMembers as Array<PubkeyType>;
} else {
throw new Error('addUpdateMessage with unknown type of change');
} }
if (UserUtils.isUsFromCache(sender)) { if (UserUtils.isUsFromCache(sender)) {

@ -1,8 +1,14 @@
import { GroupPubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../protobuf'; import { SignalService } from '../../../../../protobuf';
import { LibSodiumWrappers } from '../../../../crypto';
import { DataMessage } from '../../DataMessage'; import { DataMessage } from '../../DataMessage';
import { MessageParams } from '../../Message'; import { MessageParams } from '../../Message';
export type AdminSigDetails = {
secretKey: Uint8Array;
sodium: LibSodiumWrappers;
};
export interface GroupUpdateMessageParams extends MessageParams { export interface GroupUpdateMessageParams extends MessageParams {
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
} }

@ -1,18 +1,28 @@
import { isEmpty, isFinite } from 'lodash'; import { isEmpty, isFinite } from 'lodash';
import { SignalService } from '../../../../../../protobuf'; import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage'; import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String';
import { PreConditionFailed } from '../../../../../utils/errors';
import {
AdminSigDetails,
GroupUpdateMessage,
GroupUpdateMessageParams,
} from '../GroupUpdateMessage';
type NameChangeParams = GroupUpdateMessageParams & { type NameChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.NAME; typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.NAME;
updatedName: string; updatedName: string;
}; };
type AvatarChangeParams = GroupUpdateMessageParams & { type AvatarChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR; typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR;
}; };
type DisappearingMessageChangeParams = GroupUpdateMessageParams & { type DisappearingMessageChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES; typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES;
updatedExpirationSeconds: number; updatedExpirationSeconds: number;
}; };
@ -25,12 +35,16 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
public readonly updatedName: string = ''; public readonly updatedName: string = '';
public readonly updatedExpirationSeconds: number = 0; public readonly updatedExpirationSeconds: number = 0;
public readonly namespace = SnodeNamespaces.ClosedGroupMessages; public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
private readonly secretKey: Uint8Array; // not sent, only used for signing content as part of the message
private readonly sodium: LibSodiumWrappers;
constructor(params: NameChangeParams | AvatarChangeParams | DisappearingMessageChangeParams) { constructor(params: NameChangeParams | AvatarChangeParams | DisappearingMessageChangeParams) {
super(params); super(params);
const types = SignalService.GroupUpdateInfoChangeMessage.Type; const types = SignalService.GroupUpdateInfoChangeMessage.Type;
this.typeOfChange = params.typeOfChange; this.typeOfChange = params.typeOfChange;
this.secretKey = params.secretKey;
this.sodium = params.sodium;
switch (params.typeOfChange) { switch (params.typeOfChange) {
case types.NAME: { case types.NAME: {
@ -42,6 +56,7 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
} }
case types.AVATAR: case types.AVATAR:
// nothing to do for avatar // nothing to do for avatar
throw new PreConditionFailed('not implemented');
break; break;
case types.DISAPPEARING_MESSAGES: { case types.DISAPPEARING_MESSAGES: {
if (!isFinite(params.updatedExpirationSeconds) || params.updatedExpirationSeconds < 0) { if (!isFinite(params.updatedExpirationSeconds) || params.updatedExpirationSeconds < 0) {
@ -58,16 +73,26 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
public dataProto(): SignalService.DataMessage { public dataProto(): SignalService.DataMessage {
const infoChangeMessage = new SignalService.GroupUpdateInfoChangeMessage({ const infoChangeMessage = new SignalService.GroupUpdateInfoChangeMessage({
type: this.typeOfChange, type: this.typeOfChange,
adminSignature: this.sodium.crypto_sign_detached(
stringToUint8Array(`INFO_CHANGE${this.typeOfChange}${this.timestamp}`),
this.secretKey
),
}); });
switch (this.typeOfChange) {
if (this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.NAME) { case SignalService.GroupUpdateInfoChangeMessage.Type.NAME:
infoChangeMessage.updatedName = this.updatedName; infoChangeMessage.updatedName = this.updatedName;
}
if ( break;
this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES:
) {
infoChangeMessage.updatedExpiration = this.updatedExpirationSeconds; infoChangeMessage.updatedExpiration = this.updatedExpirationSeconds;
break;
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR:
default:
break;
} }
return new SignalService.DataMessage({ groupUpdateMessage: { infoChangeMessage } }); return new SignalService.DataMessage({ groupUpdateMessage: { infoChangeMessage } });
} }

@ -3,7 +3,13 @@ import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf'; import { SignalService } from '../../../../../../protobuf';
import { assertUnreachable } from '../../../../../../types/sqlSharedTypes'; import { assertUnreachable } from '../../../../../../types/sqlSharedTypes';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage'; import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String';
import {
AdminSigDetails,
GroupUpdateMessage,
GroupUpdateMessageParams,
} from '../GroupUpdateMessage';
type MembersAddedMessageParams = GroupUpdateMessageParams & { type MembersAddedMessageParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED; typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED;
@ -27,15 +33,24 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage {
public readonly typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type; public readonly typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type;
public readonly memberSessionIds: Array<PubkeyType> = []; // added, removed, promoted based on the type. public readonly memberSessionIds: Array<PubkeyType> = []; // added, removed, promoted based on the type.
public readonly namespace = SnodeNamespaces.ClosedGroupMessages; public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
private readonly secretKey: Uint8Array; // not sent, only used for signing content as part of the message
private readonly sodium: LibSodiumWrappers;
constructor( constructor(
params: MembersAddedMessageParams | MembersRemovedMessageParams | MembersPromotedMessageParams params: (
| MembersAddedMessageParams
| MembersRemovedMessageParams
| MembersPromotedMessageParams
) &
AdminSigDetails
) { ) {
super(params); super(params);
const { Type } = SignalService.GroupUpdateMemberChangeMessage; const { Type } = SignalService.GroupUpdateMemberChangeMessage;
const { typeOfChange } = params; const { typeOfChange } = params;
this.typeOfChange = typeOfChange; this.typeOfChange = typeOfChange;
this.secretKey = params.secretKey;
this.sodium = params.sodium;
switch (typeOfChange) { switch (typeOfChange) {
case Type.ADDED: { case Type.ADDED: {
@ -68,6 +83,10 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage {
const memberChangeMessage = new SignalService.GroupUpdateMemberChangeMessage({ const memberChangeMessage = new SignalService.GroupUpdateMemberChangeMessage({
type: this.typeOfChange, type: this.typeOfChange,
memberSessionIds: this.memberSessionIds, memberSessionIds: this.memberSessionIds,
adminSignature: this.sodium.crypto_sign_detached(
stringToUint8Array(`MEMBER_CHANGE${this.typeOfChange}${this.timestamp}`),
this.secretKey
),
}); });
return new SignalService.DataMessage({ groupUpdateMessage: { memberChangeMessage } }); return new SignalService.DataMessage({ groupUpdateMessage: { memberChangeMessage } });

@ -7,7 +7,7 @@ interface Params extends GroupUpdateMessageParams {
} }
/** /**
* GroupUpdateDeleteMessage is sent as a 1o1 message to the recipient, not through the group's swarm. * GroupUpdateDeleteMessage is sent to the group's swarm on the `revokedRetrievableGroupMessages`
*/ */
export class GroupUpdateDeleteMessage extends GroupUpdateMessage { export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public readonly adminSignature: Params['adminSignature']; public readonly adminSignature: Params['adminSignature'];
@ -27,9 +27,9 @@ export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public dataProto(): SignalService.DataMessage { public dataProto(): SignalService.DataMessage {
const deleteMessage = new SignalService.GroupUpdateDeleteMessage({ const deleteMessage = new SignalService.GroupUpdateDeleteMessage({
groupSessionId: this.destination,
adminSignature: this.adminSignature, adminSignature: this.adminSignature,
}); });
throw new Error('Not implemented');
return new SignalService.DataMessage({ groupUpdateMessage: { deleteMessage } }); return new SignalService.DataMessage({ groupUpdateMessage: { deleteMessage } });
} }

@ -61,7 +61,9 @@ function overwriteOutgoingTimestampWithNetworkTimestamp(message: { plainTextBuff
if ( if (
dataMessage.syncTarget || dataMessage.syncTarget ||
dataMessage.groupUpdateMessage?.inviteMessage || dataMessage.groupUpdateMessage?.inviteMessage ||
dataMessage.groupUpdateMessage?.infoChangeMessage ||
dataMessage.groupUpdateMessage?.deleteMemberContent || dataMessage.groupUpdateMessage?.deleteMemberContent ||
dataMessage.groupUpdateMessage?.memberChangeMessage ||
dataMessage.groupUpdateMessage?.deleteMessage dataMessage.groupUpdateMessage?.deleteMessage
) { ) {
return { return {

@ -21,6 +21,7 @@ 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';
import { getSodiumRenderer } from '../../session/crypto';
import { ClosedGroup } from '../../session/group/closed-group'; import { ClosedGroup } from '../../session/group/closed-group';
import { GroupUpdateInfoChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage'; import { GroupUpdateInfoChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage'; import { GroupUpdateMemberChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
@ -32,6 +33,7 @@ import { PreConditionFailed } from '../../session/utils/errors';
import { RunJobResult } from '../../session/utils/job_runners/PersistedJob'; import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { GroupInvite } from '../../session/utils/job_runners/jobs/GroupInviteJob'; import { GroupInvite } from '../../session/utils/job_runners/jobs/GroupInviteJob';
import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob'; import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { import {
getGroupPubkeyFromWrapperType, getGroupPubkeyFromWrapperType,
@ -129,7 +131,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const member = uniqMembers[index]; const member = uniqMembers[index];
const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member); const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member);
if (created.pubkeyHex === us) { if (created.pubkeyHex === us) {
await MetaGroupWrapperActions.memberSetPromoted(groupPk, created.pubkeyHex, false); await MetaGroupWrapperActions.memberSetAdmin(groupPk, created.pubkeyHex);
} else { } else {
await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false); await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false);
} }
@ -474,11 +476,14 @@ async function handleRemoveMembers({
timestamp, timestamp,
adminSignature: from_base64(adminSignature.signature, base64_variants.ORIGINAL), adminSignature: from_base64(adminSignature.signature, base64_variants.ORIGINAL),
}); });
console.warn(
'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.Default, 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);
@ -581,6 +586,7 @@ async function handleMemberChangeFromUIOrNot({
const member = withHistory[index]; const member = withHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member }); await GroupInvite.addGroupInviteJob({ groupPk, member });
} }
const sodium = await getSodiumRenderer();
const allAdded = [...withHistory, ...withoutHistory]; // those are already enforced to be unique (and without intersection) in `validateMemberChange()` const allAdded = [...withHistory, ...withoutHistory]; // those are already enforced to be unique (and without intersection) in `validateMemberChange()`
const timestamp = Date.now(); const timestamp = Date.now();
@ -598,6 +604,8 @@ async function handleMemberChangeFromUIOrNot({
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED, typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED,
identifier: msg.id, identifier: msg.id,
timestamp, timestamp,
secretKey: group.secretKey,
sodium,
}), }),
}); });
} }
@ -614,7 +622,9 @@ async function handleMemberChangeFromUIOrNot({
groupPk, groupPk,
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED, typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED,
identifier: msg.id, identifier: msg.id,
timestamp: Date.now(), timestamp: GetNetworkTime.getNowWithNetworkOffset(),
secretKey: group.secretKey,
sodium,
}), }),
}); });
} }
@ -637,6 +647,10 @@ async function handleNameChangeFromUIOrNot({
if (!group || !group.secretKey || isEmpty(group.secretKey)) { if (!group || !group.secretKey || isEmpty(group.secretKey)) {
throw new Error('tried to make change to group but we do not have the admin secret key'); throw new Error('tried to make change to group but we do not have the admin secret key');
} }
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
if (!infos) {
throw new PreConditionFailed('nameChange infoGet is empty');
}
// this throws if the name is the same, or empty // this throws if the name is the same, or empty
const { newName, convo, us } = validateNameChange({ const { newName, convo, us } = validateNameChange({
@ -646,8 +660,10 @@ async function handleNameChangeFromUIOrNot({
}); });
group.name = newName; group.name = newName;
infos.name = newName;
await UserGroupsWrapperActions.setGroup(group); await UserGroupsWrapperActions.setGroup(group);
console.warn('after set and refetcj', await UserGroupsWrapperActions.getGroup(group.pubkeyHex)); await MetaGroupWrapperActions.infoSet(groupPk, infos);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk, []); const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk, []);
if (batchResult !== RunJobResult.Success) { if (batchResult !== RunJobResult.Success) {
throw new Error( throw new Error(
@ -655,6 +671,8 @@ async function handleNameChangeFromUIOrNot({
); );
} }
await UserSync.queueNewJobIfNeeded();
const timestamp = Date.now(); const timestamp = Date.now();
if (fromCurrentDevice) { if (fromCurrentDevice) {
@ -666,6 +684,8 @@ async function handleNameChangeFromUIOrNot({
updatedName: newName, updatedName: newName,
identifier: msg.id, identifier: msg.id,
timestamp: Date.now(), timestamp: Date.now(),
secretKey: group.secretKey,
sodium: await getSodiumRenderer(),
}), }),
}); });
} }
@ -712,6 +732,41 @@ const currentDeviceGroupMembersChange = createAsyncThunk(
} }
); );
const markUsAsAdmin = createAsyncThunk(
'group/markUsAsAdmin',
async (
{
groupPk,
}: {
groupPk: GroupPubkeyType;
},
payloadCreator
): Promise<GroupDetailsUpdate> => {
const state = payloadCreator.getState() as StateType;
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('markUsAsAdmin group not present in redux slice');
}
const us = UserUtils.getOurPubKeyStrFromCache();
if (state.groups.members[groupPk].find(m => m.pubkeyHex === us)?.admin) {
// we are already an admin, nothing to do
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
await MetaGroupWrapperActions.memberSetAdmin(groupPk, us);
await GroupSync.queueNewJobIfNeeded(groupPk);
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
);
const currentDeviceGroupNameChange = createAsyncThunk( const currentDeviceGroupNameChange = createAsyncThunk(
'group/currentDeviceGroupNameChange', 'group/currentDeviceGroupNameChange',
async ( async (
@ -863,6 +918,17 @@ const groupSlice = createSlice({
builder.addCase(currentDeviceGroupNameChange.pending, state => { builder.addCase(currentDeviceGroupNameChange.pending, state => {
state.nameChangesFromUIPending = true; state.nameChangesFromUIPending = true;
}); });
builder.addCase(markUsAsAdmin.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
window.log.debug(`groupInfo after markUsAsAdmin: ${stringify(infos)}`);
window.log.debug(`groupMembers after markUsAsAdmin: ${stringify(members)}`);
});
builder.addCase(markUsAsAdmin.rejected, (_state, action) => {
window.log.error('a markUsAsAdmin was rejected', action.error);
});
}, },
}); });
@ -873,6 +939,7 @@ export const groupInfoActions = {
refreshGroupDetailsFromWrapper, refreshGroupDetailsFromWrapper,
handleUserGroupUpdate, handleUserGroupUpdate,
currentDeviceGroupMembersChange, currentDeviceGroupMembersChange,
markUsAsAdmin,
currentDeviceGroupNameChange, currentDeviceGroupNameChange,
...groupSlice.actions, ...groupSlice.actions,
}; };

@ -28,6 +28,7 @@ function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet {
promoted: false, promoted: false,
promotionFailed: false, promotionFailed: false,
promotionPending: false, promotionPending: false,
admin: false,
pubkeyHex, pubkeyHex,
}; };
} }

@ -438,6 +438,10 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
pubkeyHex, pubkeyHex,
failed, failed,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetPromoted']>>, ]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetPromoted']>>,
memberSetAdmin: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberSetAdmin', pubkeyHex]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberSetAdmin']>
>,
memberSetInvited: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, failed: boolean) => memberSetInvited: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, failed: boolean) =>
callLibSessionWorker([ callLibSessionWorker([
`MetaGroupConfig-${groupPk}`, `MetaGroupConfig-${groupPk}`,

Loading…
Cancel
Save