fix: add avatar change message handling

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

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

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

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

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

@ -1,5 +1,5 @@
/* 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 { compact, difference, isEmpty, isNil, isNumber, toNumber } from 'lodash';
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
try {
window.log.debug(
@ -747,12 +747,12 @@ async function handleSingleGroupUpdateToLeave(toLeave: string) {
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
const allGroupsInWrapper = await UserGroupsWrapperActions.getAllGroups();
const allGroupsInDb = ConvoHub.use()
const allGoupsIdsInDb = ConvoHub.use()
.getConversations()
.filter(m => PubKey.is03Pubkey(m.id));
.map(m => m.id)
.filter(PubKey.is03Pubkey);
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('allGoupsIdsInDb', stringify(allGoupsIdsInDb));

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

@ -440,6 +440,10 @@ export class SwarmPolling {
type: ConversationTypeEnum;
pubkey: string;
}) {
const correctlyTypedPk = PubKey.is03Pubkey(pubkey) || PubKey.is05Pubkey(pubkey) ? pubkey : null;
if (!correctlyTypedPk) {
return false;
}
const allLegacyGroupsInWrapper = await UserGroupsWrapperActions.getAllLegacyGroups();
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
const newGroupButNotInWrapper =
PubKey.is03Pubkey(pubkey) && !allGroupsInWrapper.some(m => m.pubkeyHex === pubkey);
PubKey.is03Pubkey(correctlyTypedPk) &&
!allGroupsInWrapper.some(m => m.pubkeyHex === correctlyTypedPk);
const legacyGroupButNoInWrapper =
type === ConversationTypeEnum.GROUP &&
pubkey.startsWith('05') &&
PubKey.is05Pubkey(correctlyTypedPk) &&
!allLegacyGroupsInWrapper.some(m => m.pubkeyHex === pubkey);
if (newGroupButNotInWrapper || legacyGroupButNoInWrapper) {
// 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 false;
@ -574,6 +579,9 @@ export class SwarmPolling {
}
private async notPollingForGroupAsNotInWrapper(pubkey: string, reason: string) {
if (!PubKey.is03Pubkey(pubkey) && !PubKey.is05Pubkey(pubkey)) {
return;
}
window.log.debug(
`notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"`
);
@ -581,7 +589,6 @@ export class SwarmPolling {
fromSyncMessage: true,
sendLeaveMessage: false,
});
return Promise.resolve();
}
private loadGroupIds() {

@ -1,6 +1,6 @@
/* eslint-disable no-await-in-loop */
/* 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 { Data } from '../../data/data';
@ -27,6 +27,7 @@ import { getSwarmPollingInstance } from '../apis/snode_api';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
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 { UserUtils } from '../utils';
import { UserSync } from '../utils/job_runners/jobs/UserSyncJob';
@ -204,16 +205,21 @@ class ConvoController {
public async deleteClosedGroup(
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()) {
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.
if (options.sendLeaveMessage) {
await leaveClosedGroup(groupId, options.fromSyncMessage);
// send the leave message before we delete everything for this group (including the key!)
if (sendLeaveMessage) {
await leaveClosedGroup(groupId, fromSyncMessage);
}
// 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);
}
if (!options.fromSyncMessage) {
if (!fromSyncMessage) {
await UserSync.queueNewJobIfNeeded();
}
}
@ -390,23 +396,23 @@ class ConvoController {
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);
if (!conversation) {
window.log.warn(`${deleteType} no such convo ${convoId}`);
window.log.warn(`${deleteType} no such convo ${ed25519Str(convoId)}`);
return null;
}
// 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);
window.log.info(`${deleteType} messages destroyed: ${convoId}`);
window.log.info(`${deleteType} messages destroyed: ${ed25519Str(convoId)}`);
return conversation;
}
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
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);
if (conversation) {
@ -432,7 +438,7 @@ class ConvoController {
}
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.
* 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) {
const convo = ConvoHub.use().get(groupId);
async function leaveClosedGroup(groupPk: PubkeyType | GroupPubkeyType, fromSyncMessage: boolean) {
const convo = ConvoHub.use().get(groupPk);
if (!convo || !convo.isClosedGroup()) {
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.commit();
getSwarmPollingInstance().removePubkey(groupId, 'leaveClosedGroup');
getSwarmPollingInstance().removePubkey(groupPk, 'leaveClosedGroup');
if (fromSyncMessage) {
// no need to send our leave message as our other device should already have sent it.
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 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.
@ -489,31 +530,32 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
// Send the update to the group
const ourLeavingMessage = new ClosedGroupMemberLeftMessage({
createAtNetworkTimestamp: GetNetworkTime.now(),
groupId,
groupId: groupPk,
expirationType: null, // we keep that one **not** expiring
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
const wasSent = await getMessageQueue().sendToPubKeyNonDurably({
const wasSent = await getMessageQueue().sendToLegacyGroupNonDurably({
message: ourLeavingMessage,
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.
// 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
// 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 its content
if (wasSent) {
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 {
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) {

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

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

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

@ -3,7 +3,6 @@ import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String';
import { PreConditionFailed } from '../../../../../utils/errors';
import {
AdminSigDetails,
GroupUpdateMessage,
@ -56,7 +55,6 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
}
case types.AVATAR:
// nothing to do for avatar
throw new PreConditionFailed('not implemented');
break;
case types.DISAPPEARING_MESSAGES: {
if (!isFinite(params.updatedExpirationSeconds) || params.updatedExpirationSeconds < 0) {
@ -89,6 +87,7 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
break;
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR:
default:
break;
}

@ -1,5 +1,6 @@
import { PubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { Preconditions } from '../../../preconditions';
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
*/
export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public readonly namespace = SnodeNamespaces.ClosedGroupRevokedRetrievableMessages;
public readonly adminSignature: Params['adminSignature'];
public readonly memberSessionIds: Params['memberSessionIds'];

