1 /* eslint-disable @typescript-eslint/no-throw-literal, curly */
2 import { all, fork, put, select } from 'redux-saga/effects';
4 import { PassCrypto } from '@proton/pass/lib/crypto';
5 import type { EventCursor, EventManagerEvent } from '@proton/pass/lib/events/manager';
6 import { getUserAccessIntent, getUserFeaturesIntent, syncIntent, userEvent } from '@proton/pass/store/actions';
7 import { getOrganizationSettings } from '@proton/pass/store/actions/creators/organization';
8 import { withRevalidate } from '@proton/pass/store/request/enhancers';
9 import { SyncType } from '@proton/pass/store/sagas/client/sync';
10 import { selectAllAddresses, selectLatestEventId, selectUserSettings } from '@proton/pass/store/selectors';
11 import type { RootSagaOptions } from '@proton/pass/store/types';
12 import type { MaybeNull, UserEvent } from '@proton/pass/types';
13 import { type Api } from '@proton/pass/types';
14 import { prop } from '@proton/pass/utils/fp/lens';
15 import { notIn } from '@proton/pass/utils/fp/predicates';
16 import { logId, logger } from '@proton/pass/utils/logger';
17 import { getEvents, getLatestID } from '@proton/shared/lib/api/events';
18 import type { Address, UserSettings } from '@proton/shared/lib/interfaces';
19 import identity from '@proton/utils/identity';
21 import { eventChannelFactory } from './channel.factory';
22 import { channelEventsWorker, channelInitWorker } from './channel.worker';
23 import type { EventChannel } from './types';
25 function* onUserEvent(
26 event: EventManagerEvent<UserEvent>,
27 _: EventChannel<UserEvent>,
28 { getAuthStore, getTelemetry, onLocaleUpdated }: RootSagaOptions
30 const telemetry = getTelemetry();
31 if ('error' in event) throw event.error;
33 const currentEventId = (yield select(selectLatestEventId)) as MaybeNull<string>;
34 const userId = getAuthStore().getUserID()!;
35 const userSettings: MaybeNull<UserSettings> = yield select(selectUserSettings);
37 /* dispatch only if there was a change */
38 if (currentEventId !== event.EventID) {
39 yield put(userEvent(event));
40 logger.info(`[ServerEvents::User] event ${logId(event.EventID!)}`);
43 const { User: user } = event;
45 if (event.UserSettings && telemetry) {
46 const { Telemetry } = event.UserSettings;
47 if (Telemetry !== userSettings?.Telemetry) telemetry[Telemetry === 1 ? 'start' : 'stop']();
50 if (event.UserSettings?.Locale) {
51 const { Locale } = event.UserSettings;
52 if (Locale !== userSettings?.Locale) yield onLocaleUpdated?.(Locale);
55 /* if we get the user model from the event, check if
56 * any new active user keys are available. We might be
57 * dealing with a user re-activating a disabled user key
58 * in which case we want to trigger a full data sync in
59 * order to access any previously inactive shares */
61 const localUserKeyIds = (PassCrypto.getContext().userKeys ?? []).map(prop('ID'));
62 const activeUserKeys = user.Keys.filter(({ Active }) => Active === 1);
65 activeUserKeys.length !== localUserKeyIds.length ||
66 activeUserKeys.some(({ ID }) => notIn(localUserKeyIds)(ID));
69 logger.info(`[ServerEvents::User] Detected user keys update`);
70 const keyPassword = getAuthStore().getPassword() ?? '';
71 const addresses = (yield select(selectAllAddresses)) as Address[];
72 yield PassCrypto.hydrate({ user, keyPassword, addresses, clear: false });
73 yield put(syncIntent(SyncType.FULL)); /* trigger a full data sync */
77 /* if the subscription/invoice changes, refetch the user Plan and check Organization */
78 const revalidateUserAccess = event.Subscription || event.Invoices;
80 /* Synchronize user access, feature flags & organization whenever polling
81 * for core user events. These actions are throttled via `maxAge` metadata */
82 yield put((revalidateUserAccess ? withRevalidate : identity)(getUserAccessIntent(userId)));
83 yield put(getUserFeaturesIntent(userId));
84 yield put(getOrganizationSettings.intent());
87 export const createUserChannel = (api: Api, eventID: string) =>
88 eventChannelFactory<UserEvent>({
91 initialEventID: eventID,
93 getCursor: ({ EventID, More }) => ({ EventID, More: Boolean(More) }),
94 getLatestEventID: () => api<EventCursor>(getLatestID()).then(({ EventID }) => EventID),
96 onClose: () => logger.info(`[ServerEvents::User] closing channel`),
99 export function* userChannel(api: Api, options: RootSagaOptions) {
100 logger.info(`[ServerEvents::User] start polling for user events`);
102 const eventID: string = ((yield select(selectLatestEventId)) as ReturnType<typeof selectLatestEventId>) ?? '';
103 const eventsChannel = createUserChannel(api, eventID);
104 const events = fork(channelEventsWorker<UserEvent>, eventsChannel, options);
105 const wakeup = fork(channelInitWorker<UserEvent>, eventsChannel, options);
107 yield all([events, wakeup]);