Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / pass / store / sagas / events / channel.invites.ts
blobd6475be21747921585a3431f05b37269413a5af5
1 /* eslint-disable @typescript-eslint/no-throw-literal, curly */
2 import { all, fork, put, select } from 'redux-saga/effects';
4 import { getPublicKeysForEmail } from '@proton/pass/lib/auth/address';
5 import { PassCrypto } from '@proton/pass/lib/crypto';
6 import { type EventManagerEvent, NOOP_EVENT } from '@proton/pass/lib/events/manager';
7 import { decodeVaultContent } from '@proton/pass/lib/vaults/vault-proto.transformer';
8 import { syncInvites } from '@proton/pass/store/actions';
9 import type { InviteState } from '@proton/pass/store/reducers';
10 import { selectAllVaults } from '@proton/pass/store/selectors';
11 import { selectInvites } from '@proton/pass/store/selectors/invites';
12 import type { RootSagaOptions } from '@proton/pass/store/types';
13 import type { InvitesGetResponse, MaybeNull, Share, ShareType } from '@proton/pass/types';
14 import { type Api } from '@proton/pass/types';
15 import type { Invite } from '@proton/pass/types/data/invites';
16 import { prop } from '@proton/pass/utils/fp/lens';
17 import { truthy } from '@proton/pass/utils/fp/predicates';
18 import { logId, logger } from '@proton/pass/utils/logger';
19 import { toMap } from '@proton/shared/lib/helpers/object';
21 import { eventChannelFactory } from './channel.factory';
22 import { channelEventsWorker, channelInitWorker } from './channel.worker';
24 const NAMESPACE = 'ServerEvents::Invites';
26 function* onInvitesEvent(event: EventManagerEvent<InvitesGetResponse>) {
27     if ('error' in event) throw event.error;
29     const cachedInvites: InviteState = yield select(selectInvites);
30     const cachedInviteTokens = Object.keys(cachedInvites);
32     const noop =
33         event.Invites.length === cachedInviteTokens.length &&
34         event.Invites.every(({ InviteToken }) => cachedInviteTokens.includes(InviteToken));
36     if (noop) return;
38     logger.info(`[ServerEvents::Invites] ${event.Invites.length} new invite(s) received`);
40     const vaults = (yield select(selectAllVaults)) as Share<ShareType.Vault>[];
41     const vaultIds = vaults.map(prop('vaultId'));
43     const invites: MaybeNull<Invite>[] = yield Promise.all(
44         event.Invites.map<Promise<MaybeNull<Invite>>>(async (invite) => {
45             /* Filter out invites that were just accepted. This is necessary
46              * because there might be a slight delay between invite acceptance
47              * and database replication, potentially causing accepted invites
48              * to still appear in the results. */
49             if (vaultIds.includes(invite.TargetID)) return null;
51             /* if invite already decrypted early return */
52             const cachedInvite = cachedInvites[invite.InviteToken];
53             if (cachedInvite) return cachedInvite;
55             /* FIXME: support item invites */
56             const encryptedVault = invite.VaultData;
57             if (!encryptedVault) return null;
59             const inviteKey = invite.Keys.find((key) => key.KeyRotation === encryptedVault.ContentKeyRotation);
60             if (!inviteKey) return null;
62             try {
63                 const encodedVault = await PassCrypto.readVaultInvite({
64                     encryptedVaultContent: encryptedVault.Content,
65                     invitedAddressId: invite.InvitedAddressID!,
66                     inviteKey: inviteKey,
67                     inviterPublicKeys: await getPublicKeysForEmail(invite.InviterEmail),
68                 });
70                 return {
71                     createTime: invite.CreateTime,
72                     invitedAddressId: invite.InvitedAddressID!,
73                     invitedEmail: invite.InvitedEmail,
74                     inviterEmail: invite.InviterEmail,
75                     fromNewUser: invite.FromNewUser,
76                     keys: invite.Keys,
77                     remindersSent: invite.RemindersSent,
78                     targetId: invite.TargetID,
79                     targetType: invite.TargetType,
80                     token: invite.InviteToken,
81                     vault: {
82                         content: decodeVaultContent(encodedVault),
83                         memberCount: encryptedVault.MemberCount!,
84                         itemCount: encryptedVault.ItemCount!,
85                     },
86                 };
87             } catch (err: unknown) {
88                 logger.warn(`[${NAMESPACE}] Could not decrypt invite "${logId(invite.InviteToken)}"`, err);
89                 return null;
90             }
91         })
92     );
94     yield put(syncInvites(toMap(invites.filter(truthy), 'token')));
97 export const createInvitesChannel = (api: Api) =>
98     eventChannelFactory<InvitesGetResponse>({
99         api,
100         channelId: 'invites',
101         initialEventID: NOOP_EVENT,
102         getCursor: () => ({ EventID: NOOP_EVENT, More: false }),
103         query: () => ({ url: `pass/v1/invite`, method: 'get' }),
104         onEvent: onInvitesEvent,
105         onClose: () => logger.info(`[${NAMESPACE}] closing channel`),
106     });
108 export function* invitesChannel(api: Api, options: RootSagaOptions) {
109     logger.info(`[${NAMESPACE}] start polling`);
111     const eventsChannel = createInvitesChannel(api);
112     const events = fork(channelEventsWorker<InvitesGetResponse>, eventsChannel, options);
113     const wakeup = fork(channelInitWorker<InvitesGetResponse>, eventsChannel, options);
115     yield all([events, wakeup]);