@ -12,7 +12,6 @@ import { ExpirationTimerUpdateMessage } from '../messages/outgoing/controlMessag
import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage';
import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage';
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 {
ClosedGroupV2VisibleMessage,
@ -21,6 +20,7 @@ import {
import { SyncMessageType } from '../utils/sync/syncUtils';
import { MessageSentHandler } from './MessageSentHandler';
import { PubkeyType } from 'libsession_util_nodejs';
import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil';
import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2';
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 { TypingMessage } from '../messages/outgoing/controlMessage/TypingMessage';
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 { GroupUpdateInfoChangeMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
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({
namespace,
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 message Message to be sent
*/
public async sendToPubKeyNonDurably({
public async sendTo1o1NonDurably({
namespace,
message,
pubkey,
}: {
pubkey: PubKey;
message:
| ClosedGroupNewMessage
| TypingMessage // no point of caching the typing message, they are very short lived
| DataExtractionNotificationMessage
| CallMessage
| ClosedGroupMemberLeftMessage
| ClosedGroupNewMessage
| GroupUpdateInviteMessage
| GroupUpdatePromoteMessage
| GroupUpdateDeleteMessage;
| GroupUpdatePromoteMessage;
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;
}): Promise<number | null> {
let rawMessage;

@ -439,7 +439,7 @@ async function createOfferAndSendIt(recipient: string, msgIdentifier: string | n
});
window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`);
const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably({
const negotiationOfferSendResult = await getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(recipient),
message: offerMessage,
namespace: SnodeNamespaces.Default,
@ -535,7 +535,7 @@ export async function USER_callRecipient(recipient: string) {
// initiating a call is analogous to sending a message request
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)
const rawMessage = await MessageUtils.toRawMessage(
PubKey.cast(recipient),
@ -623,7 +623,7 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
`sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}`
);
await getMessageQueue().sendToPubKeyNonDurably({
await getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(recipient),
message: callIceCandicates,
namespace: SnodeNamespaces.Default,
@ -1004,12 +1004,12 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
await Promise.all([
getMessageQueue().sendToPubKeyNonDurably({
getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(user),
message: callmessage,
namespace: SnodeNamespaces.Default,
}),
getMessageQueue().sendToPubKeyNonDurably({
getMessageQueue().sendTo1o1NonDurably({
pubkey: UserUtils.getOurPubKeyFromCache(),
message: callmessage,
namespace: SnodeNamespaces.Default,
@ -1039,7 +1039,7 @@ export async function USER_hangup(fromSender: string) {
expirationType,
expireTimer,
});
void getMessageQueue().sendToPubKeyNonDurably({
void getMessageQueue().sendTo1o1NonDurably({
pubkey: PubKey.cast(fromSender),
message: endCallMessage,
namespace: SnodeNamespaces.Default,

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

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

@ -278,11 +278,11 @@ async function removeLegacyGroupFromWrapper(convoId: string) {
}
async function removeGroupFromWrapper(groupPk: GroupPubkeyType) {
// try {
// await ConvoInfoVolatileWrapperActions.eraseGroup(groupPk);
// } catch (e) {
// window.log.warn('removeGroupFromWrapper failed with ', e.message);
// }
try {
await ConvoInfoVolatileWrapperActions.eraseGroup(groupPk);
} catch (e) {
window.log.warn('removeGroupFromWrapper failed with ', e.message);
}
window.log.warn('removeGroupFromWrapper TODO');
mappedGroupWrapperValues.delete(groupPk);
}
@ -324,10 +324,10 @@ export const SessionUtilConvoInfoVolatile = {
removeContactFromWrapper,
// 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
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
removeCommunityFromWrapper,

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

@ -19,7 +19,6 @@ import { SignalService } from '../../protobuf';
import { getMessageQueue } from '../../session';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
import { RevokeChanges, SnodeAPIRevoke } from '../../session/apis/snode_api/revokeSubaccount';
import { SnodeGroupSignature } from '../../session/apis/snode_api/signature/groupSignature';
import { ConvoHub } from '../../session/conversations';
@ -79,12 +78,18 @@ type GroupDetailsUpdate = {
members: Array<GroupMemberGet>;
};
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) {
async function checkWeAreAdmin(groupPk: GroupPubkeyType) {
const us = UserUtils.getOurPubKeyStrFromCache();
const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
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}`);
}
}
@ -532,10 +537,8 @@ async function handleRemoveMembersAndRekey({
memberSessionIds: sortedRemoved,
});
const result = await getMessageQueue().sendToPubKeyNonDurably({
const result = await getMessageQueue().sendToGroupV2NonDurably({
message: removedMemberMessage,
pubkey: PubKey.cast(groupPk),
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
});
if (!result) {
throw new Error(
@ -577,7 +580,6 @@ async function getPendingRevokeParams({
revokeChanges.push({ action: 'revoke_subaccount', tokenToRevokeHex: token });
}
return SnodeAPIRevoke.getRevokeSubaccountParams(groupPk, { revokeChanges, unrevokeChanges });
}
@ -621,8 +623,12 @@ async function getUpdateMessagesToPush({
const updateMessages: Array<GroupUpdateMemberChangeMessage> = [];
if (!fromCurrentDevice) {
return updateMessages;
}
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(
new GroupUpdateMemberChangeMessage({
added: allAdded,
@ -635,7 +641,7 @@ async function getUpdateMessagesToPush({
})
);
}
if (fromCurrentDevice && removed.length) {
if (removed.length) {
updateMessages.push(
new GroupUpdateMemberChangeMessage({
removed,
@ -760,6 +766,11 @@ async function handleMemberAddedFromUIOrNot({
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({
groupPk,
removeMembers,
@ -781,7 +792,7 @@ async function handleMemberRemovedFromUIOrNot({
groupPk,
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({
groupPk,
withHistory: [],
@ -789,7 +800,7 @@ async function handleMemberRemovedFromUIOrNot({
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.
await handleRemoveMembersAndRekey({
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(
'group/markUsAsAdmin',
async (
@ -1201,6 +1251,7 @@ const metaGroupSlice = createSlice({
state.memberChangesFromUIPending = true;
});
/** currentDeviceGroupNameChange */
builder.addCase(currentDeviceGroupNameChange.fulfilled, (state, action) => {
state.nameChangesFromUIPending = false;
@ -1218,6 +1269,21 @@ const metaGroupSlice = createSlice({
builder.addCase(currentDeviceGroupNameChange.pending, state => {
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) => {
const { infos, members, groupPk } = action.payload;
state.infos[groupPk] = infos;
@ -1253,6 +1319,7 @@ export const groupInfoActions = {
currentDeviceGroupMembersChange,
markUsAsAdmin,
inviteResponseReceived,
handleMemberLeftMessage,
currentDeviceGroupNameChange,
...metaGroupSlice.actions,
};

Loading…
Cancel
Save