feat: add ability to unsend messages in groupv2

pull/3052/head
Audric Ackermann 2 years ago
parent 05215d8c61
commit d4f3c7fdc1

@ -1,15 +1,15 @@
import React from 'react';
import styled, { CSSProperties } from 'styled-components';
import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
import { MessageAttributes } from '../../models/messageType';
import { UserUtils } from '../../session/utils';
import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { openConversationToSpecificMessage } from '../../state/ducks/conversations';
import { ContactName } from '../conversation/ContactName';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Timestamp } from '../conversation/Timestamp';
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
import { MessageAttributes } from '../../models/messageType';
import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
import { UserUtils } from '../../session/utils';
import { ContactName } from '../conversation/ContactName';
import { Timestamp } from '../conversation/Timestamp';
export type MessageResultProps = MessageAttributes & { snippet: string };

@ -26,6 +26,12 @@ import { channels } from './channels';
import * as dataInit from './dataInit';
import { cleanData } from './dataUtils';
import { SNODE_POOL_ITEM_ID } from './settings-key';
import {
DataCallArgs,
DeleteAllMessageFromSendersInConversationType,
DeleteAllMessageHashesInConversationMatchingAuthorType,
DeleteAllMessageHashesInConversationType,
} from './sharedDataTypes';
const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
@ -586,6 +592,24 @@ async function removeAllMessagesInConversation(conversationId: string): Promise<
);
}
async function deleteAllMessageFromSendersInConversation(
args: DataCallArgs<DeleteAllMessageFromSendersInConversationType>
): ReturnType<DeleteAllMessageFromSendersInConversationType> {
return channels.deleteAllMessageFromSendersInConversation(args);
}
async function deleteAllMessageHashesInConversation(
args: DataCallArgs<DeleteAllMessageHashesInConversationType>
): ReturnType<DeleteAllMessageHashesInConversationType> {
return channels.deleteAllMessageHashesInConversation(args);
}
async function deleteAllMessageHashesInConversationMatchingAuthor(
args: DataCallArgs<DeleteAllMessageHashesInConversationMatchingAuthorType>
): ReturnType<DeleteAllMessageHashesInConversationMatchingAuthorType> {
return channels.deleteAllMessageHashesInConversationMatchingAuthor(args);
}
async function getMessagesBySentAt(sentAt: number): Promise<MessageCollection> {
const messages = await channels.getMessagesBySentAt(sentAt);
return new MessageCollection(messages);
@ -866,6 +890,9 @@ export const Data = {
getLastHashBySnode,
getSeenMessagesByHashList,
removeAllMessagesInConversation,
deleteAllMessageFromSendersInConversation,
deleteAllMessageHashesInConversation,
deleteAllMessageHashesInConversationMatchingAuthor,
getMessagesBySentAt,
getExpiredMessages,
getOutgoingWithoutExpiresAt,

@ -50,6 +50,9 @@ const channelsToMake = new Set([
'getUnreadCountByConversation',
'getMessageCountByType',
'removeAllMessagesInConversation',
'deleteAllMessageFromSendersInConversation',
'deleteAllMessageHashesInConversation',
'deleteAllMessageHashesInConversationMatchingAuthor',
'getMessageCount',
'filterAlreadyFetchedOpengroupMessage',
'getMessagesBySenderAndSentAt',

@ -0,0 +1,24 @@
import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
type PrArrayMsgIds = Promise<Array<string>>;
export type DataCallArgs<T extends (args: any) => any> = Parameters<T>[0];
export type DeleteAllMessageFromSendersInConversationType = (
args: WithGroupPubkey & {
toRemove: Array<PubkeyType>;
}
) => PrArrayMsgIds;
export type DeleteAllMessageHashesInConversationType = (
args: WithGroupPubkey & {
messageHashes: Array<string>;
}
) => PrArrayMsgIds;
export type DeleteAllMessageHashesInConversationMatchingAuthorType = (
args: WithGroupPubkey & {
messageHashes: Array<string>;
author: PubkeyType;
}
) => PrArrayMsgIds;

@ -1,4 +1,5 @@
import { compact } from 'lodash';
import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { compact, isEmpty } from 'lodash';
import { SessionButtonColor } from '../../components/basic/SessionButton';
import { Data } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
@ -6,41 +7,37 @@ import { MessageModel } from '../../models/message';
import { getMessageQueue } from '../../session';
import { deleteSogsMessageByServerIds } from '../../session/apis/open_group_api/sogsv3/sogsV3DeleteMessages';
import { SnodeAPI } from '../../session/apis/snode_api/SNodeAPI';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto';
import { UnsendMessage } from '../../session/messages/outgoing/controlMessage/UnsendMessage';
import { GroupUpdateDeleteMemberContentMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage';
import { ed25519Str } from '../../session/onions/onionPath';
import { PubKey } from '../../session/types';
import { ToastUtils, UserUtils } from '../../session/utils';
import { closeRightPanel, resetSelectedMessageIds } from '../../state/ducks/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { resetRightOverlayMode } from '../../state/ducks/section';
import { MetaGroupWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
/**
* Deletes messages for everyone in a 1-1 or everyone in a closed group conversation.
*/
async function unsendMessagesForEveryone(
async function unsendMessagesForEveryone1o1AndLegacy(
conversation: ConversationModel,
destination: PubkeyType,
msgsToDelete: Array<MessageModel>
) {
window?.log?.info('Deleting messages for all users in this conversation');
const destinationId = conversation.id;
if (!destinationId) {
return;
}
if (conversation.isOpenGroupV2()) {
throw new Error(
'Cannot unsend a message for an opengroup v2. This has to be a deleteMessage api call'
);
const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
if (conversation.isClosedGroupV2()) {
throw new Error('unsendMessagesForEveryone1o1AndLegacy not compatible with group v2');
}
const unsendMsgObjects = getUnsendMessagesObjects(msgsToDelete);
if (conversation.isPrivate()) {
// sending to recipient all the messages separately for now
await Promise.all(
unsendMsgObjects.map(unsendObject =>
getMessageQueue()
.sendToPubKey(new PubKey(destinationId), unsendObject, SnodeNamespaces.Default)
.sendToPubKey(new PubKey(destination), unsendObject, SnodeNamespaces.Default)
.catch(window?.log?.error)
)
);
@ -51,7 +48,9 @@ async function unsendMessagesForEveryone(
.catch(window?.log?.error)
)
);
} else if (conversation.isClosedGroup()) {
return;
}
if (conversation.isClosedGroup()) {
// sending to recipient all the messages separately for now
await Promise.all(
unsendMsgObjects.map(unsendObject => {
@ -59,19 +58,84 @@ async function unsendMessagesForEveryone(
.sendToGroup({
message: unsendObject,
namespace: SnodeNamespaces.LegacyClosedGroup,
groupPubKey: new PubKey(destinationId),
groupPubKey: new PubKey(destination),
})
.catch(window?.log?.error);
})
);
}
}
async function unsendMessagesForEveryoneGroupV2(
conversation: ConversationModel,
groupPk: GroupPubkeyType,
msgsToDelete: Array<MessageModel>
) {
const messageHashesToUnsend = await getMessageHashes(msgsToDelete);
const group = await MetaGroupWrapperActions.infoGet(groupPk);
if (!messageHashesToUnsend.length) {
window.log.info('unsendMessagesForEveryoneGroupV2: no hashes to remove');
return;
}
if (!conversation.isClosedGroupV2()) {
throw new Error('unsendMessagesForEveryoneGroupV2 needs a group v2');
}
await getMessageQueue().sendToGroupV2NonDurably({
message: new GroupUpdateDeleteMemberContentMessage({
createAtNetworkTimestamp: GetNetworkTime.now(),
expirationType: 'unknown',
expireTimer: 0,
groupPk,
memberSessionIds: [],
messageHashes: messageHashesToUnsend,
sodium: await getSodiumRenderer(),
secretKey: group.secretKey,
}),
});
}
/**
* Deletes messages for everyone in a 1-1 or everyone in a closed group conversation.
*/
async function unsendMessagesForEveryone(
conversation: ConversationModel,
msgsToDelete: Array<MessageModel>
) {
window?.log?.info('Deleting messages for all users in this conversation');
const destinationId = conversation.id as string;
if (!destinationId) {
return;
}
if (conversation.isOpenGroupV2()) {
throw new Error(
'Cannot unsend a message for an opengroup v2. This has to be a deleteMessage api call'
);
}
if (
conversation.isPrivate() ||
(conversation.isClosedGroup() && !conversation.isClosedGroupV2())
) {
if (!PubKey.is05Pubkey(conversation.id)) {
throw new Error('unsendMessagesForEveryone1o1AndLegacy requires a 05 key');
}
await unsendMessagesForEveryone1o1AndLegacy(conversation, conversation.id, msgsToDelete);
} else if (conversation.isClosedGroupV2()) {
if (!PubKey.is03Pubkey(destinationId)) {
throw new Error('invalid conversation id (03) for unsendMessageForEveryone');
}
await unsendMessagesForEveryoneGroupV2(conversation, destinationId, msgsToDelete);
}
await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
window.inboxStore?.dispatch(resetSelectedMessageIds());
ToastUtils.pushDeleted(msgsToDelete.length);
}
function getUnsendMessagesObjects(messages: Array<MessageModel>) {
function getUnsendMessagesObjects1o1OrLegacyGroups(messages: Array<MessageModel>) {
// #region building request
return compact(
messages.map(message => {
@ -95,6 +159,14 @@ function getUnsendMessagesObjects(messages: Array<MessageModel>) {
// #endregion
}
async function getMessageHashes(messages: Array<MessageModel>) {
return compact(
messages.map(message => {
return message.get('messageHash');
})
);
}
/**
* Do a single request to the swarm with all the message hashes to delete from the swarm.
*
@ -221,7 +293,7 @@ async function unsendMessageJustForThisUser(
) {
window?.log?.warn('Deleting messages just for this user');
const unsendMsgObjects = getUnsendMessagesObjects(msgsToDelete);
const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
// sending to our other devices all the messages separately for now
await Promise.all(
@ -302,15 +374,37 @@ const doDeleteSelectedMessages = async ({
return;
}
const areAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource());
const areAllOurs = selectedMessages.every(message => message.getSource() === ourDevicePubkey);
if (conversation.isPublic()) {
await doDeleteSelectedMessagesInSOGS(selectedMessages, conversation, areAllOurs);
return;
}
// #region deletion for 1-1 and closed groups
/**
* Note: groupv2 support deleteForEveryone only.
* For groupv2, a user can delete only his messages, but an admin can delete the messages of anyone.
* */
if (deleteForEveryone || conversation.isClosedGroupV2()) {
if (conversation.isClosedGroupV2()) {
const convoId = conversation.id;
if (!PubKey.is03Pubkey(convoId)) {
throw new Error('unsend request for groupv2 but not a 03 key is impossible possible');
}
// only lookup adminKey if we need to
if (!areAllOurs) {
const group = await MetaGroupWrapperActions.infoGet(convoId);
const weAreAdmin = !isEmpty(group.secretKey);
if (!weAreAdmin) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
}
// if they are all ours, of not but we are an admin, we can move forward
await unsendMessagesForEveryone(conversation, selectedMessages);
return;
}
if (deleteForEveryone) {
if (!areAllOurs) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
@ -320,7 +414,7 @@ const doDeleteSelectedMessages = async ({
return;
}
// delete just for me in a closed group only means delete locally
// delete just for me in a legacy closed group only means delete locally
if (conversation.isClosedGroup()) {
await deleteMessagesFromSwarmAndCompletelyLocally(conversation, selectedMessages);
@ -331,8 +425,6 @@ const doDeleteSelectedMessages = async ({
}
// otherwise, delete that message locally, from our swarm and from our other devices
await unsendMessageJustForThisUser(conversation, selectedMessages);
// #endregion
};
export async function deleteMessagesByIdForEveryone(

@ -242,7 +242,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
);
}
public isClosedGroupV2(): boolean {
public isClosedGroupV2() {
return Boolean(this.get('type') === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(this.id));
}
@ -1097,6 +1097,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (this.isClosedGroup()) {
if (this.isAdmin(UserUtils.getOurPubKeyStrFromCache())) {
if (this.isClosedGroupV2()) {
if (!PubKey.is03Pubkey(this.id)) {
throw new Error('updateExpireTimer v2 group requires a 03 key');
}
const group = await UserGroupsWrapperActions.getGroup(this.id);
if (!group || !group.secretKey) {
throw new Error(
@ -2162,6 +2165,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
private async sendMessageToGroupV2(chatMessageParams: VisibleMessageParams) {
if (!PubKey.is03Pubkey(this.id)) {
throw new Error('sendMessageToGroupV2 needs a 03 key');
}
const visibleMessage = new VisibleMessage(chatMessageParams);
const groupVisibleMessage = new ClosedGroupV2VisibleMessage({
chatMessage: visibleMessage,

@ -105,6 +105,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion34,
updateToSessionSchemaVersion35,
updateToSessionSchemaVersion36,
updateToSessionSchemaVersion37,
];
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@ -1952,6 +1953,38 @@ function updateToSessionSchemaVersion36(currentVersion: number, db: BetterSqlite
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion37(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 37;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN messageHash TEXT;
UPDATE ${MESSAGES_TABLE} SET
messageHash = json_extract(json, '$.messageHash');
`);
db.exec(`CREATE INDEX messages_t_messageHash ON ${MESSAGES_TABLE} (
messageHash
);`);
db.exec(`CREATE INDEX messages_t_messageHash_author ON ${MESSAGES_TABLE} (
messageHash,
source
);`);
db.exec(`CREATE INDEX messages_t_messageHash_author_convoId ON ${MESSAGES_TABLE} (
messageHash,
source,
conversationId
);`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
export function printTableColumns(table: string, db: BetterSqlite3.Database) {
console.info(db.pragma(`table_info('${table}');`));
}

@ -1,6 +1,6 @@
import path from 'path';
import * as BetterSqlite3 from '@signalapp/better-sqlite3';
import { isNumber } from 'lodash';
import path from 'path';
import {
ATTACHMENT_DOWNLOADS_TABLE,

@ -53,6 +53,7 @@ import { StorageItem } from './storage_item'; // checked - only node
import { OpenGroupV2Room } from '../data/opengroups';
import {
AwaitedReturn,
CONFIG_DUMP_TABLE,
MsgDuplicateSearchOpenGroup,
roomHasBlindEnabled,
@ -63,6 +64,12 @@ import {
} from '../types/sqlSharedTypes';
import { KNOWN_BLINDED_KEYS_ITEM, SettingsKey } from '../data/settings-key';
import {
DataCallArgs,
DeleteAllMessageFromSendersInConversationType,
DeleteAllMessageHashesInConversationMatchingAuthorType,
DeleteAllMessageHashesInConversationType,
} from '../data/sharedDataTypes';
import { MessageAttributes } from '../models/messageType';
import { SignalService } from '../protobuf';
import { Quote } from '../receiver/types';
@ -804,6 +811,7 @@ function saveMessage(data: MessageAttributes) {
expireTimer,
expirationStartTimestamp,
flags,
messageHash,
} = data;
if (!id) {
@ -836,6 +844,7 @@ function saveMessage(data: MessageAttributes) {
type: type || '',
unread,
flags: flags ?? 0,
messageHash,
};
assertGlobalInstance()
@ -860,7 +869,8 @@ function saveMessage(data: MessageAttributes) {
source,
type,
unread,
flags
flags,
messageHash
) values (
$id,
$json,
@ -881,7 +891,8 @@ function saveMessage(data: MessageAttributes) {
$source,
$type,
$unread,
$flags
$flags,
$messageHash
);`
)
.run(payload);
@ -1047,6 +1058,56 @@ function removeAllMessagesInConversation(
.run({ conversationId });
}
function deleteAllMessageFromSendersInConversation(
{ groupPk, toRemove }: DataCallArgs<DeleteAllMessageFromSendersInConversationType>,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageFromSendersInConversationType> {
if (!groupPk || !toRemove.length) {
return [];
}
return assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING id`
)
.all(groupPk, toRemove)
.map(m => m.id);
}
function deleteAllMessageHashesInConversation(
{ groupPk, messageHashes }: DataCallArgs<DeleteAllMessageHashesInConversationType>,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationType> {
if (!groupPk || !messageHashes.length) {
return [];
}
return assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id`
)
.all(groupPk, ...messageHashes)
.map(m => m.id);
}
function deleteAllMessageHashesInConversationMatchingAuthor(
{
author,
groupPk,
messageHashes,
}: DataCallArgs<DeleteAllMessageHashesInConversationMatchingAuthorType>,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationMatchingAuthorType> {
if (!groupPk || !author || !messageHashes.length) {
return [];
}
console.warn('messageHashes', messageHashes);
return assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND source = ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id`
)
.all(groupPk, author, ...messageHashes)
.map(m => m.id);
}
function cleanUpExpirationTimerUpdateHistory(
conversationId: string,
isPrivate: boolean,
@ -2541,6 +2602,9 @@ export const sqlNode = {
getAllMessagesWithAttachmentsInConversationSentBefore,
cleanUpExpirationTimerUpdateHistory,
removeAllMessagesInConversation,
deleteAllMessageFromSendersInConversation,
deleteAllMessageHashesInConversation,
deleteAllMessageHashesInConversationMatchingAuthor,
getUnreadByConversation,
getUnreadDisappearingByConversation,
markAllAsReadByConversationNoExpiration,

@ -1,5 +1,6 @@
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty, isFinite, isNumber } from 'lodash';
import { Data } from '../../data/data';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
@ -19,6 +20,7 @@ import { PreConditionFailed } from '../../session/utils/errors';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile';
import { messagesExpired } from '../../state/ducks/conversations';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
@ -343,24 +345,49 @@ async function handleGroupMemberLeftMessage({
convo.set({
active_at: signatureTimestamp,
});
// debugger TODO We should process this message type even if the sender is blocked
// TODO We should process this message type even if the sender is blocked
}
async function handleGroupDeleteMemberContentMessage({
groupPk,
signatureTimestamp,
change,
author,
}: GroupUpdateGeneric<SignalService.GroupUpdateDeleteMemberContentMessage>) {
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
/**
* When handling a GroupUpdateDeleteMemberContentMessage we need to do a few things.
* When `adminSignature` is empty,
* 1. we only delete the messageHashes which are in the change.messageHashes AND sent by that same author.
* When `adminSignature` is not empty and valid,
* 2. we delete all the messages in the group sent by any of change.memberSessionIds AND
* 3. we delete all the messageHashes in the conversation matching the change.messageHashes (even if not from the right sender)
*/
if (isEmpty(change.adminSignature)) {
// this is step 1.
const msgsDeleted = await Data.deleteAllMessageHashesInConversationMatchingAuthor({
author,
groupPk,
messageHashes: change.messageHashes,
});
window.inboxStore.dispatch(
messagesExpired(msgsDeleted.map(m => ({ conversationKey: groupPk, messageId: m })))
);
convo.updateLastMessage();
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexStringNoPrefix(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(
`DELETE_CONTENT${signatureTimestamp}${change.memberSessionIds.join()}${change.messageHashes.join()}`
`DELETE_CONTENT${signatureTimestamp}${change.memberSessionIds.join('')}${change.messageHashes.join('')}`
),
});
@ -369,11 +396,27 @@ async function handleGroupDeleteMemberContentMessage({
return;
}
const toRemove = change.memberSessionIds.filter(PubKey.is05Pubkey);
const deletedBySenders = await Data.deleteAllMessageFromSendersInConversation({
groupPk,
toRemove,
}); // this is step 2.
const deletedByHashes = await Data.deleteAllMessageHashesInConversation({
groupPk,
messageHashes: change.messageHashes,
}); // this is step 3.
window.inboxStore.dispatch(
messagesExpired(
[...deletedByHashes, ...deletedBySenders].map(m => ({
conversationKey: groupPk,
messageId: m,
}))
)
);
convo.updateLastMessage();
// TODO we should process this message type even if the sender is blocked
convo.set({
active_at: signatureTimestamp,
});
throw new Error('Not implemented');
}
async function handleGroupUpdateDeleteMessage({
@ -389,7 +432,7 @@ async function handleGroupUpdateDeleteMessage({
const sigValid = await verifySig({
pubKey: HexString.fromHexStringNoPrefix(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`DELETE${signatureTimestamp}${change.memberSessionIds.join()}`),
data: stringToUint8Array(`DELETE${signatureTimestamp}${change.memberSessionIds.join('')}`),
});
if (!sigValid) {

@ -2,36 +2,47 @@ import { PubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { stringToUint8Array } from '../../../../../utils/String';
import { Preconditions } from '../../../preconditions';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
import {
AdminSigDetails,
GroupUpdateMessage,
GroupUpdateMessageParams,
} from '../GroupUpdateMessage';
type Params = GroupUpdateMessageParams & {
memberSessionIds: Array<PubkeyType>;
adminSignature: Uint8Array; // this is a signature of `"DELETE_CONTENT" || timestamp || sessionId[0] || ... || sessionId[N]`
};
// Note: `Partial<AdminSigDetails>` because that message can also be sent as a non-admin and we always give sodium but not always the secretKey
type Params = GroupUpdateMessageParams &
Partial<Omit<AdminSigDetails, 'sodium'>> &
Omit<AdminSigDetails, 'secretKey'> & {
memberSessionIds: Array<PubkeyType>;
messageHashes: Array<string>;
};
/**
* GroupUpdateDeleteMemberContentMessage is sent as a message to group's swarm.
*/
export class GroupUpdateDeleteMemberContentMessage extends GroupUpdateMessage {
public readonly createAtNetworkTimestamp: Params['createAtNetworkTimestamp'];
public readonly memberSessionIds: Params['memberSessionIds'];
public readonly adminSignature: Params['adminSignature'];
public readonly messageHashes: Params['messageHashes'];
public readonly secretKey: Params['secretKey'];
public readonly sodium: Params['sodium'];
public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
constructor(params: Params) {
super(params);
this.adminSignature = params.adminSignature;
this.memberSessionIds = params.memberSessionIds;
if (isEmpty(this.memberSessionIds)) {
throw new Error('GroupUpdateDeleteMemberContentMessage needs members in list');
this.messageHashes = params.messageHashes;
this.secretKey = params.secretKey;
this.createAtNetworkTimestamp = params.createAtNetworkTimestamp;
this.sodium = params.sodium;
if (isEmpty(this.memberSessionIds) && isEmpty(this.messageHashes)) {
throw new Error(
'GroupUpdateDeleteMemberContentMessage needs members or messageHashes to be filled'
);
}
Preconditions.checkUin8tArrayOrThrow({
data: this.adminSignature,
expectedLength: 64,
varName: 'adminSignature',
context: this.constructor.toString(),
});
Preconditions.checkArrayHaveOnly05Pubkeys({
arr: this.memberSessionIds,
@ -41,9 +52,21 @@ export class GroupUpdateDeleteMemberContentMessage extends GroupUpdateMessage {
}
public dataProto(): SignalService.DataMessage {
// If we have the secretKey, we can delete it for anyone `"DELETE_CONTENT" || timestamp || sessionId[0] || ... || messageHashes[0] || ...`
let adminSignature = new Uint8Array();
if (this.secretKey && this.sodium) {
adminSignature = this.sodium.crypto_sign_detached(
stringToUint8Array(
`DELETE_CONTENT${this.createAtNetworkTimestamp}${this.memberSessionIds.join('')}${this.messageHashes.join('')}`
),
this.secretKey
);
}
const deleteMemberContent = new SignalService.GroupUpdateDeleteMemberContentMessage({
adminSignature: this.adminSignature,
adminSignature,
memberSessionIds: this.memberSessionIds,
messageHashes: this.messageHashes,
});
return new SignalService.DataMessage({ groupUpdateMessage: { deleteMemberContent } });

@ -8,10 +8,8 @@ import { downloadAttachment, downloadAttachmentSogsV3 } from '../../receiver/att
import { initializeAttachmentLogic, processNewAttachment } from '../../types/MessageAttachment';
import { getAttachmentMetadata } from '../../types/message/initializeAttachmentMetadata';
import { AttachmentDownloadMessageDetails } from '../../types/sqlSharedTypes';
import { MetaGroupWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { was404Error } from '../apis/snode_api/onions';
import * as Constants from '../constants';
import { PubKey } from '../types';
// this may cause issues if we increment that value to > 1, but only having one job will block the whole queue while one attachment is downloading
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
@ -139,34 +137,6 @@ async function _maybeStartJob() {
}
}
async function shouldSkipGroupAttachmentDownload({
groupPk,
messageModel,
}: {
groupPk: string;
messageModel: MessageModel;
}) {
if (!PubKey.is03Pubkey(groupPk)) {
return false;
}
try {
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
const sentAt = messageModel.get('sent_at');
if (!sentAt) {
return false;
}
if (
(infos.deleteAttachBeforeSeconds && sentAt <= infos.deleteAttachBeforeSeconds * 1000) ||
(infos.deleteBeforeSeconds && sentAt <= infos.deleteBeforeSeconds * 1000)
) {
return true;
}
} catch (e) {
window.log.warn('shouldSkipGroupAttachmentDownload failed with ', e.message);
}
return false; // try to download it
}
async function _runJob(job: any) {
const { id, messageId, attachment, type, index, attempts, isOpenGroupV2, openGroupV2Details } =
job || {};
@ -182,16 +152,6 @@ async function _runJob(job: any) {
await _finishJob(null, id);
return;
}
const shouldSkipJobForGroup = await shouldSkipGroupAttachmentDownload({
groupPk: found.get('conversationId'),
messageModel: found,
});
if (shouldSkipJobForGroup) {
logger.info('_runJob: shouldSkipGroupAttachmentDownload is true, deleting job');
await _finishJob(null, id);
return;
}
const isTrusted = found.isTrustedForAttachmentDownload();

@ -498,7 +498,7 @@ describe('SwarmPolling:pollForAllKeys', () => {
);
stubWithLegacyGroups([]);
stubWithGroups([convo.id]);
stubWithGroups([convo.id as GroupPubkeyType]);
convo.set('active_at', Date.now());
groupConvoPubkey = PubKey.cast(convo.id as string);

Loading…
Cancel
Save