fix: add avatar change message handling

still needs to be able to send one, but that's chunk3
pull/2963/head
Audric Ackermann 2 years ago
parent 528d15bf2d
commit 77a62e82e7

@ -143,6 +143,14 @@ const ChangeItemPromoted = (promoted: Array<PubkeyType>): string => {
throw new PreConditionFailed('ChangeItemPromoted only applies to groupv2'); throw new PreConditionFailed('ChangeItemPromoted only applies to groupv2');
}; };
const ChangeItemAvatar = (): string => {
const isGroupV2 = useSelectedIsGroupV2();
if (isGroupV2) {
return window.i18n('groupAvatarChange');
}
throw new PreConditionFailed('ChangeItemAvatar only applies to groupv2');
};
const ChangeItemLeft = (left: Array<PubkeyType>): string => { const ChangeItemLeft = (left: Array<PubkeyType>): string => {
if (!left.length) { if (!left.length) {
throw new Error('Group update remove is missing contacts'); throw new Error('Group update remove is missing contacts');
@ -175,14 +183,14 @@ const ChangeItem = (change: PropsForGroupUpdateType): string => {
return ChangeItemName(change.newName); return ChangeItemName(change.newName);
case 'add': case 'add':
return ChangeItemJoined(change.added); return ChangeItemJoined(change.added);
case 'left': case 'left':
return ChangeItemLeft(change.left); return ChangeItemLeft(change.left);
case 'kicked': case 'kicked':
return ChangeItemKicked(change.kicked); return ChangeItemKicked(change.kicked);
case 'promoted': case 'promoted':
return ChangeItemPromoted(change.promoted); return ChangeItemPromoted(change.promoted);
case 'avatarChange':
return ChangeItemAvatar();
default: default:
assertUnreachable(type, `ChangeItem: Missing case error "${type}"`); assertUnreachable(type, `ChangeItem: Missing case error "${type}"`);
return ''; return '';

@ -2459,7 +2459,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const pubkey = new PubKey(recipientId); const pubkey = new PubKey(recipientId);
void getMessageQueue() void getMessageQueue()
.sendToPubKeyNonDurably({ .sendTo1o1NonDurably({
pubkey, pubkey,
message: typingMessage, message: typingMessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,

@ -77,6 +77,7 @@ import {
PropsForGroupInvitation, PropsForGroupInvitation,
PropsForGroupUpdate, PropsForGroupUpdate,
PropsForGroupUpdateAdd, PropsForGroupUpdateAdd,
PropsForGroupUpdateAvatarChange,
PropsForGroupUpdateKicked, PropsForGroupUpdateKicked,
PropsForGroupUpdateLeft, PropsForGroupUpdateLeft,
PropsForGroupUpdateName, PropsForGroupUpdateName,
@ -488,6 +489,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}; };
return { change, ...sharedProps }; return { change, ...sharedProps };
} }
if (groupUpdate.avatarChange) {
const change: PropsForGroupUpdateAvatarChange = {
type: 'avatarChange',
};
return { change, ...sharedProps };
}
return null; return null;
} }

@ -159,6 +159,7 @@ export type MessageGroupUpdate = {
kicked?: Array<string>; kicked?: Array<string>;
promoted?: Array<PubkeyType>; promoted?: Array<PubkeyType>;
name?: string; name?: string;
avatarChange?: boolean;
}; };
export interface MessageAttributesOptionals { export interface MessageAttributesOptionals {

@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { ContactInfo, UserGroupsGet } from 'libsession_util_nodejs'; import { ContactInfo, GroupPubkeyType, UserGroupsGet } from 'libsession_util_nodejs';
import { base64_variants, from_base64 } from 'libsodium-wrappers-sumo'; import { base64_variants, from_base64 } from 'libsodium-wrappers-sumo';
import { compact, difference, isEmpty, isNil, isNumber, toNumber } from 'lodash'; import { compact, difference, isEmpty, isNil, isNumber, toNumber } from 'lodash';
import { ConfigDumpData } from '../data/configDump/configDump'; import { ConfigDumpData } from '../data/configDump/configDump';
@ -725,7 +725,7 @@ async function handleSingleGroupUpdate({
} }
} }
async function handleSingleGroupUpdateToLeave(toLeave: string) { async function handleSingleGroupUpdateToLeave(toLeave: GroupPubkeyType) {
// that group is not in the wrapper but in our local DB. it must be removed and cleaned // that group is not in the wrapper but in our local DB. it must be removed and cleaned
try { try {
window.log.debug( window.log.debug(
@ -747,12 +747,12 @@ async function handleSingleGroupUpdateToLeave(toLeave: string) {
async function handleGroupUpdate(latestEnvelopeTimestamp: number) { async function handleGroupUpdate(latestEnvelopeTimestamp: number) {
// first let's check which groups needs to be joined or left by doing a diff of what is in the wrapper and what is in the DB // first let's check which groups needs to be joined or left by doing a diff of what is in the wrapper and what is in the DB
const allGroupsInWrapper = await UserGroupsWrapperActions.getAllGroups(); const allGroupsInWrapper = await UserGroupsWrapperActions.getAllGroups();
const allGroupsInDb = ConvoHub.use() const allGoupsIdsInDb = ConvoHub.use()
.getConversations() .getConversations()
.filter(m => PubKey.is03Pubkey(m.id)); .map(m => m.id)
.filter(PubKey.is03Pubkey);
const allGoupsIdsInWrapper = allGroupsInWrapper.map(m => m.pubkeyHex); const allGoupsIdsInWrapper = allGroupsInWrapper.map(m => m.pubkeyHex);
const allGoupsIdsInDb = allGroupsInDb.map(m => m.id as string);
window.log.debug('allGoupsIdsInWrapper', stringify(allGoupsIdsInWrapper)); window.log.debug('allGoupsIdsInWrapper', stringify(allGoupsIdsInWrapper));
window.log.debug('allGoupsIdsInDb', stringify(allGoupsIdsInDb)); window.log.debug('allGoupsIdsInDb', stringify(allGoupsIdsInDb));

@ -202,7 +202,6 @@ async function handleGroupInfoChangeMessage({
await ClosedGroup.addUpdateMessage({ await ClosedGroup.addUpdateMessage({
convo, convo,
diff: { newName: change.updatedName }, diff: { newName: change.updatedName },
sender: author, sender: author,
sentAt: envelopeTimestamp, sentAt: envelopeTimestamp,
expireUpdate: null, expireUpdate: null,
@ -211,8 +210,14 @@ async function handleGroupInfoChangeMessage({
break; break;
} }
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR: { case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR: {
console.warn('Not implemented'); await ClosedGroup.addUpdateMessage({
throw new Error('Not implemented'); convo,
diff: { avatarChange: true },
sender: author,
sentAt: envelopeTimestamp,
expireUpdate: null,
});
break;
} }
case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES: { case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES: {
if ( if (
@ -221,6 +226,13 @@ async function handleGroupInfoChangeMessage({
isFinite(change.updatedExpiration) && isFinite(change.updatedExpiration) &&
change.updatedExpiration >= 0 change.updatedExpiration >= 0
) { ) {
await ClosedGroup.addUpdateMessage({
convo,
diff: { newName: change.updatedName },
sender: author,
sentAt: envelopeTimestamp,
expireUpdate: null,
});
await convo.updateExpireTimer({ await convo.updateExpireTimer({
providedExpireTimer: change.updatedExpiration, providedExpireTimer: change.updatedExpiration,
providedSource: author, providedSource: author,
@ -307,6 +319,14 @@ async function handleGroupMemberLeftMessage({
return; return;
} }
// this does nothing if we are not an admin
window.inboxStore.dispatch(
groupInfoActions.handleMemberLeftMessage({
groupPk,
memberLeft: author,
})
);
await ClosedGroup.addUpdateMessage({ await ClosedGroup.addUpdateMessage({
convo, convo,
diff: { leavingMembers: [author] }, diff: { leavingMembers: [author] },
@ -314,10 +334,11 @@ async function handleGroupMemberLeftMessage({
sentAt: envelopeTimestamp, sentAt: envelopeTimestamp,
expireUpdate: null, expireUpdate: null,
}); });
convo.set({ convo.set({
active_at: envelopeTimestamp, active_at: envelopeTimestamp,
}); });
// TODO We should process this message type even if the sender is blocked // debugger TODO We should process this message type even if the sender is blocked
} }
async function handleGroupDeleteMemberContentMessage({ async function handleGroupDeleteMemberContentMessage({

@ -440,6 +440,10 @@ export class SwarmPolling {
type: ConversationTypeEnum; type: ConversationTypeEnum;
pubkey: string; pubkey: string;
}) { }) {
const correctlyTypedPk = PubKey.is03Pubkey(pubkey) || PubKey.is05Pubkey(pubkey) ? pubkey : null;
if (!correctlyTypedPk) {
return false;
}
const allLegacyGroupsInWrapper = await UserGroupsWrapperActions.getAllLegacyGroups(); const allLegacyGroupsInWrapper = await UserGroupsWrapperActions.getAllLegacyGroups();
const allGroupsInWrapper = await UserGroupsWrapperActions.getAllGroups(); const allGroupsInWrapper = await UserGroupsWrapperActions.getAllGroups();
@ -447,15 +451,16 @@ export class SwarmPolling {
// this can happen when a group is removed from the wrapper while we were polling // this can happen when a group is removed from the wrapper while we were polling
const newGroupButNotInWrapper = const newGroupButNotInWrapper =
PubKey.is03Pubkey(pubkey) && !allGroupsInWrapper.some(m => m.pubkeyHex === pubkey); PubKey.is03Pubkey(correctlyTypedPk) &&
!allGroupsInWrapper.some(m => m.pubkeyHex === correctlyTypedPk);
const legacyGroupButNoInWrapper = const legacyGroupButNoInWrapper =
type === ConversationTypeEnum.GROUP && type === ConversationTypeEnum.GROUP &&
pubkey.startsWith('05') && PubKey.is05Pubkey(correctlyTypedPk) &&
!allLegacyGroupsInWrapper.some(m => m.pubkeyHex === pubkey); !allLegacyGroupsInWrapper.some(m => m.pubkeyHex === pubkey);
if (newGroupButNotInWrapper || legacyGroupButNoInWrapper) { if (newGroupButNotInWrapper || legacyGroupButNoInWrapper) {
// not tracked anymore in the wrapper. Discard messages and stop polling // not tracked anymore in the wrapper. Discard messages and stop polling
await this.notPollingForGroupAsNotInWrapper(pubkey, 'not in wrapper after poll'); await this.notPollingForGroupAsNotInWrapper(correctlyTypedPk, 'not in wrapper after poll');
return true; return true;
} }
return false; return false;
@ -574,6 +579,9 @@ export class SwarmPolling {
} }
private async notPollingForGroupAsNotInWrapper(pubkey: string, reason: string) { private async notPollingForGroupAsNotInWrapper(pubkey: string, reason: string) {
if (!PubKey.is03Pubkey(pubkey) && !PubKey.is05Pubkey(pubkey)) {
return;
}
window.log.debug( window.log.debug(
`notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"` `notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"`
); );
@ -581,7 +589,6 @@ export class SwarmPolling {
fromSyncMessage: true, fromSyncMessage: true,
sendLeaveMessage: false, sendLeaveMessage: false,
}); });
return Promise.resolve();
} }
private loadGroupIds() { private loadGroupIds() {

@ -1,6 +1,6 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
import { ConvoVolatileType, GroupPubkeyType } from 'libsession_util_nodejs'; import { ConvoVolatileType, GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { isEmpty, isNil } from 'lodash'; import { isEmpty, isNil } from 'lodash';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
@ -27,6 +27,7 @@ import { getSwarmPollingInstance } from '../apis/snode_api';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage';
import { ed25519Str } from '../onions/onionPath'; import { ed25519Str } from '../onions/onionPath';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { UserSync } from '../utils/job_runners/jobs/UserSyncJob'; import { UserSync } from '../utils/job_runners/jobs/UserSyncJob';
@ -204,16 +205,21 @@ class ConvoController {
public async deleteClosedGroup( public async deleteClosedGroup(
groupId: string, groupId: string,
options: { fromSyncMessage: boolean; sendLeaveMessage: boolean } { fromSyncMessage, sendLeaveMessage }: { fromSyncMessage: boolean; sendLeaveMessage: boolean }
) { ) {
const conversation = await this.deleteConvoInitialChecks(groupId, 'LegacyGroup'); if (!PubKey.is03Pubkey(groupId) && !PubKey.is05Pubkey(groupId)) {
return;
}
const typeOfDelete: ConvoVolatileType = PubKey.is03Pubkey(groupId) ? 'Group' : 'LegacyGroup';
const conversation = await this.deleteConvoInitialChecks(groupId, typeOfDelete);
if (!conversation || !conversation.isClosedGroup()) { if (!conversation || !conversation.isClosedGroup()) {
return; return;
} }
window.log.info(`deleteClosedGroup: ${groupId}, sendLeaveMessage?:${options.sendLeaveMessage}`); window.log.info(`deleteClosedGroup: ${groupId}, sendLeaveMessage?:${sendLeaveMessage}`);
getSwarmPollingInstance().removePubkey(groupId, 'deleteClosedGroup'); // we don't need to keep polling anymore. getSwarmPollingInstance().removePubkey(groupId, 'deleteClosedGroup'); // we don't need to keep polling anymore.
if (options.sendLeaveMessage) { // send the leave message before we delete everything for this group (including the key!)
await leaveClosedGroup(groupId, options.fromSyncMessage); if (sendLeaveMessage) {
await leaveClosedGroup(groupId, fromSyncMessage);
} }
// if we were kicked or sent our left message, we have nothing to do more with that group. // if we were kicked or sent our left message, we have nothing to do more with that group.
@ -225,7 +231,7 @@ class ConvoController {
await removeLegacyGroupFromWrappers(groupId); await removeLegacyGroupFromWrappers(groupId);
} }
if (!options.fromSyncMessage) { if (!fromSyncMessage) {
await UserSync.queueNewJobIfNeeded(); await UserSync.queueNewJobIfNeeded();
} }
} }
@ -390,23 +396,23 @@ class ConvoController {
throw new Error(`ConvoHub.${deleteType} needs complete initial fetch`); throw new Error(`ConvoHub.${deleteType} needs complete initial fetch`);
} }
window.log.info(`${deleteType} with ${convoId}`); window.log.info(`${deleteType} with ${ed25519Str(convoId)}`);
const conversation = this.conversations.get(convoId); const conversation = this.conversations.get(convoId);
if (!conversation) { if (!conversation) {
window.log.warn(`${deleteType} no such convo ${convoId}`); window.log.warn(`${deleteType} no such convo ${ed25519Str(convoId)}`);
return null; return null;
} }
// those are the stuff to do for all conversation types // those are the stuff to do for all conversation types
window.log.info(`${deleteType} destroyingMessages: ${convoId}`); window.log.info(`${deleteType} destroyingMessages: ${ed25519Str(convoId)}`);
await deleteAllMessagesByConvoIdNoConfirmation(convoId); await deleteAllMessagesByConvoIdNoConfirmation(convoId);
window.log.info(`${deleteType} messages destroyed: ${convoId}`); window.log.info(`${deleteType} messages destroyed: ${ed25519Str(convoId)}`);
return conversation; return conversation;
} }
private async removeGroupOrCommunityFromDBAndRedux(convoId: string) { private async removeGroupOrCommunityFromDBAndRedux(convoId: string) {
window.log.info(`cleanUpGroupConversation, removing convo from DB: ${convoId}`); window.log.info(`cleanUpGroupConversation, removing convo from DB: ${ed25519Str(convoId)}`);
// not a private conversation, so not a contact for the ContactWrapper // not a private conversation, so not a contact for the ContactWrapper
await Data.removeConversation(convoId); await Data.removeConversation(convoId);
@ -420,7 +426,7 @@ class ConvoController {
} }
} }
window.log.info(`cleanUpGroupConversation, convo removed from DB: ${convoId}`); window.log.info(`cleanUpGroupConversation, convo removed from DB: ${ed25519Str(convoId)}`);
const conversation = this.conversations.get(convoId); const conversation = this.conversations.get(convoId);
if (conversation) { if (conversation) {
@ -432,7 +438,7 @@ class ConvoController {
} }
window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId)); window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId));
window.log.info(`cleanUpGroupConversation, convo removed from store: ${convoId}`); window.log.info(`cleanUpGroupConversation, convo removed from store: ${ed25519Str(convoId)}`);
} }
} }
@ -442,8 +448,8 @@ class ConvoController {
* Note: `fromSyncMessage` is used to know if we need to send a leave group message to the group first. * Note: `fromSyncMessage` is used to know if we need to send a leave group message to the group first.
* So if the user made the action on this device, fromSyncMessage should be false, but if it happened from a linked device polled update, set this to true. * So if the user made the action on this device, fromSyncMessage should be false, but if it happened from a linked device polled update, set this to true.
*/ */
async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { async function leaveClosedGroup(groupPk: PubkeyType | GroupPubkeyType, fromSyncMessage: boolean) {
const convo = ConvoHub.use().get(groupId); const convo = ConvoHub.use().get(groupPk);
if (!convo || !convo.isClosedGroup()) { if (!convo || !convo.isClosedGroup()) {
window?.log?.error('Cannot leave non-existing group'); window?.log?.error('Cannot leave non-existing group');
@ -472,14 +478,49 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
await convo.updateGroupAdmins(admins, false); await convo.updateGroupAdmins(admins, false);
await convo.commit(); await convo.commit();
getSwarmPollingInstance().removePubkey(groupId, 'leaveClosedGroup'); getSwarmPollingInstance().removePubkey(groupPk, 'leaveClosedGroup');
if (fromSyncMessage) { if (fromSyncMessage) {
// no need to send our leave message as our other device should already have sent it. // no need to send our leave message as our other device should already have sent it.
return; return;
} }
const keypair = await Data.getLatestClosedGroupEncryptionKeyPair(groupId); if (PubKey.is03Pubkey(groupPk)) {
// Send the update to the 03 group
const ourLeavingMessage = new GroupUpdateMemberLeftMessage({
createAtNetworkTimestamp: GetNetworkTime.now(),
groupPk,
expirationType: null, // we keep that one **not** expiring
expireTimer: null,
});
window?.log?.info(
`We are leaving the group ${ed25519Str(groupPk)}. Sending our leaving message.`
);
// We might not be able to send our leaving messages (no encryption keypair, we were already removed, no network, etc).
// If that happens, we should just remove everything from our current user.
try {
const wasSent = await getMessageQueue().sendToGroupV2NonDurably({
message: ourLeavingMessage,
});
if (!wasSent) {
throw new Error(
`Even with the retries, leaving message for group ${ed25519Str(
groupPk
)} failed to be sent... Still deleting everything`
);
}
} catch (e) {
window.log.warn('leaving groupv2 error:', e.message);
}
// the rest of the cleaning of that conversation is done in the `deleteClosedGroup()`
return;
}
// TODO remove legacy group support
const keypair = await Data.getLatestClosedGroupEncryptionKeyPair(groupPk);
if (!keypair || isEmpty(keypair) || isEmpty(keypair.publicHex) || isEmpty(keypair.privateHex)) { if (!keypair || isEmpty(keypair) || isEmpty(keypair.publicHex) || isEmpty(keypair.privateHex)) {
// if we do not have a keypair, we won't be able to send our leaving message neither, so just skip sending it. // if we do not have a keypair, we won't be able to send our leaving message neither, so just skip sending it.
// this can happen when getting a group from a broken libsession usergroup wrapper, but not only. // this can happen when getting a group from a broken libsession usergroup wrapper, but not only.
@ -489,31 +530,32 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
// Send the update to the group // Send the update to the group
const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ const ourLeavingMessage = new ClosedGroupMemberLeftMessage({
createAtNetworkTimestamp: GetNetworkTime.now(), createAtNetworkTimestamp: GetNetworkTime.now(),
groupId, groupId: groupPk,
expirationType: null, // we keep that one **not** expiring expirationType: null, // we keep that one **not** expiring
expireTimer: null, expireTimer: null,
}); });
window?.log?.info(`We are leaving the group ${groupId}. Sending our leaving message.`); window?.log?.info(`We are leaving the legacygroup ${groupPk}. Sending our leaving message.`);
// if we do not have a keypair for that group, we can't send our leave message, so just skip the message sending part // if we do not have a keypair for that group, we can't send our leave message, so just skip the message sending part
const wasSent = await getMessageQueue().sendToPubKeyNonDurably({ const wasSent = await getMessageQueue().sendToLegacyGroupNonDurably({
message: ourLeavingMessage, message: ourLeavingMessage,
namespace: SnodeNamespaces.LegacyClosedGroup, namespace: SnodeNamespaces.LegacyClosedGroup,
pubkey: PubKey.cast(groupId), destination: groupPk,
}); });
// TODO our leaving message might fail to be sent for some specific reason we want to still delete the group. // The leaving message might fail to be sent for some specific reason we want to still delete the group.
// for instance, if we do not have the encryption keypair anymore, we cannot send our left message, but we should still delete it's content // For instance, if we do not have the encryption keypair anymore, we cannot send our left message, but we should still delete its content
if (wasSent) { if (wasSent) {
window?.log?.info( window?.log?.info(
`Leaving message sent ${groupId}. Removing everything related to this group.` `Leaving message sent ${ed25519Str(groupPk)}. Removing everything related to this group.`
); );
} else { } else {
window?.log?.info( window?.log?.info(
`Leaving message failed to be sent for ${groupId}. But still removing everything related to this group....` `Leaving message failed to be sent for ${ed25519Str(
groupPk
)}. But still removing everything related to this group....`
); );
} }
// the rest of the cleaning of that conversation is done in the `deleteClosedGroup()`
} }
async function removeLegacyGroupFromWrappers(groupId: string) { async function removeLegacyGroupFromWrappers(groupId: string) {

@ -197,7 +197,7 @@ function createInvitePromises(
expireTimer: 0, expireTimer: 0,
}; };
const message = new ClosedGroupNewMessage(messageParams); const message = new ClosedGroupNewMessage(messageParams);
return getMessageQueue().sendToPubKeyNonDurably({ return getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(m), pubkey: PubKey.cast(m),
message, message,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,

@ -42,16 +42,14 @@ export type GroupInfo = {
admins?: Array<string>; admins?: Array<string>;
}; };
export interface MemberChanges { export type GroupDiff = {
joiningMembers?: Array<string>; joiningMembers?: Array<string>;
leavingMembers?: Array<string>; leavingMembers?: Array<string>;
kickedMembers?: Array<string>; kickedMembers?: Array<string>;
promotedMembers?: Array<string>; promotedMembers?: Array<string>;
}
export interface GroupDiff extends MemberChanges {
newName?: string; newName?: string;
} avatarChange?: boolean;
};
/** /**
* 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.
@ -181,6 +179,8 @@ export async function addUpdateMessage({
groupUpdate.kicked = diff.kickedMembers; groupUpdate.kicked = diff.kickedMembers;
} else if (diff.promotedMembers) { } else if (diff.promotedMembers) {
groupUpdate.promoted = diff.promotedMembers as Array<PubkeyType>; groupUpdate.promoted = diff.promotedMembers as Array<PubkeyType>;
} else if (diff.avatarChange) {
groupUpdate.avatarChange = true;
} else { } else {
throw new Error('addUpdateMessage with unknown type of change'); throw new Error('addUpdateMessage with unknown type of change');
} }

@ -76,7 +76,7 @@ export const sendDataExtractionNotification = async (
); );
try { try {
await getMessageQueue().sendToPubKeyNonDurably({ await getMessageQueue().sendTo1o1NonDurably({
pubkey, pubkey,
message: dataExtractionNotificationMessage, message: dataExtractionNotificationMessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,

@ -3,7 +3,6 @@ import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { LibSodiumWrappers } from '../../../../../crypto'; import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String'; import { stringToUint8Array } from '../../../../../utils/String';
import { PreConditionFailed } from '../../../../../utils/errors';
import { import {
AdminSigDetails, AdminSigDetails,
GroupUpdateMessage, GroupUpdateMessage,
@ -56,7 +55,6 @@ 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) {
@ -89,6 +87,7 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
break; break;
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR: case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR:
default: default:
break; break;
} }

@ -1,5 +1,6 @@
import { PubkeyType } from 'libsession_util_nodejs'; import { PubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../../protobuf'; import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { Preconditions } from '../../../preconditions'; import { Preconditions } from '../../../preconditions';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage'; import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
@ -12,6 +13,7 @@ interface Params extends GroupUpdateMessageParams {
* GroupUpdateDeleteMessage is sent to the group's swarm on the `revokedRetrievableGroupMessages` namespace * GroupUpdateDeleteMessage is sent to the group's swarm on the `revokedRetrievableGroupMessages` namespace
*/ */
export class GroupUpdateDeleteMessage extends GroupUpdateMessage { export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public readonly namespace = SnodeNamespaces.ClosedGroupRevokedRetrievableMessages;
public readonly adminSignature: Params['adminSignature']; public readonly adminSignature: Params['adminSignature'];
public readonly memberSessionIds: Params['memberSessionIds']; public readonly memberSessionIds: Params['memberSessionIds'];

@ -12,7 +12,6 @@ import { ExpirationTimerUpdateMessage } from '../messages/outgoing/controlMessag
import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage'; import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage';
import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage'; import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage';
import { import {
ClosedGroupV2VisibleMessage, ClosedGroupV2VisibleMessage,
@ -21,6 +20,7 @@ import {
import { SyncMessageType } from '../utils/sync/syncUtils'; import { SyncMessageType } from '../utils/sync/syncUtils';
import { MessageSentHandler } from './MessageSentHandler'; import { MessageSentHandler } from './MessageSentHandler';
import { PubkeyType } from 'libsession_util_nodejs';
import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil'; import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil';
import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2'; import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2';
import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction'; import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction';
@ -33,6 +33,7 @@ import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { DataExtractionNotificationMessage } from '../messages/outgoing/controlMessage/DataExtractionNotificationMessage'; import { DataExtractionNotificationMessage } from '../messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { TypingMessage } from '../messages/outgoing/controlMessage/TypingMessage'; import { TypingMessage } from '../messages/outgoing/controlMessage/TypingMessage';
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage'; import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { GroupUpdateDeleteMemberContentMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage'; import { GroupUpdateDeleteMemberContentMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage';
import { GroupUpdateInfoChangeMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage'; import { GroupUpdateInfoChangeMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage'; import { GroupUpdateMemberChangeMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
@ -231,6 +232,48 @@ export class MessageQueue {
); );
} }
public async sendToGroupV2NonDurably({
message,
}: {
message:
| ClosedGroupV2VisibleMessage
| GroupUpdateMemberChangeMessage
| GroupUpdateInfoChangeMessage
| GroupUpdateDeleteMemberContentMessage
| GroupUpdateMemberLeftMessage
| GroupUpdateDeleteMessage;
}) {
if (!message.destination || !PubKey.is03Pubkey(message.destination)) {
throw new Error('Invalid group message passed in sendToGroupV2NonDurably.');
}
return this.sendToPubKeyNonDurably({
message,
namespace: message.namespace,
pubkey: PubKey.cast(message.destination),
});
}
public async sendToLegacyGroupNonDurably({
message,
namespace,
destination,
}: {
message: ClosedGroupMemberLeftMessage;
namespace: SnodeNamespaces.LegacyClosedGroup;
destination: PubkeyType;
}) {
if (!destination || !PubKey.is05Pubkey(destination)) {
throw new Error('Invalid legacygroup message passed in sendToLegacyGroupNonDurably.');
}
return this.sendToPubKeyNonDurably({
message,
namespace,
pubkey: PubKey.cast(destination),
});
}
public async sendSyncMessage({ public async sendSyncMessage({
namespace, namespace,
message, message,
@ -252,25 +295,40 @@ export class MessageQueue {
} }
/** /**
* Sends a message that awaits until the message is completed sending * Send a message to a 1o1 swarm
* @param user user pub key to send to * @param user user pub key to send to
* @param message Message to be sent * @param message Message to be sent
*/ */
public async sendToPubKeyNonDurably({ public async sendTo1o1NonDurably({
namespace, namespace,
message, message,
pubkey, pubkey,
}: { }: {
pubkey: PubKey; pubkey: PubKey;
message: message:
| ClosedGroupNewMessage
| TypingMessage // no point of caching the typing message, they are very short lived | TypingMessage // no point of caching the typing message, they are very short lived
| DataExtractionNotificationMessage | DataExtractionNotificationMessage
| CallMessage | CallMessage
| ClosedGroupMemberLeftMessage | ClosedGroupNewMessage
| GroupUpdateInviteMessage | GroupUpdateInviteMessage
| GroupUpdatePromoteMessage | GroupUpdatePromoteMessage;
| GroupUpdateDeleteMessage; namespace: SnodeNamespaces.Default;
}): Promise<number | null> {
return this.sendToPubKeyNonDurably({ message, namespace, pubkey });
}
/**
* Sends a message that awaits until the message is completed sending
* @param user user pub key to send to
* @param message Message to be sent
*/
private async sendToPubKeyNonDurably({
namespace,
message,
pubkey,
}: {
pubkey: PubKey;
message: ContentMessage;
namespace: SnodeNamespaces; namespace: SnodeNamespaces;
}): Promise<number | null> { }): Promise<number | null> {
let rawMessage; let rawMessage;

@ -439,7 +439,7 @@ async function createOfferAndSendIt(recipient: string, msgIdentifier: string | n
}); });
window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`); window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`);
const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably({ const negotiationOfferSendResult = await getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(recipient), pubkey: PubKey.cast(recipient),
message: offerMessage, message: offerMessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
@ -535,7 +535,7 @@ export async function USER_callRecipient(recipient: string) {
// initiating a call is analogous to sending a message request // initiating a call is analogous to sending a message request
await approveConvoAndSendResponse(recipient); await approveConvoAndSendResponse(recipient);
// Note: we do the sending of the preoffer manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess // Note: we do the sending of the preoffer manually as the sendTo1o1NonDurably rely on having a message saved to the db for MessageSentSuccess
// which is not the case for a pre offer message (the message only exists in memory) // which is not the case for a pre offer message (the message only exists in memory)
const rawMessage = await MessageUtils.toRawMessage( const rawMessage = await MessageUtils.toRawMessage(
PubKey.cast(recipient), PubKey.cast(recipient),
@ -623,7 +623,7 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
`sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}` `sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}`
); );
await getMessageQueue().sendToPubKeyNonDurably({ await getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(recipient), pubkey: PubKey.cast(recipient),
message: callIceCandicates, message: callIceCandicates,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
@ -1004,12 +1004,12 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
await Promise.all([ await Promise.all([
getMessageQueue().sendToPubKeyNonDurably({ getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(user), pubkey: PubKey.cast(user),
message: callmessage, message: callmessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
}), }),
getMessageQueue().sendToPubKeyNonDurably({ getMessageQueue().sendTo1o1NonDurably({
pubkey: UserUtils.getOurPubKeyFromCache(), pubkey: UserUtils.getOurPubKeyFromCache(),
message: callmessage, message: callmessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
@ -1039,7 +1039,7 @@ export async function USER_hangup(fromSender: string) {
expirationType, expirationType,
expireTimer, expireTimer,
}); });
void getMessageQueue().sendToPubKeyNonDurably({ void getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(fromSender), pubkey: PubKey.cast(fromSender),
message: endCallMessage, message: endCallMessage,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,

@ -142,7 +142,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
groupPk, groupPk,
}); });
const storedAt = await getMessageQueue().sendToPubKeyNonDurably({ const storedAt = await getMessageQueue().sendTo1o1NonDurably({
message: inviteDetails, message: inviteDetails,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(member), pubkey: PubKey.cast(member),

@ -100,7 +100,7 @@ class GroupPromoteJob extends PersistedJob<GroupPromotePersistedData> {
groupPk, groupPk,
}); });
const storedAt = await getMessageQueue().sendToPubKeyNonDurably({ const storedAt = await getMessageQueue().sendTo1o1NonDurably({
message, message,
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(member), pubkey: PubKey.cast(member),

@ -278,11 +278,11 @@ async function removeLegacyGroupFromWrapper(convoId: string) {
} }
async function removeGroupFromWrapper(groupPk: GroupPubkeyType) { async function removeGroupFromWrapper(groupPk: GroupPubkeyType) {
// try { try {
// await ConvoInfoVolatileWrapperActions.eraseGroup(groupPk); await ConvoInfoVolatileWrapperActions.eraseGroup(groupPk);
// } catch (e) { } catch (e) {
// window.log.warn('removeGroupFromWrapper failed with ', e.message); window.log.warn('removeGroupFromWrapper failed with ', e.message);
// } }
window.log.warn('removeGroupFromWrapper TODO'); window.log.warn('removeGroupFromWrapper TODO');
mappedGroupWrapperValues.delete(groupPk); mappedGroupWrapperValues.delete(groupPk);
} }
@ -324,10 +324,10 @@ export const SessionUtilConvoInfoVolatile = {
removeContactFromWrapper, removeContactFromWrapper,
// legacy group // legacy group
removeLegacyGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed // TODOLATER removeLegacyGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed
// group // group
removeGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed // TODOLATER removeGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed
// communities // communities
removeCommunityFromWrapper, removeCommunityFromWrapper,

@ -117,6 +117,10 @@ export type PropsForGroupUpdatePromoted = {
promoted: Array<PubkeyType>; promoted: Array<PubkeyType>;
}; };
export type PropsForGroupUpdateAvatarChange = {
type: 'avatarChange';
};
export type PropsForGroupUpdateLeft = { export type PropsForGroupUpdateLeft = {
type: 'left'; type: 'left';
left: Array<PubkeyType>; left: Array<PubkeyType>;
@ -131,6 +135,7 @@ export type PropsForGroupUpdateType =
| PropsForGroupUpdateAdd | PropsForGroupUpdateAdd
| PropsForGroupUpdateKicked | PropsForGroupUpdateKicked
| PropsForGroupUpdatePromoted | PropsForGroupUpdatePromoted
| PropsForGroupUpdateAvatarChange
| PropsForGroupUpdateName | PropsForGroupUpdateName
| PropsForGroupUpdateLeft; | PropsForGroupUpdateLeft;

@ -19,7 +19,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';
@ -79,12 +78,18 @@ type GroupDetailsUpdate = {
members: Array<GroupMemberGet>; members: Array<GroupMemberGet>;
}; };
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) { async function checkWeAreAdmin(groupPk: GroupPubkeyType) {
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us); const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk); const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
if (isEmpty(inUserGroup?.secretKey) || !usInGroup?.promoted) { // if the secretKey is not empty AND we are a member of the group, we are a current admin
return Boolean(!isEmpty(inUserGroup?.secretKey) && usInGroup?.promoted);
}
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) {
const areWeAdmin = await checkWeAreAdmin(groupPk);
if (!areWeAdmin) {
throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`); throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`);
} }
} }
@ -532,10 +537,8 @@ async function handleRemoveMembersAndRekey({
memberSessionIds: sortedRemoved, memberSessionIds: sortedRemoved,
}); });
const result = await getMessageQueue().sendToPubKeyNonDurably({ const result = await getMessageQueue().sendToGroupV2NonDurably({
message: removedMemberMessage, message: removedMemberMessage,
pubkey: PubKey.cast(groupPk),
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
}); });
if (!result) { if (!result) {
throw new Error( throw new Error(
@ -577,7 +580,6 @@ async function getPendingRevokeParams({
revokeChanges.push({ action: 'revoke_subaccount', tokenToRevokeHex: token }); revokeChanges.push({ action: 'revoke_subaccount', tokenToRevokeHex: token });
} }
return SnodeAPIRevoke.getRevokeSubaccountParams(groupPk, { revokeChanges, unrevokeChanges }); return SnodeAPIRevoke.getRevokeSubaccountParams(groupPk, { revokeChanges, unrevokeChanges });
} }
@ -621,8 +623,12 @@ async function getUpdateMessagesToPush({
const updateMessages: Array<GroupUpdateMemberChangeMessage> = []; const updateMessages: Array<GroupUpdateMemberChangeMessage> = [];
if (!fromCurrentDevice) {
return updateMessages;
}
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()`
if (fromCurrentDevice && allAdded.length) { if (allAdded.length) {
updateMessages.push( updateMessages.push(
new GroupUpdateMemberChangeMessage({ new GroupUpdateMemberChangeMessage({
added: allAdded, added: allAdded,
@ -635,7 +641,7 @@ async function getUpdateMessagesToPush({
}) })
); );
} }
if (fromCurrentDevice && removed.length) { if (removed.length) {
updateMessages.push( updateMessages.push(
new GroupUpdateMemberChangeMessage({ new GroupUpdateMemberChangeMessage({
removed, removed,
@ -760,6 +766,11 @@ async function handleMemberAddedFromUIOrNot({
await convo.commit(); await convo.commit();
} }
/**
* This function is called in two cases:
* - to udpate the state when kicking a member from the group from the UI
* - to update the state when handling a MEMBER_LEFT message
*/
async function handleMemberRemovedFromUIOrNot({ async function handleMemberRemovedFromUIOrNot({
groupPk, groupPk,
removeMembers, removeMembers,
@ -781,7 +792,7 @@ async function handleMemberRemovedFromUIOrNot({
groupPk, groupPk,
removed: removeMembers, removed: removeMembers,
}); });
// first, get revoke requests that need to be pushed for removed members // first, get revoke requests that need to be pushed for leaving member
const revokeUnrevokeParams = await getPendingRevokeParams({ const revokeUnrevokeParams = await getPendingRevokeParams({
groupPk, groupPk,
withHistory: [], withHistory: [],
@ -789,7 +800,7 @@ async function handleMemberRemovedFromUIOrNot({
removed, removed,
}); });
// Send the groupUpdateDeleteMessage that can still be decrypted by those removed members to namespace ClosedGroupRevokedRetrievableMessages. // Send the groupUpdateDeleteMessage that can still be decrypted by those removed members to namespace ClosedGroupRevokedRetrievableMessages. (not when handling a MEMBER_LEFT message)
// Then, rekey the wrapper, but don't push the changes yet, we want to batch all of the requests to be made together in the `pushChangesToGroupSwarmIfNeeded` below. // Then, rekey the wrapper, but don't push the changes yet, we want to batch all of the requests to be made together in the `pushChangesToGroupSwarmIfNeeded` below.
await handleRemoveMembersAndRekey({ await handleRemoveMembersAndRekey({
groupPk, groupPk,
@ -990,6 +1001,45 @@ const currentDeviceGroupMembersChange = createAsyncThunk(
} }
); );
/**
* This action is used to trigger a change when the local user does a change to a group v2 members list.
* GroupV2 added members can be added two ways: with and without the history of messages.
* GroupV2 removed members have their subaccount token revoked on the server side so they cannot poll anymore from the group's swarm.
*/
const handleMemberLeftMessage = createAsyncThunk(
'group/handleMemberLeftMessage',
async (
{
groupPk,
memberLeft,
}: {
groupPk: GroupPubkeyType;
memberLeft: PubkeyType;
},
payloadCreator
): Promise<GroupDetailsUpdate> => {
const state = payloadCreator.getState() as StateType;
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed(
'currentDeviceGroupMembersChange group not present in redux slice'
);
}
await handleMemberRemovedFromUIOrNot({
groupPk,
removeMembers: [memberLeft],
fromCurrentDevice: true,
fromMemberLeftMessage: true,
});
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
);
const markUsAsAdmin = createAsyncThunk( const markUsAsAdmin = createAsyncThunk(
'group/markUsAsAdmin', 'group/markUsAsAdmin',
async ( async (
@ -1201,6 +1251,7 @@ const metaGroupSlice = createSlice({
state.memberChangesFromUIPending = true; state.memberChangesFromUIPending = true;
}); });
/** currentDeviceGroupNameChange */
builder.addCase(currentDeviceGroupNameChange.fulfilled, (state, action) => { builder.addCase(currentDeviceGroupNameChange.fulfilled, (state, action) => {
state.nameChangesFromUIPending = false; state.nameChangesFromUIPending = false;
@ -1218,6 +1269,21 @@ const metaGroupSlice = createSlice({
builder.addCase(currentDeviceGroupNameChange.pending, state => { builder.addCase(currentDeviceGroupNameChange.pending, state => {
state.nameChangesFromUIPending = true; state.nameChangesFromUIPending = true;
}); });
/** handleMemberLeftMessage */
builder.addCase(handleMemberLeftMessage.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
window.log.debug(`groupInfo after handleMemberLeftMessage: ${stringify(infos)}`);
window.log.debug(`groupMembers after handleMemberLeftMessage: ${stringify(members)}`);
});
builder.addCase(handleMemberLeftMessage.rejected, (_state, action) => {
window.log.error('a handleMemberLeftMessage was rejected', action.error);
});
/** markUsAsAdmin */
builder.addCase(markUsAsAdmin.fulfilled, (state, action) => { builder.addCase(markUsAsAdmin.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload; const { infos, members, groupPk } = action.payload;
state.infos[groupPk] = infos; state.infos[groupPk] = infos;
@ -1253,6 +1319,7 @@ export const groupInfoActions = {
currentDeviceGroupMembersChange, currentDeviceGroupMembersChange,
markUsAsAdmin, markUsAsAdmin,
inviteResponseReceived, inviteResponseReceived,
handleMemberLeftMessage,
currentDeviceGroupNameChange, currentDeviceGroupNameChange,
...metaGroupSlice.actions, ...metaGroupSlice.actions,
}; };

Loading…
Cancel
Save