@headless-lms/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +24 -18
- package/dist/assets/file-urls.d.ts +11 -7
- package/dist/assets/file-urls.js +14 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +4 -3
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +37 -17
- package/dist/assets/service.d.ts +30 -15
- package/dist/assets/service.js +229 -59
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +137 -65
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +20 -20
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +36 -20
- package/dist/automations/community-mail.js +32 -24
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +12 -4
- package/dist/automations/index.js +9 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +346 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +96 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +276 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +35 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +98 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +29 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +560 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +134 -21
- package/dist/automations/service.d.ts +30 -30
- package/dist/automations/service.js +111 -144
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +437 -46
- package/dist/content/events.js +45 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +8 -4
- package/dist/content/index.js +5 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +37 -0
- package/dist/content/notification-categories.js +73 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +132 -68
- package/dist/content/service.d.ts +100 -68
- package/dist/content/service.js +703 -146
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +25 -0
- package/dist/content/titles.js +82 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +63 -39
- package/dist/discussion/chats.js +455 -190
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +227 -23
- package/dist/discussion/events.js +114 -19
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -4
- package/dist/discussion/index.js +9 -2
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +279 -114
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +60 -44
- package/dist/discussion/posts.js +453 -287
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +50 -35
- package/dist/discussion/service.js +333 -205
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +115 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +28 -0
- package/dist/discussion/titles.js +98 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +3 -2
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +68 -38
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +43 -28
- package/dist/entitlements/service.js +387 -160
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +109 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/index.d.ts +6 -5
- package/dist/identity/index.js +4 -3
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +114 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +80 -56
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +17 -13
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +33 -16
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +45 -0
- package/dist/organizations/effective-permissions.js +113 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +320 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +15 -10
- package/dist/organizations/index.js +8 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +34 -0
- package/dist/organizations/notification-categories.js +66 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +104 -35
- package/dist/organizations/service.js +1247 -323
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +23 -8
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +80 -26
- package/dist/reporting/member/ports.d.ts +50 -28
- package/dist/reporting/member/service.d.ts +81 -35
- package/dist/reporting/member/service.js +438 -145
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +22 -3
- package/dist/shared/errors.d.ts +15 -2
- package/dist/shared/errors.js +21 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +7 -0
- package/dist/shared/id.js +8 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +68 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +119 -6
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +14 -17
- package/dist/types/index.d.ts +13 -12
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +596 -11
- package/dist/types/schemas/content.js +225 -9
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +362 -66
- package/dist/types/schemas/discussion.js +123 -15
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +14 -1
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -7
- package/dist/types/schemas/index.js +8 -7
- package/dist/types/schemas/notifications.d.ts +96 -0
- package/dist/types/schemas/notifications.js +57 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +96 -60
- package/dist/types/schemas/organizations.js +78 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +0 -1
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Logger } from "../../types/ports.js";
|
|
2
|
+
import { DeliveryDispatch } from "../../types/automations.js";
|
|
3
|
+
import "../../types/index.js";
|
|
4
|
+
import { ChannelRegistry } from "../../shared/ports.js";
|
|
5
|
+
import { DevicesService } from "../../identity/ports.js";
|
|
6
|
+
import "../../identity/index.js";
|
|
7
|
+
import { NotificationsRepository } from "../ports.js";
|
|
8
|
+
//#region src/automations/notifications/delivery.d.ts
|
|
9
|
+
declare const CHANNEL_NOT_CONFIGURED = "channel not configured";
|
|
10
|
+
declare const NO_ADDRESS = "no address";
|
|
11
|
+
declare const NOT_ACCEPTED = "not accepted";
|
|
12
|
+
type NotificationDeliveryParams = {
|
|
13
|
+
repo: Pick<NotificationsRepository, 'findDelivery' | 'markSent' | 'recordError' | 'markFailed'>;
|
|
14
|
+
channels: Pick<ChannelRegistry, 'get'>;
|
|
15
|
+
devices: Pick<DevicesService, 'removeDead'>;
|
|
16
|
+
logger?: Logger;
|
|
17
|
+
};
|
|
18
|
+
declare class NotificationDelivery {
|
|
19
|
+
private readonly repo;
|
|
20
|
+
private readonly channels;
|
|
21
|
+
private readonly devices;
|
|
22
|
+
private readonly logger;
|
|
23
|
+
constructor(params: NotificationDeliveryParams);
|
|
24
|
+
run(d: DeliveryDispatch, lastAttempt: boolean): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery, NotificationDeliveryParams };
|
|
28
|
+
//# sourceMappingURL=delivery.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { noopLogger } from "../../shared/logger.js";
|
|
2
|
+
//#region src/automations/notifications/delivery.ts
|
|
3
|
+
const CHANNEL_NOT_CONFIGURED = "channel not configured";
|
|
4
|
+
const NO_ADDRESS = "no address";
|
|
5
|
+
const NOT_ACCEPTED = "not accepted";
|
|
6
|
+
function messageOf(err) {
|
|
7
|
+
return err instanceof Error ? err.message : String(err);
|
|
8
|
+
}
|
|
9
|
+
var NotificationDelivery = class {
|
|
10
|
+
repo;
|
|
11
|
+
channels;
|
|
12
|
+
devices;
|
|
13
|
+
logger;
|
|
14
|
+
constructor(params) {
|
|
15
|
+
this.repo = params.repo;
|
|
16
|
+
this.channels = params.channels;
|
|
17
|
+
this.devices = params.devices;
|
|
18
|
+
this.logger = params.logger ?? noopLogger;
|
|
19
|
+
}
|
|
20
|
+
async run(d, lastAttempt) {
|
|
21
|
+
const found = await this.repo.findDelivery(d.orgId, d.deliveryId);
|
|
22
|
+
if (!found || found.delivery.status !== "pending") {
|
|
23
|
+
this.logger.debug("notification delivery skipped", {
|
|
24
|
+
orgId: d.orgId,
|
|
25
|
+
deliveryId: d.deliveryId,
|
|
26
|
+
status: found?.delivery.status ?? null
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { delivery, notification } = found;
|
|
31
|
+
const log = {
|
|
32
|
+
orgId: d.orgId,
|
|
33
|
+
deliveryId: d.deliveryId,
|
|
34
|
+
notificationId: notification.id,
|
|
35
|
+
channel: delivery.channel,
|
|
36
|
+
type: notification.type
|
|
37
|
+
};
|
|
38
|
+
const channel = this.channels.get(delivery.channel);
|
|
39
|
+
if (!channel) {
|
|
40
|
+
await this.repo.markFailed(d.orgId, d.deliveryId, CHANNEL_NOT_CONFIGURED);
|
|
41
|
+
this.logger.warn("notification delivery failed: channel not configured", log);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let to;
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
to = await channel.recipient(d.orgId, notification.userId);
|
|
48
|
+
if (!to) {
|
|
49
|
+
await this.repo.markFailed(d.orgId, d.deliveryId, NO_ADDRESS);
|
|
50
|
+
this.logger.info("notification delivery failed: no address", log);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
result = await channel.deliver(notification, to);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const error = messageOf(err);
|
|
56
|
+
if (lastAttempt) {
|
|
57
|
+
await this.repo.markFailed(d.orgId, d.deliveryId, error);
|
|
58
|
+
this.logger.warn("notification delivery failed on its last attempt", {
|
|
59
|
+
...log,
|
|
60
|
+
err
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await this.repo.recordError(d.orgId, d.deliveryId, error);
|
|
65
|
+
this.logger.warn("notification delivery attempt failed", {
|
|
66
|
+
...log,
|
|
67
|
+
err
|
|
68
|
+
});
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
if (result.deadDeviceIds.length > 0) await this.devices.removeDead(d.orgId, result.deadDeviceIds);
|
|
72
|
+
if (!result.accepted) {
|
|
73
|
+
const error = result.error ?? "not accepted";
|
|
74
|
+
await this.repo.markFailed(d.orgId, d.deliveryId, error);
|
|
75
|
+
this.logger.info("notification delivery failed: not accepted", {
|
|
76
|
+
...log,
|
|
77
|
+
error
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await this.repo.markSent(d.orgId, d.deliveryId, /* @__PURE__ */ new Date());
|
|
82
|
+
this.logger.info("notification delivered", log);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=delivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery.js","names":[],"sources":["../../../src/automations/notifications/delivery.ts"],"sourcesContent":["import type { DevicesService } from '../../identity/index.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport type { ChannelRegistry, Logger } from '../../shared/ports.js';\nimport type { ChannelRecipient, DeliveryDispatch, DeliveryResult } from '../../types/index.js';\nimport type { NotificationsRepository } from '../ports.js';\n\nexport const CHANNEL_NOT_CONFIGURED = 'channel not configured';\nexport const NO_ADDRESS = 'no address';\nexport const NOT_ACCEPTED = 'not accepted';\n\nexport type NotificationDeliveryParams = {\n repo: Pick<NotificationsRepository, 'findDelivery' | 'markSent' | 'recordError' | 'markFailed'>;\n channels: Pick<ChannelRegistry, 'get'>;\n devices: Pick<DevicesService, 'removeDead'>;\n logger?: Logger;\n};\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport class NotificationDelivery {\n private readonly repo: NotificationDeliveryParams['repo'];\n private readonly channels: Pick<ChannelRegistry, 'get'>;\n private readonly devices: Pick<DevicesService, 'removeDead'>;\n private readonly logger: Logger;\n\n constructor(params: NotificationDeliveryParams) {\n this.repo = params.repo;\n this.channels = params.channels;\n this.devices = params.devices;\n this.logger = params.logger ?? noopLogger;\n }\n\n async run(d: DeliveryDispatch, lastAttempt: boolean): Promise<void> {\n const found = await this.repo.findDelivery(d.orgId, d.deliveryId);\n if (!found || found.delivery.status !== 'pending') {\n this.logger.debug('notification delivery skipped', {\n orgId: d.orgId,\n deliveryId: d.deliveryId,\n status: found?.delivery.status ?? null,\n });\n return;\n }\n const { delivery, notification } = found;\n const log = {\n orgId: d.orgId,\n deliveryId: d.deliveryId,\n notificationId: notification.id,\n channel: delivery.channel,\n type: notification.type,\n };\n const channel = this.channels.get(delivery.channel);\n if (!channel) {\n await this.repo.markFailed(d.orgId, d.deliveryId, CHANNEL_NOT_CONFIGURED);\n this.logger.warn('notification delivery failed: channel not configured', log);\n return;\n }\n let to: ChannelRecipient | null;\n let result: DeliveryResult;\n try {\n to = await channel.recipient(d.orgId, notification.userId);\n if (!to) {\n await this.repo.markFailed(d.orgId, d.deliveryId, NO_ADDRESS);\n this.logger.info('notification delivery failed: no address', log);\n return;\n }\n result = await channel.deliver(notification, to);\n } catch (err) {\n const error = messageOf(err);\n if (lastAttempt) {\n await this.repo.markFailed(d.orgId, d.deliveryId, error);\n this.logger.warn('notification delivery failed on its last attempt', { ...log, err });\n return;\n }\n await this.repo.recordError(d.orgId, d.deliveryId, error);\n this.logger.warn('notification delivery attempt failed', { ...log, err });\n throw err;\n }\n if (result.deadDeviceIds.length > 0) {\n await this.devices.removeDead(d.orgId, result.deadDeviceIds);\n }\n if (!result.accepted) {\n const error = result.error ?? NOT_ACCEPTED;\n await this.repo.markFailed(d.orgId, d.deliveryId, error);\n this.logger.info('notification delivery failed: not accepted', { ...log, error });\n return;\n }\n await this.repo.markSent(d.orgId, d.deliveryId, new Date());\n this.logger.info('notification delivered', log);\n }\n}\n"],"mappings":";;AAMA,MAAa,yBAAyB;AACtC,MAAa,aAAa;AAC1B,MAAa,eAAe;AAS5B,SAAS,UAAU,KAAsB;CACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,IAAa,uBAAb,MAAkC;CAChC;CACA;CACA;CACA;CAEA,YAAY,QAAoC;EAC9C,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW,OAAO;EACvB,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,IAAI,GAAqB,aAAqC;EAClE,MAAM,QAAQ,MAAM,KAAK,KAAK,aAAa,EAAE,OAAO,EAAE,UAAU;EAChE,IAAI,CAAC,SAAS,MAAM,SAAS,WAAW,WAAW;GACjD,KAAK,OAAO,MAAM,iCAAiC;IACjD,OAAO,EAAE;IACT,YAAY,EAAE;IACd,QAAQ,OAAO,SAAS,UAAU;GACpC,CAAC;GACD;EACF;EACA,MAAM,EAAE,UAAU,iBAAiB;EACnC,MAAM,MAAM;GACV,OAAO,EAAE;GACT,YAAY,EAAE;GACd,gBAAgB,aAAa;GAC7B,SAAS,SAAS;GAClB,MAAM,aAAa;EACrB;EACA,MAAM,UAAU,KAAK,SAAS,IAAI,SAAS,OAAO;EAClD,IAAI,CAAC,SAAS;GACZ,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,sBAAsB;GACxE,KAAK,OAAO,KAAK,wDAAwD,GAAG;GAC5E;EACF;EACA,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,KAAK,MAAM,QAAQ,UAAU,EAAE,OAAO,aAAa,MAAM;GACzD,IAAI,CAAC,IAAI;IACP,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,UAAU;IAC5D,KAAK,OAAO,KAAK,4CAA4C,GAAG;IAChE;GACF;GACA,SAAS,MAAM,QAAQ,QAAQ,cAAc,EAAE;EACjD,SAAS,KAAK;GACZ,MAAM,QAAQ,UAAU,GAAG;GAC3B,IAAI,aAAa;IACf,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK;IACvD,KAAK,OAAO,KAAK,oDAAoD;KAAE,GAAG;KAAK;IAAI,CAAC;IACpF;GACF;GACA,MAAM,KAAK,KAAK,YAAY,EAAE,OAAO,EAAE,YAAY,KAAK;GACxD,KAAK,OAAO,KAAK,wCAAwC;IAAE,GAAG;IAAK;GAAI,CAAC;GACxE,MAAM;EACR;EACA,IAAI,OAAO,cAAc,SAAS,GAChC,MAAM,KAAK,QAAQ,WAAW,EAAE,OAAO,OAAO,aAAa;EAE7D,IAAI,CAAC,OAAO,UAAU;GACpB,MAAM,QAAQ,OAAO,SAAA;GACrB,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK;GACvD,KAAK,OAAO,KAAK,8CAA8C;IAAE,GAAG;IAAK;GAAM,CAAC;GAChF;EACF;EACA,MAAM,KAAK,KAAK,SAAS,EAAE,OAAO,EAAE,4BAAY,IAAI,KAAK,CAAC;EAC1D,KAAK,OAAO,KAAK,0BAA0B,GAAG;CAChD;AACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DomainEvent } from "../../types/shared.js";
|
|
2
|
+
import { Logger } from "../../types/ports.js";
|
|
3
|
+
import { AutomationEngine } from "../../types/automations.js";
|
|
4
|
+
import { RecipientList } from "../../types/notifications.js";
|
|
5
|
+
import "../../types/index.js";
|
|
6
|
+
import { ChannelRegistry, NotificationTypeRegistry } from "../../shared/ports.js";
|
|
7
|
+
import { DevicesService, NotificationSettingsService } from "../../identity/ports.js";
|
|
8
|
+
import "../../identity/index.js";
|
|
9
|
+
import { ChatDebounce, NotificationCategories, NotificationReads, NotificationsRepository, RecipientRule } from "../ports.js";
|
|
10
|
+
//#region src/automations/notifications/processor.d.ts
|
|
11
|
+
declare const IN_APP = "inApp";
|
|
12
|
+
declare const PUSH = "push";
|
|
13
|
+
declare const MESSAGE_SENT = "message.sent";
|
|
14
|
+
declare const DEVICE_WINDOW_MS: number;
|
|
15
|
+
type NotificationProcessorParams = {
|
|
16
|
+
rules: Record<string, RecipientRule>;
|
|
17
|
+
reads: NotificationReads;
|
|
18
|
+
types: Pick<NotificationTypeRegistry, 'get'>;
|
|
19
|
+
channels: Pick<ChannelRegistry, 'list' | 'get'>;
|
|
20
|
+
categories: NotificationCategories;
|
|
21
|
+
settings: Pick<NotificationSettingsService, 'getMany'>;
|
|
22
|
+
devices: Pick<DevicesService, 'withActiveDevices'>;
|
|
23
|
+
repo: Pick<NotificationsRepository, 'insert' | 'pendingDeliveriesForEvent'>;
|
|
24
|
+
engine: Pick<AutomationEngine, 'deliver'>;
|
|
25
|
+
debounce?: ChatDebounce;
|
|
26
|
+
now?: () => Date;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
};
|
|
29
|
+
declare function chatOf(refs: Pick<RecipientList, 'subject' | 'parent'>): string | null;
|
|
30
|
+
declare class NotificationProcessor {
|
|
31
|
+
private readonly rules;
|
|
32
|
+
private readonly reads;
|
|
33
|
+
private readonly types;
|
|
34
|
+
private readonly channels;
|
|
35
|
+
private readonly categories;
|
|
36
|
+
private readonly settings;
|
|
37
|
+
private readonly devices;
|
|
38
|
+
private readonly repo;
|
|
39
|
+
private readonly engine;
|
|
40
|
+
private readonly debounce;
|
|
41
|
+
private readonly now;
|
|
42
|
+
private readonly logger;
|
|
43
|
+
constructor(params: NotificationProcessorParams);
|
|
44
|
+
process(event: DomainEvent): Promise<void>;
|
|
45
|
+
file(orgId: string, sourceEventId: string, lists: RecipientList[], options: {
|
|
46
|
+
occurredAt: Date;
|
|
47
|
+
debounce: boolean;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
private isValid;
|
|
50
|
+
private groups;
|
|
51
|
+
private categoriesOf;
|
|
52
|
+
private expand;
|
|
53
|
+
private collect;
|
|
54
|
+
private match;
|
|
55
|
+
private peopleWithDevice;
|
|
56
|
+
private fileOne;
|
|
57
|
+
private filed;
|
|
58
|
+
private coveredPairs;
|
|
59
|
+
private mentionsInChats;
|
|
60
|
+
private dispatch;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, NotificationProcessorParams, PUSH, chatOf };
|
|
64
|
+
//# sourceMappingURL=processor.d.ts.map
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { noopLogger } from "../../shared/logger.js";
|
|
2
|
+
import { NotFoundError } from "../../shared/errors.js";
|
|
3
|
+
import { genId } from "../../shared/id.js";
|
|
4
|
+
import { resolveChannels } from "../../shared/notification-settings.js";
|
|
5
|
+
//#region src/automations/notifications/processor.ts
|
|
6
|
+
const IN_APP = "inApp";
|
|
7
|
+
const PUSH = "push";
|
|
8
|
+
const MESSAGE_SENT = "message.sent";
|
|
9
|
+
const DEVICE_WINDOW_MS = 720 * 60 * 60 * 1e3;
|
|
10
|
+
const noChatDebounce = {
|
|
11
|
+
arm: async () => {},
|
|
12
|
+
mentioned: async () => {}
|
|
13
|
+
};
|
|
14
|
+
function sameKeys(a, b) {
|
|
15
|
+
const left = new Set(a);
|
|
16
|
+
const right = new Set(b);
|
|
17
|
+
return left.size === right.size && [...left].every((key) => right.has(key));
|
|
18
|
+
}
|
|
19
|
+
function chatOf(refs) {
|
|
20
|
+
if (refs.parent?.type === "chat") return refs.parent.id;
|
|
21
|
+
return refs.subject?.type === "chat" ? refs.subject.id : null;
|
|
22
|
+
}
|
|
23
|
+
var NotificationProcessor = class {
|
|
24
|
+
rules;
|
|
25
|
+
reads;
|
|
26
|
+
types;
|
|
27
|
+
channels;
|
|
28
|
+
categories;
|
|
29
|
+
settings;
|
|
30
|
+
devices;
|
|
31
|
+
repo;
|
|
32
|
+
engine;
|
|
33
|
+
debounce;
|
|
34
|
+
now;
|
|
35
|
+
logger;
|
|
36
|
+
constructor(params) {
|
|
37
|
+
this.rules = params.rules;
|
|
38
|
+
this.reads = params.reads;
|
|
39
|
+
this.types = params.types;
|
|
40
|
+
this.channels = params.channels;
|
|
41
|
+
this.categories = params.categories;
|
|
42
|
+
this.settings = params.settings;
|
|
43
|
+
this.devices = params.devices;
|
|
44
|
+
this.repo = params.repo;
|
|
45
|
+
this.engine = params.engine;
|
|
46
|
+
this.debounce = params.debounce ?? noChatDebounce;
|
|
47
|
+
this.now = params.now ?? (() => /* @__PURE__ */ new Date());
|
|
48
|
+
this.logger = params.logger ?? noopLogger;
|
|
49
|
+
}
|
|
50
|
+
async process(event) {
|
|
51
|
+
const rule = Object.prototype.hasOwnProperty.call(this.rules, event.type) ? this.rules[event.type] : void 0;
|
|
52
|
+
if (!rule) return;
|
|
53
|
+
const lists = await rule(event, this.reads);
|
|
54
|
+
await this.file(event.orgId, event.id, lists, {
|
|
55
|
+
occurredAt: new Date(event.occurredAt),
|
|
56
|
+
debounce: true
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async file(orgId, sourceEventId, lists, options) {
|
|
60
|
+
const valid = lists.filter((list) => this.isValid(orgId, sourceEventId, list));
|
|
61
|
+
const filed = [];
|
|
62
|
+
for (const group of this.groups(valid)) {
|
|
63
|
+
const categories = await this.categoriesOf(orgId, sourceEventId, group.communityId);
|
|
64
|
+
if (!categories) continue;
|
|
65
|
+
const expanded = [];
|
|
66
|
+
for (const list of group.lists) {
|
|
67
|
+
const ready = list.audience ? await this.expand(orgId, list, categories) : list;
|
|
68
|
+
if (options.debounce && ready.type === "message.sent") {
|
|
69
|
+
await this.debounce.arm(orgId, ready, options.occurredAt);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
expanded.push(ready);
|
|
73
|
+
}
|
|
74
|
+
filed.push(...await this.match(orgId, sourceEventId, group.communityId, expanded, categories));
|
|
75
|
+
}
|
|
76
|
+
if (filed.length > 0) {
|
|
77
|
+
await this.repo.insert(orgId, filed.map((entry) => entry.notification), filed.flatMap((entry) => entry.deliveries));
|
|
78
|
+
this.logger.info("notifications filed", {
|
|
79
|
+
orgId,
|
|
80
|
+
sourceEventId,
|
|
81
|
+
count: filed.length,
|
|
82
|
+
types: [...new Set(filed.map((entry) => entry.notification.type))]
|
|
83
|
+
});
|
|
84
|
+
await this.mentionsInChats(orgId, filed);
|
|
85
|
+
}
|
|
86
|
+
await this.dispatch(orgId, sourceEventId);
|
|
87
|
+
}
|
|
88
|
+
isValid(orgId, sourceEventId, list) {
|
|
89
|
+
const drop = (reason) => {
|
|
90
|
+
this.logger.error("notification recipient list dropped", {
|
|
91
|
+
orgId,
|
|
92
|
+
sourceEventId,
|
|
93
|
+
type: list.type,
|
|
94
|
+
scope: list.scope,
|
|
95
|
+
reason
|
|
96
|
+
});
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
const declaration = this.types.get(list.type);
|
|
100
|
+
if (!declaration) return drop("unknown type");
|
|
101
|
+
if (!declaration.scopes.includes(list.scope)) return drop("scope not declared");
|
|
102
|
+
const keys = declaration.display[list.scope] ?? [];
|
|
103
|
+
if (!sameKeys(Object.keys(list.display), keys)) return drop("display keys do not match the declaration");
|
|
104
|
+
if (list.recipients === void 0 === (list.audience === void 0)) return drop("a list needs either recipients or an audience");
|
|
105
|
+
if ((list.audience ? [list.audience.reason] : (list.recipients ?? []).flatMap((recipient) => recipient.reasons)).some((reason) => !declaration.reasons.includes(reason))) return drop("reason not allowed for the type");
|
|
106
|
+
if (list.audience && list.communityId === null) return drop("an audience needs a community");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
groups(lists) {
|
|
110
|
+
const byCommunity = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const list of lists) {
|
|
112
|
+
const group = byCommunity.get(list.communityId) ?? {
|
|
113
|
+
communityId: list.communityId,
|
|
114
|
+
lists: []
|
|
115
|
+
};
|
|
116
|
+
group.lists.push(list);
|
|
117
|
+
byCommunity.set(list.communityId, group);
|
|
118
|
+
}
|
|
119
|
+
return [...byCommunity.values()];
|
|
120
|
+
}
|
|
121
|
+
async categoriesOf(orgId, sourceEventId, communityId) {
|
|
122
|
+
try {
|
|
123
|
+
return await this.categories.listFor(orgId, communityId);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (!(err instanceof NotFoundError)) throw err;
|
|
126
|
+
this.logger.warn("notification group dropped: the community no longer exists", {
|
|
127
|
+
orgId,
|
|
128
|
+
sourceEventId,
|
|
129
|
+
communityId
|
|
130
|
+
});
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async expand(orgId, list, categories) {
|
|
135
|
+
const audience = list.audience;
|
|
136
|
+
const communityId = list.communityId;
|
|
137
|
+
const category = categories.find((candidate) => candidate.covers.some((cover) => cover.type === list.type && cover.reasons.includes(audience.reason)));
|
|
138
|
+
let orgUserIds = [];
|
|
139
|
+
if (category) if (resolveChannels(category, [], audience.spaceId, this.channels.list()).some((channel) => channel.enabled)) orgUserIds = await this.reads.spaceOpeners(orgId, audience.spaceId);
|
|
140
|
+
else {
|
|
141
|
+
const switched = await this.reads.switchedOn(orgId, communityId, category.key, audience.spaceId);
|
|
142
|
+
orgUserIds = switched.length > 0 ? await this.reads.spaceOpenersAmong(orgId, audience.spaceId, switched) : [];
|
|
143
|
+
}
|
|
144
|
+
const excluded = new Set(audience.exclude);
|
|
145
|
+
const candidates = [...new Set(orgUserIds)].filter((id) => !excluded.has(id));
|
|
146
|
+
const muted = candidates.length > 0 ? new Set(await this.reads.mutedAmong(orgId, {
|
|
147
|
+
type: "space",
|
|
148
|
+
id: audience.spaceId
|
|
149
|
+
}, candidates, this.now())) : /* @__PURE__ */ new Set();
|
|
150
|
+
const kept = candidates.filter((id) => !muted.has(id));
|
|
151
|
+
const userIds = kept.length > 0 ? await this.reads.userIdsOf(orgId, kept) : {};
|
|
152
|
+
const recipients = kept.flatMap((orgUserId) => {
|
|
153
|
+
const userId = userIds[orgUserId];
|
|
154
|
+
return userId ? [{
|
|
155
|
+
orgUserId,
|
|
156
|
+
userId,
|
|
157
|
+
reasons: [audience.reason]
|
|
158
|
+
}] : [];
|
|
159
|
+
});
|
|
160
|
+
const { audience: _audience, ...rest } = list;
|
|
161
|
+
return {
|
|
162
|
+
...rest,
|
|
163
|
+
recipients
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
collect(lists) {
|
|
167
|
+
const people = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const list of lists) for (const recipient of list.recipients ?? []) {
|
|
169
|
+
const person = people.get(recipient.userId) ?? {
|
|
170
|
+
orgUserId: recipient.orgUserId,
|
|
171
|
+
userId: recipient.userId,
|
|
172
|
+
pairs: []
|
|
173
|
+
};
|
|
174
|
+
person.pairs.push({
|
|
175
|
+
list,
|
|
176
|
+
type: list.type,
|
|
177
|
+
reasons: [...new Set(recipient.reasons)]
|
|
178
|
+
});
|
|
179
|
+
people.set(recipient.userId, person);
|
|
180
|
+
}
|
|
181
|
+
return [...people.values()];
|
|
182
|
+
}
|
|
183
|
+
async match(orgId, sourceEventId, communityId, lists, categories) {
|
|
184
|
+
const people = this.collect(lists);
|
|
185
|
+
if (people.length === 0 || categories.length === 0) return [];
|
|
186
|
+
const settings = await this.settings.getMany(orgId, people.map((person) => person.userId), communityId ? { communityId } : "org", categories.map((category) => category.key));
|
|
187
|
+
const configured = this.channels.list();
|
|
188
|
+
const withDevice = await this.peopleWithDevice(orgId, people, categories, settings, configured);
|
|
189
|
+
const filed = [];
|
|
190
|
+
for (const person of people) {
|
|
191
|
+
const entry = this.fileOne(sourceEventId, communityId, person, categories, settings[person.userId] ?? [], configured, withDevice.has(person.userId));
|
|
192
|
+
if (entry) filed.push(entry);
|
|
193
|
+
}
|
|
194
|
+
return filed;
|
|
195
|
+
}
|
|
196
|
+
async peopleWithDevice(orgId, people, categories, settings, configured) {
|
|
197
|
+
const wanting = people.filter((person) => categories.some((category) => this.coveredPairs(category, person.pairs).some(({ pair }) => resolveChannels(category, settings[person.userId] ?? [], pair.list.spaceId, configured).some((channel) => channel.enabled && channel.channel === "push"))));
|
|
198
|
+
if (wanting.length === 0) return /* @__PURE__ */ new Set();
|
|
199
|
+
const since = /* @__PURE__ */ new Date(this.now().getTime() - DEVICE_WINDOW_MS);
|
|
200
|
+
return new Set(await this.devices.withActiveDevices(orgId, wanting.map((person) => person.userId), since));
|
|
201
|
+
}
|
|
202
|
+
fileOne(sourceEventId, communityId, person, categories, settings, configured, hasDevice) {
|
|
203
|
+
for (const category of categories) for (const { pair, cover } of this.coveredPairs(category, person.pairs)) {
|
|
204
|
+
let on = resolveChannels(category, settings, pair.list.spaceId, configured).filter((channel) => channel.enabled).map((channel) => channel.channel);
|
|
205
|
+
if (!hasDevice) on = on.filter((channel) => channel !== PUSH);
|
|
206
|
+
if (on.length > 0) return this.filed(sourceEventId, communityId, person, category, pair, cover, on);
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
filed(sourceEventId, communityId, person, category, pair, cover, on) {
|
|
211
|
+
const list = pair.list;
|
|
212
|
+
const notificationId = genId("notification");
|
|
213
|
+
const reasons = [...new Set(person.pairs.flatMap((candidate) => candidate.reasons))];
|
|
214
|
+
return {
|
|
215
|
+
notification: {
|
|
216
|
+
id: notificationId,
|
|
217
|
+
userId: person.userId,
|
|
218
|
+
sourceEventId,
|
|
219
|
+
type: pair.type,
|
|
220
|
+
scope: list.scope,
|
|
221
|
+
reasons,
|
|
222
|
+
matchedReason: cover.reasons.find((reason) => pair.reasons.includes(reason)),
|
|
223
|
+
category: category.key,
|
|
224
|
+
communityId,
|
|
225
|
+
spaceId: list.spaceId,
|
|
226
|
+
actorId: list.actorId,
|
|
227
|
+
subject: list.subject,
|
|
228
|
+
parent: list.parent,
|
|
229
|
+
display: list.display,
|
|
230
|
+
inApp: on.includes(IN_APP),
|
|
231
|
+
createdAt: this.now()
|
|
232
|
+
},
|
|
233
|
+
deliveries: on.filter((channel) => channel !== "inApp" && this.channels.get(channel) !== void 0).map((channel) => ({
|
|
234
|
+
id: genId("notificationDelivery"),
|
|
235
|
+
notificationId,
|
|
236
|
+
channel
|
|
237
|
+
}))
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
coveredPairs(category, pairs) {
|
|
241
|
+
const covered = [];
|
|
242
|
+
for (const pair of pairs) {
|
|
243
|
+
if (category.perSpace === true && pair.list.spaceId === null) continue;
|
|
244
|
+
const cover = category.covers.find((candidate) => candidate.type === pair.type && candidate.reasons.some((reason) => pair.reasons.includes(reason)));
|
|
245
|
+
if (cover) covered.push({
|
|
246
|
+
pair,
|
|
247
|
+
cover
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return covered;
|
|
251
|
+
}
|
|
252
|
+
async mentionsInChats(orgId, filed) {
|
|
253
|
+
for (const { notification } of filed) {
|
|
254
|
+
if (notification.type !== "mention" || notification.scope !== "chat") continue;
|
|
255
|
+
const chatId = chatOf(notification);
|
|
256
|
+
if (chatId) await this.debounce.mentioned(orgId, notification.userId, chatId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async dispatch(orgId, sourceEventId) {
|
|
260
|
+
const pending = await this.repo.pendingDeliveriesForEvent(orgId, sourceEventId);
|
|
261
|
+
for (const delivery of pending) await this.engine.deliver({
|
|
262
|
+
orgId,
|
|
263
|
+
deliveryId: delivery.id
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
//#endregion
|
|
268
|
+
export { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, PUSH, chatOf };
|
|
269
|
+
|
|
270
|
+
//# sourceMappingURL=processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processor.js","names":[],"sources":["../../../src/automations/notifications/processor.ts"],"sourcesContent":["import type { DevicesService, NotificationSettingsService } from '../../identity/index.js';\nimport { NotFoundError } from '../../shared/errors.js';\nimport { genId } from '../../shared/id.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport { resolveChannels } from '../../shared/notification-settings.js';\nimport type {\n ChannelRegistry,\n DomainEvent,\n Logger,\n NotificationTypeRegistry,\n} from '../../shared/ports.js';\nimport type {\n NotificationCategory,\n Reason,\n RecipientList,\n StoredSetting,\n} from '../../types/index.js';\nimport type {\n AutomationEngine,\n ChatDebounce,\n NewDelivery,\n NewNotification,\n NotificationCategories,\n NotificationReads,\n NotificationsRepository,\n RecipientRule,\n} from '../ports.js';\n\nexport const IN_APP = 'inApp';\nexport const PUSH = 'push';\nexport const MESSAGE_SENT = 'message.sent';\nexport const DEVICE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;\n\nconst noChatDebounce: ChatDebounce = {\n arm: async () => {},\n mentioned: async () => {},\n};\n\ntype Recipient = NonNullable<RecipientList['recipients']>[number];\n\ninterface Pair {\n list: RecipientList;\n type: string;\n reasons: Reason[];\n}\n\ninterface Person {\n orgUserId: string;\n userId: string;\n pairs: Pair[];\n}\n\ninterface Group {\n communityId: string | null;\n lists: RecipientList[];\n}\n\ninterface Filed {\n notification: NewNotification;\n deliveries: NewDelivery[];\n}\n\nexport type NotificationProcessorParams = {\n rules: Record<string, RecipientRule>;\n reads: NotificationReads;\n types: Pick<NotificationTypeRegistry, 'get'>;\n channels: Pick<ChannelRegistry, 'list' | 'get'>;\n categories: NotificationCategories;\n settings: Pick<NotificationSettingsService, 'getMany'>;\n devices: Pick<DevicesService, 'withActiveDevices'>;\n repo: Pick<NotificationsRepository, 'insert' | 'pendingDeliveriesForEvent'>;\n engine: Pick<AutomationEngine, 'deliver'>;\n debounce?: ChatDebounce;\n now?: () => Date;\n logger?: Logger;\n};\n\nfunction sameKeys(a: string[], b: string[]): boolean {\n const left = new Set(a);\n const right = new Set(b);\n return left.size === right.size && [...left].every((key) => right.has(key));\n}\n\nexport function chatOf(refs: Pick<RecipientList, 'subject' | 'parent'>): string | null {\n if (refs.parent?.type === 'chat') {\n return refs.parent.id;\n }\n return refs.subject?.type === 'chat' ? refs.subject.id : null;\n}\n\nexport class NotificationProcessor {\n private readonly rules: Record<string, RecipientRule>;\n private readonly reads: NotificationReads;\n private readonly types: Pick<NotificationTypeRegistry, 'get'>;\n private readonly channels: Pick<ChannelRegistry, 'list' | 'get'>;\n private readonly categories: NotificationCategories;\n private readonly settings: Pick<NotificationSettingsService, 'getMany'>;\n private readonly devices: Pick<DevicesService, 'withActiveDevices'>;\n private readonly repo: Pick<NotificationsRepository, 'insert' | 'pendingDeliveriesForEvent'>;\n private readonly engine: Pick<AutomationEngine, 'deliver'>;\n private readonly debounce: ChatDebounce;\n private readonly now: () => Date;\n private readonly logger: Logger;\n\n constructor(params: NotificationProcessorParams) {\n this.rules = params.rules;\n this.reads = params.reads;\n this.types = params.types;\n this.channels = params.channels;\n this.categories = params.categories;\n this.settings = params.settings;\n this.devices = params.devices;\n this.repo = params.repo;\n this.engine = params.engine;\n this.debounce = params.debounce ?? noChatDebounce;\n this.now = params.now ?? (() => new Date());\n this.logger = params.logger ?? noopLogger;\n }\n\n async process(event: DomainEvent): Promise<void> {\n const rule = Object.prototype.hasOwnProperty.call(this.rules, event.type)\n ? this.rules[event.type]\n : undefined;\n if (!rule) {\n return;\n }\n const lists = await rule(event, this.reads);\n await this.file(event.orgId, event.id, lists, {\n occurredAt: new Date(event.occurredAt),\n debounce: true,\n });\n }\n\n async file(\n orgId: string,\n sourceEventId: string,\n lists: RecipientList[],\n options: { occurredAt: Date; debounce: boolean },\n ): Promise<void> {\n const valid = lists.filter((list) => this.isValid(orgId, sourceEventId, list));\n const filed: Filed[] = [];\n for (const group of this.groups(valid)) {\n const categories = await this.categoriesOf(orgId, sourceEventId, group.communityId);\n if (!categories) {\n continue;\n }\n const expanded: RecipientList[] = [];\n for (const list of group.lists) {\n const ready = list.audience ? await this.expand(orgId, list, categories) : list;\n if (options.debounce && ready.type === MESSAGE_SENT) {\n await this.debounce.arm(orgId, ready, options.occurredAt);\n continue;\n }\n expanded.push(ready);\n }\n filed.push(\n ...(await this.match(orgId, sourceEventId, group.communityId, expanded, categories)),\n );\n }\n if (filed.length > 0) {\n await this.repo.insert(\n orgId,\n filed.map((entry) => entry.notification),\n filed.flatMap((entry) => entry.deliveries),\n );\n this.logger.info('notifications filed', {\n orgId,\n sourceEventId,\n count: filed.length,\n types: [...new Set(filed.map((entry) => entry.notification.type))],\n });\n await this.mentionsInChats(orgId, filed);\n }\n await this.dispatch(orgId, sourceEventId);\n }\n\n private isValid(orgId: string, sourceEventId: string, list: RecipientList): boolean {\n const drop = (reason: string): false => {\n this.logger.error('notification recipient list dropped', {\n orgId,\n sourceEventId,\n type: list.type,\n scope: list.scope,\n reason,\n });\n return false;\n };\n const declaration = this.types.get(list.type);\n if (!declaration) {\n return drop('unknown type');\n }\n if (!declaration.scopes.includes(list.scope)) {\n return drop('scope not declared');\n }\n const keys = declaration.display[list.scope] ?? [];\n if (!sameKeys(Object.keys(list.display), keys)) {\n return drop('display keys do not match the declaration');\n }\n if ((list.recipients === undefined) === (list.audience === undefined)) {\n return drop('a list needs either recipients or an audience');\n }\n const reasons = list.audience\n ? [list.audience.reason]\n : (list.recipients ?? []).flatMap((recipient) => recipient.reasons);\n if (reasons.some((reason) => !declaration.reasons.includes(reason))) {\n return drop('reason not allowed for the type');\n }\n if (list.audience && list.communityId === null) {\n return drop('an audience needs a community');\n }\n return true;\n }\n\n private groups(lists: RecipientList[]): Group[] {\n const byCommunity = new Map<string | null, Group>();\n for (const list of lists) {\n const group = byCommunity.get(list.communityId) ?? {\n communityId: list.communityId,\n lists: [],\n };\n group.lists.push(list);\n byCommunity.set(list.communityId, group);\n }\n return [...byCommunity.values()];\n }\n\n private async categoriesOf(\n orgId: string,\n sourceEventId: string,\n communityId: string | null,\n ): Promise<NotificationCategory[] | null> {\n try {\n return await this.categories.listFor(orgId, communityId);\n } catch (err) {\n if (!(err instanceof NotFoundError)) {\n throw err;\n }\n this.logger.warn('notification group dropped: the community no longer exists', {\n orgId,\n sourceEventId,\n communityId,\n });\n return null;\n }\n }\n\n private async expand(\n orgId: string,\n list: RecipientList,\n categories: NotificationCategory[],\n ): Promise<RecipientList> {\n const audience = list.audience!;\n const communityId = list.communityId!;\n const category = categories.find((candidate) =>\n candidate.covers.some(\n (cover) => cover.type === list.type && cover.reasons.includes(audience.reason),\n ),\n );\n let orgUserIds: string[] = [];\n if (category) {\n const onByDefault = resolveChannels(\n category,\n [],\n audience.spaceId,\n this.channels.list(),\n ).some((channel) => channel.enabled);\n if (onByDefault) {\n orgUserIds = await this.reads.spaceOpeners(orgId, audience.spaceId);\n } else {\n const switched = await this.reads.switchedOn(\n orgId,\n communityId,\n category.key,\n audience.spaceId,\n );\n orgUserIds =\n switched.length > 0\n ? await this.reads.spaceOpenersAmong(orgId, audience.spaceId, switched)\n : [];\n }\n }\n const excluded = new Set(audience.exclude);\n const candidates = [...new Set(orgUserIds)].filter((id) => !excluded.has(id));\n const muted =\n candidates.length > 0\n ? new Set(\n await this.reads.mutedAmong(\n orgId,\n { type: 'space', id: audience.spaceId },\n candidates,\n this.now(),\n ),\n )\n : new Set<string>();\n const kept = candidates.filter((id) => !muted.has(id));\n const userIds = kept.length > 0 ? await this.reads.userIdsOf(orgId, kept) : {};\n const recipients: Recipient[] = kept.flatMap((orgUserId) => {\n const userId = userIds[orgUserId];\n return userId ? [{ orgUserId, userId, reasons: [audience.reason] }] : [];\n });\n const { audience: _audience, ...rest } = list;\n return { ...rest, recipients };\n }\n\n private collect(lists: RecipientList[]): Person[] {\n const people = new Map<string, Person>();\n for (const list of lists) {\n for (const recipient of list.recipients ?? []) {\n const person = people.get(recipient.userId) ?? {\n orgUserId: recipient.orgUserId,\n userId: recipient.userId,\n pairs: [],\n };\n person.pairs.push({ list, type: list.type, reasons: [...new Set(recipient.reasons)] });\n people.set(recipient.userId, person);\n }\n }\n return [...people.values()];\n }\n\n private async match(\n orgId: string,\n sourceEventId: string,\n communityId: string | null,\n lists: RecipientList[],\n categories: NotificationCategory[],\n ): Promise<Filed[]> {\n const people = this.collect(lists);\n if (people.length === 0 || categories.length === 0) {\n return [];\n }\n const settings = await this.settings.getMany(\n orgId,\n people.map((person) => person.userId),\n communityId ? { communityId } : 'org',\n categories.map((category) => category.key),\n );\n const configured = this.channels.list();\n const withDevice = await this.peopleWithDevice(\n orgId,\n people,\n categories,\n settings,\n configured,\n );\n const filed: Filed[] = [];\n for (const person of people) {\n const entry = this.fileOne(\n sourceEventId,\n communityId,\n person,\n categories,\n settings[person.userId] ?? [],\n configured,\n withDevice.has(person.userId),\n );\n if (entry) {\n filed.push(entry);\n }\n }\n return filed;\n }\n\n private async peopleWithDevice(\n orgId: string,\n people: Person[],\n categories: NotificationCategory[],\n settings: Record<string, StoredSetting[]>,\n configured: string[],\n ): Promise<Set<string>> {\n const wanting = people.filter((person) =>\n categories.some((category) =>\n this.coveredPairs(category, person.pairs).some(({ pair }) =>\n resolveChannels(\n category,\n settings[person.userId] ?? [],\n pair.list.spaceId,\n configured,\n ).some((channel) => channel.enabled && channel.channel === PUSH),\n ),\n ),\n );\n if (wanting.length === 0) {\n return new Set();\n }\n const since = new Date(this.now().getTime() - DEVICE_WINDOW_MS);\n return new Set(\n await this.devices.withActiveDevices(\n orgId,\n wanting.map((person) => person.userId),\n since,\n ),\n );\n }\n\n private fileOne(\n sourceEventId: string,\n communityId: string | null,\n person: Person,\n categories: NotificationCategory[],\n settings: StoredSetting[],\n configured: string[],\n hasDevice: boolean,\n ): Filed | null {\n for (const category of categories) {\n for (const { pair, cover } of this.coveredPairs(category, person.pairs)) {\n let on = resolveChannels(category, settings, pair.list.spaceId, configured)\n .filter((channel) => channel.enabled)\n .map((channel) => channel.channel);\n if (!hasDevice) {\n on = on.filter((channel) => channel !== PUSH);\n }\n if (on.length > 0) {\n return this.filed(sourceEventId, communityId, person, category, pair, cover, on);\n }\n }\n }\n return null;\n }\n\n private filed(\n sourceEventId: string,\n communityId: string | null,\n person: Person,\n category: NotificationCategory,\n pair: Pair,\n cover: NotificationCategory['covers'][number],\n on: string[],\n ): Filed {\n const list = pair.list;\n const notificationId = genId('notification');\n const reasons = [...new Set(person.pairs.flatMap((candidate) => candidate.reasons))];\n return {\n notification: {\n id: notificationId,\n userId: person.userId,\n sourceEventId,\n type: pair.type,\n scope: list.scope,\n reasons,\n matchedReason: cover.reasons.find((reason) => pair.reasons.includes(reason))!,\n category: category.key,\n communityId,\n spaceId: list.spaceId,\n actorId: list.actorId,\n subject: list.subject,\n parent: list.parent,\n display: list.display,\n inApp: on.includes(IN_APP),\n createdAt: this.now(),\n },\n deliveries: on\n .filter((channel) => channel !== IN_APP && this.channels.get(channel) !== undefined)\n .map((channel) => ({\n id: genId('notificationDelivery'),\n notificationId,\n channel,\n })),\n };\n }\n\n private coveredPairs(\n category: NotificationCategory,\n pairs: Pair[],\n ): { pair: Pair; cover: NotificationCategory['covers'][number] }[] {\n const covered: { pair: Pair; cover: NotificationCategory['covers'][number] }[] = [];\n for (const pair of pairs) {\n if (category.perSpace === true && pair.list.spaceId === null) {\n continue;\n }\n const cover = category.covers.find(\n (candidate) =>\n candidate.type === pair.type &&\n candidate.reasons.some((reason) => pair.reasons.includes(reason)),\n );\n if (cover) {\n covered.push({ pair, cover });\n }\n }\n return covered;\n }\n\n private async mentionsInChats(orgId: string, filed: Filed[]): Promise<void> {\n for (const { notification } of filed) {\n if (notification.type !== 'mention' || notification.scope !== 'chat') {\n continue;\n }\n const chatId = chatOf(notification);\n if (chatId) {\n await this.debounce.mentioned(orgId, notification.userId, chatId);\n }\n }\n }\n\n private async dispatch(orgId: string, sourceEventId: string): Promise<void> {\n const pending = await this.repo.pendingDeliveriesForEvent(orgId, sourceEventId);\n for (const delivery of pending) {\n await this.engine.deliver({ orgId, deliveryId: delivery.id });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { contentEvents } from "../../../content/events.js";
|
|
2
|
+
import { previousStatusOf } from "../../community-mail.js";
|
|
3
|
+
import { Reasons, actorOr, eventAt, listOf, recipientsOf } from "./recipients.js";
|
|
4
|
+
import { eventBase, eventDisplay } from "./events.js";
|
|
5
|
+
//#region src/automations/notifications/rules/content.ts
|
|
6
|
+
const activityPublished = async (event, reads) => {
|
|
7
|
+
const parsed = contentEvents.activityPublished.safeParse(event);
|
|
8
|
+
if (!parsed.success) return [];
|
|
9
|
+
const { activityId, courseId } = parsed.data.data;
|
|
10
|
+
const orgId = event.orgId;
|
|
11
|
+
const activity = await reads.activity(orgId, activityId);
|
|
12
|
+
if (!activity) return [];
|
|
13
|
+
return listOf({
|
|
14
|
+
type: "course.activity_added",
|
|
15
|
+
scope: "org",
|
|
16
|
+
actorId: actorOr(event, null),
|
|
17
|
+
communityId: null,
|
|
18
|
+
spaceId: null,
|
|
19
|
+
subject: {
|
|
20
|
+
type: "activity",
|
|
21
|
+
id: activityId
|
|
22
|
+
},
|
|
23
|
+
parent: {
|
|
24
|
+
type: "course",
|
|
25
|
+
id: courseId
|
|
26
|
+
},
|
|
27
|
+
display: {
|
|
28
|
+
courseTitle: activity.courseTitle,
|
|
29
|
+
moduleTitle: activity.moduleTitle,
|
|
30
|
+
activityTitle: activity.title
|
|
31
|
+
}
|
|
32
|
+
}, await recipientsOf(reads, orgId, new Reasons().add(await reads.courseMembers(orgId, courseId), "course_member"), {
|
|
33
|
+
exclude: [actorOr(event, null)],
|
|
34
|
+
at: eventAt(event)
|
|
35
|
+
}));
|
|
36
|
+
};
|
|
37
|
+
const LEAD_TEXT = {
|
|
38
|
+
"1d": "1 day",
|
|
39
|
+
"1h": "1 hour"
|
|
40
|
+
};
|
|
41
|
+
function metadataText(event, key) {
|
|
42
|
+
const value = event.metadata?.[key];
|
|
43
|
+
return typeof value === "string" ? value : void 0;
|
|
44
|
+
}
|
|
45
|
+
function whatChanged(event, next) {
|
|
46
|
+
const time = metadataText(event, "previousStartsAt") !== next.startsAt.toISOString() || metadataText(event, "previousEndsAt") !== next.endsAt.toISOString();
|
|
47
|
+
const location = event.metadata?.locationChanged === true;
|
|
48
|
+
if (time && location) return "time_and_location";
|
|
49
|
+
return time ? "time" : location ? "location" : null;
|
|
50
|
+
}
|
|
51
|
+
async function toAttendees(reads, domainEvent, event, type, actorId, display) {
|
|
52
|
+
return listOf({
|
|
53
|
+
...eventBase(type, event, actorId),
|
|
54
|
+
display
|
|
55
|
+
}, await recipientsOf(reads, domainEvent.orgId, new Reasons().add(await reads.eventAttendees(domainEvent.orgId, event.id), "event_member"), {
|
|
56
|
+
exclude: [actorId],
|
|
57
|
+
at: eventAt(domainEvent)
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
const eventUpdated = async (domainEvent, reads) => {
|
|
61
|
+
const parsed = contentEvents.eventUpdated.safeParse(domainEvent);
|
|
62
|
+
if (!parsed.success) return [];
|
|
63
|
+
const next = parsed.data.data;
|
|
64
|
+
const event = await reads.event(domainEvent.orgId, next.id);
|
|
65
|
+
if (!event?.settings.notifications) return [];
|
|
66
|
+
const actor = actorOr(domainEvent, null);
|
|
67
|
+
const previousStatus = previousStatusOf(domainEvent);
|
|
68
|
+
if (next.status === "canceled" && previousStatus !== "canceled") return toAttendees(reads, domainEvent, event, "event.canceled", actor, eventDisplay(next));
|
|
69
|
+
if (previousStatus !== "published" || next.status !== "published" || next.endsAt.getTime() <= eventAt(domainEvent).getTime()) return [];
|
|
70
|
+
const changed = whatChanged(domainEvent, next);
|
|
71
|
+
if (!changed) return [];
|
|
72
|
+
return toAttendees(reads, domainEvent, event, "event.changed", actor, {
|
|
73
|
+
...eventDisplay(next),
|
|
74
|
+
changed
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const eventStartingSoon = async (domainEvent, reads) => {
|
|
78
|
+
const parsed = contentEvents.eventStartingSoon.safeParse(domainEvent);
|
|
79
|
+
if (!parsed.success) return [];
|
|
80
|
+
const { eventId, lead } = parsed.data.data;
|
|
81
|
+
const event = await reads.event(domainEvent.orgId, eventId);
|
|
82
|
+
if (!event?.settings.notifications || event.status !== "published") return [];
|
|
83
|
+
return toAttendees(reads, domainEvent, event, "event.starting_soon", null, {
|
|
84
|
+
...eventDisplay(event),
|
|
85
|
+
leadText: LEAD_TEXT[lead]
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const CONTENT_RULES = {
|
|
89
|
+
"content.course.activity.published": activityPublished,
|
|
90
|
+
"content.event.updated": eventUpdated,
|
|
91
|
+
"content.event.starting_soon": eventStartingSoon
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
export { CONTENT_RULES };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","names":[],"sources":["../../../../src/automations/notifications/rules/content.ts"],"sourcesContent":["import { contentEvents, type Event } from '../../../content/index.js';\nimport type { DomainEvent } from '../../../shared/ports.js';\nimport type { NotificationReads, RecipientRule } from '../../ports.js';\nimport { previousStatusOf } from '../../community-mail.js';\nimport { eventBase, eventDisplay } from './events.js';\nimport { actorOr, eventAt, listOf, Reasons, recipientsOf } from './recipients.js';\n\nconst activityPublished: RecipientRule = async (event, reads) => {\n const parsed = contentEvents.activityPublished.safeParse(event);\n if (!parsed.success) {\n return [];\n }\n const { activityId, courseId } = parsed.data.data;\n const orgId = event.orgId;\n const activity = await reads.activity(orgId, activityId);\n if (!activity) {\n return [];\n }\n return listOf(\n {\n type: 'course.activity_added',\n scope: 'org',\n actorId: actorOr(event, null),\n communityId: null,\n spaceId: null,\n subject: { type: 'activity', id: activityId },\n parent: { type: 'course', id: courseId },\n display: {\n courseTitle: activity.courseTitle,\n moduleTitle: activity.moduleTitle,\n activityTitle: activity.title,\n },\n },\n await recipientsOf(\n reads,\n orgId,\n new Reasons().add(await reads.courseMembers(orgId, courseId), 'course_member'),\n { exclude: [actorOr(event, null)], at: eventAt(event) },\n ),\n );\n};\n\nconst LEAD_TEXT = { '1d': '1 day', '1h': '1 hour' } as const;\n\nfunction metadataText(event: DomainEvent, key: string): string | undefined {\n const value = event.metadata?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction whatChanged(\n event: DomainEvent,\n next: Event,\n): 'time' | 'location' | 'time_and_location' | null {\n const time =\n metadataText(event, 'previousStartsAt') !== next.startsAt.toISOString() ||\n metadataText(event, 'previousEndsAt') !== next.endsAt.toISOString();\n const location = event.metadata?.locationChanged === true;\n if (time && location) {\n return 'time_and_location';\n }\n return time ? 'time' : location ? 'location' : null;\n}\n\nasync function toAttendees(\n reads: NotificationReads,\n domainEvent: DomainEvent,\n event: Event,\n type: string,\n actorId: string | null,\n display: Record<string, string>,\n) {\n return listOf(\n { ...eventBase(type, event, actorId), display },\n await recipientsOf(\n reads,\n domainEvent.orgId,\n new Reasons().add(await reads.eventAttendees(domainEvent.orgId, event.id), 'event_member'),\n { exclude: [actorId], at: eventAt(domainEvent) },\n ),\n );\n}\n\nconst eventUpdated: RecipientRule = async (domainEvent, reads) => {\n const parsed = contentEvents.eventUpdated.safeParse(domainEvent);\n if (!parsed.success) {\n return [];\n }\n const next = parsed.data.data;\n const event = await reads.event(domainEvent.orgId, next.id);\n if (!event?.settings.notifications) {\n return [];\n }\n const actor = actorOr(domainEvent, null);\n const previousStatus = previousStatusOf(domainEvent);\n if (next.status === 'canceled' && previousStatus !== 'canceled') {\n return toAttendees(reads, domainEvent, event, 'event.canceled', actor, eventDisplay(next));\n }\n if (\n previousStatus !== 'published' ||\n next.status !== 'published' ||\n next.endsAt.getTime() <= eventAt(domainEvent).getTime()\n ) {\n return [];\n }\n const changed = whatChanged(domainEvent, next);\n if (!changed) {\n return [];\n }\n return toAttendees(reads, domainEvent, event, 'event.changed', actor, {\n ...eventDisplay(next),\n changed,\n });\n};\n\nconst eventStartingSoon: RecipientRule = async (domainEvent, reads) => {\n const parsed = contentEvents.eventStartingSoon.safeParse(domainEvent);\n if (!parsed.success) {\n return [];\n }\n const { eventId, lead } = parsed.data.data;\n const event = await reads.event(domainEvent.orgId, eventId);\n if (!event?.settings.notifications || event.status !== 'published') {\n return [];\n }\n return toAttendees(reads, domainEvent, event, 'event.starting_soon', null, {\n ...eventDisplay(event),\n leadText: LEAD_TEXT[lead],\n });\n};\n\nexport const CONTENT_RULES: Record<string, RecipientRule> = {\n 'content.course.activity.published': activityPublished,\n 'content.event.updated': eventUpdated,\n 'content.event.starting_soon': eventStartingSoon,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RecipientRule } from "../../ports.js";
|
|
2
|
+
//#region src/automations/notifications/rules/discussion.d.ts
|
|
3
|
+
declare function messageSummary(count: number): string;
|
|
4
|
+
declare const DISCUSSION_RULES: Record<string, RecipientRule>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { DISCUSSION_RULES, messageSummary };
|
|
7
|
+
//# sourceMappingURL=discussion.d.ts.map
|