You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts

291 lines
10 KiB
TypeScript

/* eslint-disable no-await-in-loop */
import { WithGroupPubkey } from 'libsession_util_nodejs';
import { compact, isEmpty, isNumber } from 'lodash';
import { v4 } from 'uuid';
import AbortController from 'abort-controller';
import { StringUtils } from '../..';
import { Data } from '../../../../data/data';
import { deleteMessagesFromSwarmOnly } from '../../../../interactions/conversations/unsendingInteractions';
import {
MetaGroupWrapperActions,
MultiEncryptWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
StoreGroupMessageSubRequest,
StoreGroupRevokedRetrievableSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes';
import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory';
import { RevokeChanges, SnodeAPIRevoke } from '../../../apis/snode_api/revokeSubaccount';
import { concatUInt8Array, getSodiumRenderer } from '../../../crypto';
import { GroupUpdateDeleteMemberContentMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage';
import { MessageSender } from '../../../sending';
import { fromHexToArray } from '../../String';
import { runners } from '../JobRunner';
import {
AddJobCheckReturn,
GroupPendingRemovalsPersistedData,
PersistedJob,
RunJobResult,
} from '../PersistedJob';
import { GroupSync } from './GroupSyncJob';
import { NetworkTime } from '../../../../util/NetworkTime';
import {
WithAddWithHistoryMembers,
WithAddWithoutHistoryMembers,
WithRemoveMembers,
WithSecretKey,
} from '../../../types/with';
import { groupInfoActions } from '../../../../state/ducks/metaGroups';
import { DURATION, TTL_DEFAULT } from '../../../constants';
import { timeoutWithAbort } from '../../Promise';
const defaultMsBetweenRetries = 10000;
const defaultMaxAttempts = 1;
type JobExtraArgs = Pick<GroupPendingRemovalsPersistedData, 'groupPk'>;
async function addJob({ groupPk }: JobExtraArgs) {
const pendingRemovalJob = new GroupPendingRemovalsJob({
groupPk,
nextAttemptTimestamp: Date.now() + 1000, // postpone by 1s
});
window.log.debug(`addGroupPendingRemovalJob: adding group pending removal for ${groupPk} `);
await runners.groupPendingRemovalJobRunner.addJob(pendingRemovalJob);
}
async function getPendingRevokeParams({
withoutHistory,
withHistory,
removed,
groupPk,
secretKey,
}: WithGroupPubkey &
WithSecretKey &
WithAddWithoutHistoryMembers &
WithAddWithHistoryMembers &
WithRemoveMembers) {
const revokeChanges: RevokeChanges = [];
const unrevokeChanges: RevokeChanges = [];
const toUnrevoke = withoutHistory.concat(withHistory);
for (let index = 0; index < toUnrevoke.length; index++) {
const m = toUnrevoke[index];
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, m);
unrevokeChanges.push({ action: 'unrevoke_subaccount', tokenToRevokeHex: token });
}
for (let index = 0; index < removed.length; index++) {
const m = removed[index];
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, m);
revokeChanges.push({ action: 'revoke_subaccount', tokenToRevokeHex: token });
}
return SnodeAPIRevoke.getRevokeSubaccountParams(groupPk, secretKey, {
revokeChanges,
unrevokeChanges,
});
}
class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersistedData> {
constructor({
groupPk,
nextAttemptTimestamp,
maxAttempts,
currentRetry,
identifier,
}: Pick<GroupPendingRemovalsPersistedData, 'groupPk'> &
Partial<
Pick<
GroupPendingRemovalsPersistedData,
'nextAttemptTimestamp' | 'identifier' | 'maxAttempts' | 'currentRetry'
>
>) {
super({
jobType: 'GroupPendingRemovalJobType',
identifier: identifier || v4(),
groupPk,
delayBetweenRetries: defaultMsBetweenRetries,
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts,
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
currentRetry: isNumber(currentRetry) ? currentRetry : 0,
});
}
public async run() {
const { groupPk, jobType, identifier } = this.persistedData;
window.log.info(`running job ${jobType} with groupPk:"${groupPk}" id:"${identifier}" `);
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group || !group.secretKey || isEmpty(group.secretKey)) {
window.log.warn(
`GroupPendingRemovalsJob: Did not find group in wrapper or no valid info in wrapper`
);
return RunJobResult.PermanentFailure;
}
try {
const pendingRemovals = await MetaGroupWrapperActions.memberGetAllPendingRemovals(groupPk);
if (!pendingRemovals.length) {
return RunJobResult.Success;
}
const deleteMessagesOfMembers = pendingRemovals
.filter(m => m.memberStatus === 'REMOVED_MEMBER_AND_MESSAGES')
.map(m => m.pubkeyHex);
const sessionIdsHex = pendingRemovals.map(m => m.pubkeyHex);
const sessionIds = sessionIdsHex.map(m => fromHexToArray(m).slice(1));
const currentGen = await MetaGroupWrapperActions.keyGetCurrentGen(groupPk);
const dataToEncrypt = sessionIds.map(s => {
return concatUInt8Array(s, StringUtils.stringToUint8Array(`${currentGen}`));
});
const multiEncryptedMessage = await MultiEncryptWrapperActions.multiEncrypt({
messages: dataToEncrypt,
recipients: sessionIds,
ed25519SecretKey: group.secretKey,
domain: 'SessionGroupKickedMessage',
});
// first, get revoke requests that need to be pushed for leaving member
const revokeUnrevokeParams = await getPendingRevokeParams({
groupPk,
withHistory: [],
withoutHistory: [],
removed: sessionIdsHex,
secretKey: group.secretKey,
});
const multiEncryptRequest = new StoreGroupRevokedRetrievableSubRequest({
encryptedData: multiEncryptedMessage,
groupPk,
secretKey: group.secretKey,
ttlMs: TTL_DEFAULT.CONFIG_MESSAGE,
getNow: NetworkTime.now,
});
const revokeRequests = compact([
revokeUnrevokeParams.revokeSubRequest ? revokeUnrevokeParams.revokeSubRequest : null,
revokeUnrevokeParams.unrevokeSubRequest ? revokeUnrevokeParams.unrevokeSubRequest : null,
]);
let storeRequests: Array<StoreGroupMessageSubRequest> = [];
if (deleteMessagesOfMembers.length) {
const deleteContentMsg = new GroupUpdateDeleteMemberContentMessage({
createAtNetworkTimestamp: NetworkTime.now(),
expirationType: 'unknown', // GroupUpdateDeleteMemberContentMessage this is not displayed so not expiring.
expireTimer: 0,
groupPk,
memberSessionIds: deleteMessagesOfMembers,
messageHashes: [],
sodium: await getSodiumRenderer(),
secretKey: group.secretKey,
});
storeRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[deleteContentMsg],
{ authData: null, secretKey: group.secretKey }
);
}
const sortedSubRequests = compact([multiEncryptRequest, ...revokeRequests, ...storeRequests]);
const controller = new AbortController();
const result = await timeoutWithAbort(
MessageSender.sendEncryptedDataToSnode({
sortedSubRequests,
destination: groupPk,
method: 'sequence',
abortSignal: controller.signal,
allow401s: false,
}),
30 * DURATION.SECONDS,
controller
);
if (
!result ||
result.length !== sortedSubRequests.length ||
result.some(m => m.code !== 200)
) {
window.log.warn(
'GroupPendingRemovalsJob: sendEncryptedDataToSnode unexpected result length or content. Scheduling retry if possible'
);
return RunJobResult.RetryJobIfPossible;
}
// both requests success, remove the members from the group member entirely and sync
await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, sessionIdsHex);
await GroupSync.queueNewJobIfNeeded(groupPk);
try {
if (deleteMessagesOfMembers.length) {
const models = await Data.findAllMessageFromSendersInConversation({
groupPk,
toRemove: deleteMessagesOfMembers,
signatureTimestamp: NetworkTime.now(),
});
const messageHashes = compact(models.map(m => m.getMessageHash()));
if (messageHashes.length) {
await deleteMessagesFromSwarmOnly(messageHashes, groupPk);
}
for (let index = 0; index < models.length; index++) {
const messageModel = models[index];
try {
// eslint-disable-next-line no-await-in-loop
await messageModel.markAsDeleted();
} catch (e) {
window.log.warn(
`GroupPendingRemoval markAsDeleted of ${messageModel.getMessageHash()} failed with`,
e.message
);
}
}
}
} catch (e) {
window.log.warn('GroupPendingRemovalsJob allowed to fail part failed with:', e.message);
}
window.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
);
// return true so this job is marked as a success and we don't need to retry it
return RunJobResult.Success;
} catch (e) {
window.log.warn('GroupPendingRemovalsJob failed with', e.message);
return RunJobResult.RetryJobIfPossible;
}
}
public serializeJob() {
return super.serializeBase();
}
public nonRunningJobsToRemove(_jobs: Array<GroupPendingRemovalsPersistedData>) {
return [];
}
public addJobCheck(jobs: Array<GroupPendingRemovalsPersistedData>): AddJobCheckReturn {
// avoid adding the same job if the exact same one is already planned
const hasSameJob = jobs.some(j => {
return j.groupPk === this.persistedData.groupPk;
});
if (hasSameJob) {
return 'skipAddSameJobPresent';
}
return null;
}
public getJobTimeoutMs(): number {
return 15 * DURATION.SECONDS;
}
}
export const GroupPendingRemovals = {
addJob,
getPendingRevokeParams,
};