fix: delete member + content from the admin side

pull/3052/head
Audric Ackermann 10 months ago
parent c476ad1704
commit aa6d39c270

@ -35,6 +35,7 @@ window.sessionFeatureFlags = {
debug: {
debugLogging: !_.isEmpty(process.env.SESSION_DEBUG),
debugLibsessionDumps: !_.isEmpty(process.env.SESSION_DEBUG_LIBSESSION_DUMPS),
debugBuiltSnodeRequests: !_.isEmpty(process.env.SESSION_DEBUG_BUILT_SNODE_REQUEST),
debugFileServerRequests: false,
debugNonSnodeRequests: false,
debugOnionRequests: false,

@ -1,7 +1,5 @@
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 = (
@ -9,14 +7,14 @@ export type DeleteAllMessageFromSendersInConversationType = (
toRemove: Array<PubkeyType>;
signatureTimestamp: number;
}
) => PrArrayMsgIds;
) => Promise<{ messageHashes: Array<string> }>;
export type DeleteAllMessageHashesInConversationType = (
args: WithGroupPubkey & {
messageHashes: Array<string>;
signatureTimestamp: number;
}
) => PrArrayMsgIds;
) => Promise<{ messageHashes: Array<string> }>;
export type DeleteAllMessageHashesInConversationMatchingAuthorType = (
args: WithGroupPubkey & {

@ -188,6 +188,7 @@ export async function deleteMessagesFromSwarmOnly(
pubkey: PubkeyType | GroupPubkeyType
) {
const deletionMessageHashes = isStringArray(messages) ? messages : getMessageHashes(messages);
try {
if (isEmpty(messages)) {
return false;

@ -1095,14 +1095,17 @@ function deleteAllMessageFromSendersInConversation(
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageFromSendersInConversationType> {
if (!groupPk || !toRemove.length) {
return [];
return { messageHashes: [] };
}
return assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING id`
)
.all(groupPk, signatureTimestamp, ...toRemove)
.map(m => m.id);
const messageHashes = compact(
assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING messageHash`
)
.all(groupPk, signatureTimestamp, ...toRemove)
.map(m => m.messageHash)
);
return { messageHashes };
}
function deleteAllMessageHashesInConversation(
@ -1114,14 +1117,17 @@ function deleteAllMessageHashesInConversation(
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationType> {
if (!groupPk || !messageHashes.length) {
return [];
return { messageHashes: [] };
}
return assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id`
)
.all(groupPk, signatureTimestamp, ...messageHashes)
.map(m => m.id);
const deletedMessageHashes = compact(
assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING messageHash`
)
.all(groupPk, signatureTimestamp, ...messageHashes)
.map(m => m.messageHash)
);
return { messageHashes: deletedMessageHashes };
}
function deleteAllMessageHashesInConversationMatchingAuthor(

@ -1,5 +1,5 @@
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty, isFinite, isNumber } from 'lodash';
import { compact, isEmpty, isFinite, isNumber } from 'lodash';
import { Data } from '../../data/data';
import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/unsendingInteractions';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
@ -21,7 +21,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 { messageHashesExpired, messagesExpired } from '../../state/ducks/conversations';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
@ -460,10 +460,10 @@ async function handleGroupDeleteMemberContentMessage({
}); // this is step 3.
window.inboxStore.dispatch(
messagesExpired(
[...deletedByHashes, ...deletedBySenders].map(m => ({
messageHashesExpired(
compact([...deletedByHashes.messageHashes, ...deletedBySenders.messageHashes]).map(m => ({
conversationKey: groupPk,
messageId: m,
messageHash: m,
}))
)
);

@ -565,7 +565,7 @@ export class SwarmPolling {
}
if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) {
const toBump = await MetaGroupWrapperActions.currentHashes(pubkey);
window.log.debug(`configHashesToBump group count: ${toBump.length}`);
window.log.debug(`configHashesToBump group(${ed25519Str(pubkey)}) count: ${toBump.length}`);
return toBump;
}
return [];

@ -56,6 +56,7 @@ import { UserUtils } from '../utils';
import { ed25519Str, fromUInt8ArrayToBase64 } from '../utils/String';
import { MessageSentHandler } from './MessageSentHandler';
import { MessageWrapper } from './MessageWrapper';
import { stringify } from '../../types/sqlSharedTypes';
// ================ SNODE STORE ================
@ -347,6 +348,19 @@ async function getSignatureParamsFromNamespace(
return {};
}
function logBuildSubRequests(subRequests: Array<BuiltSnodeSubRequests>) {
if (!window.sessionFeatureFlags.debug.debugBuiltSnodeRequests) {
return;
}
window.log.debug(
`\n========================================\nsubRequests: [\n\t${subRequests
.map(m => {
return stringify(m);
})
.join(',\n\t')}]\n========================================`
);
}
async function signSubRequests(
params: Array<RawSnodeSubRequests>
): Promise<Array<BuiltSnodeSubRequests>> {
@ -356,6 +370,8 @@ async function signSubRequests(
})
);
logBuildSubRequests(signedRequests);
return signedRequests;
}

@ -5,6 +5,7 @@ import { v4 } from 'uuid';
import { StringUtils } from '../..';
import { Data } from '../../../../data/data';
import { deleteMessagesFromSwarmOnly } from '../../../../interactions/conversations/unsendingInteractions';
import { messageHashesExpired } from '../../../../state/ducks/conversations';
import {
MetaGroupWrapperActions,
MultiEncryptWrapperActions,
@ -217,8 +218,21 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
signatureTimestamp: GetNetworkTime.now(),
});
if (msgHashesToDeleteOnGroupSwarm.length) {
await deleteMessagesFromSwarmOnly(msgHashesToDeleteOnGroupSwarm, groupPk);
if (msgHashesToDeleteOnGroupSwarm.messageHashes.length) {
const deleted = await deleteMessagesFromSwarmOnly(
msgHashesToDeleteOnGroupSwarm.messageHashes,
groupPk
);
if (deleted) {
window.inboxStore.dispatch(
messageHashesExpired(
msgHashesToDeleteOnGroupSwarm.messageHashes.map(messageHash => ({
conversationKey: groupPk,
messageHash,
}))
)
);
}
}
}
} catch (e) {

@ -11,6 +11,7 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
DeleteAllFromGroupMsgNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest,
StoreGroupKeysSubRequest,
StoreGroupMessageSubRequest,
SubaccountRevokeSubRequest,
@ -144,27 +145,30 @@ async function pushChangesToGroupSwarmIfNeeded({
m =>
m instanceof SubaccountRevokeSubRequest ||
m instanceof SubaccountUnrevokeSubRequest ||
m instanceof DeleteAllFromGroupMsgNodeSubRequest
m instanceof DeleteAllFromGroupMsgNodeSubRequest ||
m instanceof DeleteHashesFromGroupNodeSubRequest
);
const sortedSubRequests = compact([
supplementalKeysSubRequest, // this needs to be stored first
...pendingConfigRequests, // groupKeys are first in this array, so all good, then groupInfos are next
...extraStoreRequests, // this can be stored anytime
...extraRequests,
]);
const result = await MessageSender.sendEncryptedDataToSnode({
// Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests
// as this is to avoid a race condition where a device is polling right
// while we are posting the configs (already encrypted with the new keys)
sortedSubRequests: compact([
supplementalKeysSubRequest, // this needs to be stored first
...pendingConfigRequests, // groupKeys are first in this array, so all good, then groupInfos are next
...extraStoreRequests, // this can be stored anytime
...extraRequests,
]),
sortedSubRequests,
destination: groupPk,
method: 'sequence',
});
const expectedReplyLength =
pendingConfigRequests.length + // each of those are sent as a subrequest
(supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single subrequest
(extraStoreRequests ? 1 : 0) + // each of those are sent as a subrequest
pendingConfigRequests.length + // each of those are sent as a subrequest
extraStoreRequests.length + // each of those are sent as a subrequest
extraRequestWithExpectedResults.length; // each of those are sent as a subrequest, but they don't all return something...
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure

@ -601,18 +601,32 @@ function handleMessagesChangedOrAdded(
function handleMessageExpiredOrDeleted(
state: ConversationsStateType,
payload: {
messageId: string;
conversationKey: string;
}
payload: { conversationKey: string } & (
| {
messageId: string;
}
| {
messageHash: string;
}
)
) {
const { conversationKey, messageId } = payload;
const { conversationKey } = payload;
const messageId = (payload as any).messageId as string | undefined;
const messageHash = (payload as any).messageHash as string | undefined;
if (conversationKey === state.selectedConversation) {
// search if we find this message id.
// we might have not loaded yet, so this case might not happen
const messageInStoreIndex = state?.messages.findIndex(m => m.propsForMessage.id === messageId);
const messageInStoreIndex = state?.messages.findIndex(
m =>
(messageId && m.propsForMessage.id === messageId) ||
(messageHash && m.propsForMessage.messageHash === messageHash)
);
const editedQuotes = { ...state.quotes };
if (messageInStoreIndex >= 0) {
const msgToRemove = state.messages[messageInStoreIndex];
const extractedMessageId = msgToRemove.propsForMessage.id;
// we cannot edit the array directly, so slice the first part, and slice the second part,
// keeping the index removed out
const editedMessages = [
@ -637,7 +651,9 @@ function handleMessageExpiredOrDeleted(
messages: editedMessages,
quotes: editedQuotes,
firstUnreadMessageId:
state.firstUnreadMessageId === messageId ? undefined : state.firstUnreadMessageId,
state.firstUnreadMessageId === extractedMessageId
? undefined
: state.firstUnreadMessageId,
};
}
@ -649,10 +665,16 @@ function handleMessageExpiredOrDeleted(
function handleMessagesExpiredOrDeleted(
state: ConversationsStateType,
action: PayloadAction<
Array<{
messageId: string;
conversationKey: string;
}>
Array<
{ conversationKey: string } & (
| {
messageId: string;
}
| {
messageHash: string;
}
)
>
>
): ConversationsStateType {
let stateCopy = state;
@ -797,6 +819,17 @@ const conversationsSlice = createSlice({
) {
return handleMessagesExpiredOrDeleted(state, action);
},
messageHashesExpired(
state: ConversationsStateType,
action: PayloadAction<
Array<{
messageHash: string;
conversationKey: string;
}>
>
) {
return handleMessagesExpiredOrDeleted(state, action);
},
messagesDeleted(
state: ConversationsStateType,
@ -1139,6 +1172,7 @@ export const {
conversationRemoved,
removeAllConversations,
messagesExpired,
messageHashesExpired,
messagesDeleted,
conversationReset,
messagesChanged,

@ -308,11 +308,15 @@ export function toFixedUint8ArrayOfLength<T extends number>(
}
export function stringify(obj: unknown) {
return JSON.stringify(obj, (_key, value) => {
return value instanceof Uint8Array
? `Uint8Array(${value.length}): ${toHex(value)}`
: value?.type === 'Buffer' && value?.data
? `Buffer: ${toHex(value.data)}`
: value;
});
return JSON.stringify(
obj,
(_key, value) => {
return value instanceof Uint8Array
? `Uint8Array(${value.length}): ${toHex(value)}`
: value?.type === 'Buffer' && value?.data
? `Buffer: ${toHex(value.data)}`
: value;
},
2
);
}

1
ts/window.d.ts vendored

@ -34,6 +34,7 @@ declare global {
debug: {
debugLogging: boolean;
debugLibsessionDumps: boolean;
debugBuiltSnodeRequests: boolean;
debugFileServerRequests: boolean;
debugNonSnodeRequests: boolean;
debugOnionRequests: boolean;

Loading…
Cancel
Save