@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.
Files changed (307) hide show
  1. package/dist/assets/events.d.ts +24 -18
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +5 -4
  6. package/dist/assets/index.js +4 -3
  7. package/dist/assets/jobs.d.ts +15 -0
  8. package/dist/assets/jobs.js +30 -0
  9. package/dist/assets/jobs.js.map +1 -0
  10. package/dist/assets/ports.d.ts +37 -17
  11. package/dist/assets/service.d.ts +30 -15
  12. package/dist/assets/service.js +229 -59
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.js +2 -63
  15. package/dist/automations/actions.js.map +1 -1
  16. package/dist/automations/catalog.d.ts +1 -2
  17. package/dist/automations/catalog.js +137 -65
  18. package/dist/automations/catalog.js.map +1 -1
  19. package/dist/automations/community-derivations.js +20 -20
  20. package/dist/automations/community-derivations.js.map +1 -1
  21. package/dist/automations/community-mail.d.ts +36 -20
  22. package/dist/automations/community-mail.js +32 -24
  23. package/dist/automations/community-mail.js.map +1 -1
  24. package/dist/automations/event-permissions.d.ts +12 -0
  25. package/dist/automations/event-permissions.js +38 -0
  26. package/dist/automations/event-permissions.js.map +1 -0
  27. package/dist/automations/events.d.ts +0 -10
  28. package/dist/automations/index.d.ts +12 -4
  29. package/dist/automations/index.js +9 -1
  30. package/dist/automations/notifications/applicable.d.ts +8 -0
  31. package/dist/automations/notifications/applicable.js +12 -0
  32. package/dist/automations/notifications/applicable.js.map +1 -0
  33. package/dist/automations/notifications/debounce.d.ts +25 -0
  34. package/dist/automations/notifications/debounce.js +93 -0
  35. package/dist/automations/notifications/debounce.js.map +1 -0
  36. package/dist/automations/notifications/defaults.d.ts +8 -0
  37. package/dist/automations/notifications/defaults.js +346 -0
  38. package/dist/automations/notifications/defaults.js.map +1 -0
  39. package/dist/automations/notifications/delivery.d.ts +28 -0
  40. package/dist/automations/notifications/delivery.js +88 -0
  41. package/dist/automations/notifications/delivery.js.map +1 -0
  42. package/dist/automations/notifications/processor.d.ts +64 -0
  43. package/dist/automations/notifications/processor.js +270 -0
  44. package/dist/automations/notifications/processor.js.map +1 -0
  45. package/dist/automations/notifications/rules/content.d.ts +6 -0
  46. package/dist/automations/notifications/rules/content.js +96 -0
  47. package/dist/automations/notifications/rules/content.js.map +1 -0
  48. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  49. package/dist/automations/notifications/rules/discussion.js +678 -0
  50. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  51. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  52. package/dist/automations/notifications/rules/entitlements.js +276 -0
  53. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  54. package/dist/automations/notifications/rules/events.d.ts +9 -0
  55. package/dist/automations/notifications/rules/events.js +27 -0
  56. package/dist/automations/notifications/rules/events.js.map +1 -0
  57. package/dist/automations/notifications/rules/index.d.ts +6 -0
  58. package/dist/automations/notifications/rules/index.js +15 -0
  59. package/dist/automations/notifications/rules/index.js.map +1 -0
  60. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  61. package/dist/automations/notifications/rules/organizations.js +35 -0
  62. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  63. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  64. package/dist/automations/notifications/rules/recipients.js +98 -0
  65. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  66. package/dist/automations/notifications/service.d.ts +29 -0
  67. package/dist/automations/notifications/service.js +45 -0
  68. package/dist/automations/notifications/service.js.map +1 -0
  69. package/dist/automations/notifications/types.d.ts +7 -0
  70. package/dist/automations/notifications/types.js +560 -0
  71. package/dist/automations/notifications/types.js.map +1 -0
  72. package/dist/automations/ports.d.ts +134 -21
  73. package/dist/automations/service.d.ts +30 -30
  74. package/dist/automations/service.js +111 -144
  75. package/dist/automations/service.js.map +1 -1
  76. package/dist/content/event-sight.d.ts +11 -0
  77. package/dist/content/event-sight.js +11 -0
  78. package/dist/content/event-sight.js.map +1 -0
  79. package/dist/content/events.d.ts +437 -46
  80. package/dist/content/events.js +45 -1
  81. package/dist/content/events.js.map +1 -1
  82. package/dist/content/index.d.ts +8 -4
  83. package/dist/content/index.js +5 -1
  84. package/dist/content/jobs.d.ts +16 -0
  85. package/dist/content/jobs.js +37 -0
  86. package/dist/content/jobs.js.map +1 -0
  87. package/dist/content/model.d.ts +2 -2
  88. package/dist/content/notification-categories.d.ts +37 -0
  89. package/dist/content/notification-categories.js +73 -0
  90. package/dist/content/notification-categories.js.map +1 -0
  91. package/dist/content/ports.d.ts +132 -68
  92. package/dist/content/service.d.ts +100 -68
  93. package/dist/content/service.js +703 -146
  94. package/dist/content/service.js.map +1 -1
  95. package/dist/content/titles.d.ts +25 -0
  96. package/dist/content/titles.js +82 -0
  97. package/dist/content/titles.js.map +1 -0
  98. package/dist/content/types.d.ts +2 -2
  99. package/dist/discussion/authors.d.ts +4 -2
  100. package/dist/discussion/authors.js +9 -5
  101. package/dist/discussion/authors.js.map +1 -1
  102. package/dist/discussion/chats.d.ts +63 -39
  103. package/dist/discussion/chats.js +455 -190
  104. package/dist/discussion/chats.js.map +1 -1
  105. package/dist/discussion/digests.d.ts +24 -0
  106. package/dist/discussion/digests.js +104 -0
  107. package/dist/discussion/digests.js.map +1 -0
  108. package/dist/discussion/events.d.ts +227 -23
  109. package/dist/discussion/events.js +114 -19
  110. package/dist/discussion/events.js.map +1 -1
  111. package/dist/discussion/index.d.ts +12 -4
  112. package/dist/discussion/index.js +9 -2
  113. package/dist/discussion/jobs.d.ts +16 -0
  114. package/dist/discussion/jobs.js +40 -0
  115. package/dist/discussion/jobs.js.map +1 -0
  116. package/dist/discussion/live.d.ts +106 -0
  117. package/dist/discussion/live.js +642 -0
  118. package/dist/discussion/live.js.map +1 -0
  119. package/dist/discussion/mentions.d.ts +11 -0
  120. package/dist/discussion/mentions.js +35 -0
  121. package/dist/discussion/mentions.js.map +1 -0
  122. package/dist/discussion/ports.d.ts +279 -114
  123. package/dist/discussion/ports.js.map +1 -1
  124. package/dist/discussion/posts.d.ts +60 -44
  125. package/dist/discussion/posts.js +453 -287
  126. package/dist/discussion/posts.js.map +1 -1
  127. package/dist/discussion/rules.d.ts +18 -0
  128. package/dist/discussion/rules.js +22 -0
  129. package/dist/discussion/rules.js.map +1 -0
  130. package/dist/discussion/service.d.ts +50 -35
  131. package/dist/discussion/service.js +333 -205
  132. package/dist/discussion/service.js.map +1 -1
  133. package/dist/discussion/subscriptions.d.ts +37 -0
  134. package/dist/discussion/subscriptions.js +115 -0
  135. package/dist/discussion/subscriptions.js.map +1 -0
  136. package/dist/discussion/titles.d.ts +28 -0
  137. package/dist/discussion/titles.js +98 -0
  138. package/dist/discussion/titles.js.map +1 -0
  139. package/dist/entitlements/events.d.ts +20 -20
  140. package/dist/entitlements/index.d.ts +5 -4
  141. package/dist/entitlements/index.js +3 -2
  142. package/dist/entitlements/jobs.d.ts +12 -0
  143. package/dist/entitlements/jobs.js +18 -0
  144. package/dist/entitlements/jobs.js.map +1 -0
  145. package/dist/entitlements/model.d.ts +2 -2
  146. package/dist/entitlements/ports.d.ts +68 -38
  147. package/dist/entitlements/rules.d.ts +3 -6
  148. package/dist/entitlements/rules.js +5 -10
  149. package/dist/entitlements/rules.js.map +1 -1
  150. package/dist/entitlements/service.d.ts +43 -28
  151. package/dist/entitlements/service.js +387 -160
  152. package/dist/entitlements/service.js.map +1 -1
  153. package/dist/identity/devices.d.ts +30 -0
  154. package/dist/identity/devices.js +109 -0
  155. package/dist/identity/devices.js.map +1 -0
  156. package/dist/identity/index.d.ts +6 -5
  157. package/dist/identity/index.js +4 -3
  158. package/dist/identity/model.d.ts +2 -2
  159. package/dist/identity/notification-settings.d.ts +28 -0
  160. package/dist/identity/notification-settings.js +114 -0
  161. package/dist/identity/notification-settings.js.map +1 -0
  162. package/dist/identity/ports.d.ts +80 -56
  163. package/dist/identity/service.d.ts +8 -7
  164. package/dist/identity/service.js +17 -13
  165. package/dist/identity/service.js.map +1 -1
  166. package/dist/integrations/ports.d.ts +9 -8
  167. package/dist/integrations/service.d.ts +9 -8
  168. package/dist/integrations/service.js +33 -16
  169. package/dist/integrations/service.js.map +1 -1
  170. package/dist/organizations/default-roles.d.ts +22 -0
  171. package/dist/organizations/default-roles.js +89 -0
  172. package/dist/organizations/default-roles.js.map +1 -0
  173. package/dist/organizations/effective-permissions.d.ts +45 -0
  174. package/dist/organizations/effective-permissions.js +113 -0
  175. package/dist/organizations/effective-permissions.js.map +1 -0
  176. package/dist/organizations/events.d.ts +320 -181
  177. package/dist/organizations/events.js +90 -18
  178. package/dist/organizations/events.js.map +1 -1
  179. package/dist/organizations/index.d.ts +15 -10
  180. package/dist/organizations/index.js +8 -3
  181. package/dist/organizations/members.d.ts +66 -8
  182. package/dist/organizations/members.js.map +1 -1
  183. package/dist/organizations/notification-catalog.d.ts +31 -0
  184. package/dist/organizations/notification-catalog.js +32 -0
  185. package/dist/organizations/notification-catalog.js.map +1 -0
  186. package/dist/organizations/notification-categories.d.ts +34 -0
  187. package/dist/organizations/notification-categories.js +66 -0
  188. package/dist/organizations/notification-categories.js.map +1 -0
  189. package/dist/organizations/org-roles.d.ts +33 -0
  190. package/dist/organizations/org-roles.js +49 -0
  191. package/dist/organizations/org-roles.js.map +1 -0
  192. package/dist/organizations/permissions.d.ts +311 -0
  193. package/dist/organizations/permissions.js +327 -0
  194. package/dist/organizations/permissions.js.map +1 -0
  195. package/dist/organizations/ports.d.ts +187 -65
  196. package/dist/organizations/service.d.ts +104 -35
  197. package/dist/organizations/service.js +1247 -323
  198. package/dist/organizations/service.js.map +1 -1
  199. package/dist/organizations/testing.d.ts +18 -0
  200. package/dist/organizations/testing.js +37 -0
  201. package/dist/organizations/testing.js.map +1 -0
  202. package/dist/organizations/types.d.ts +5 -16
  203. package/dist/progress/index.d.ts +2 -2
  204. package/dist/progress/ports.d.ts +11 -3
  205. package/dist/progress/service.d.ts +6 -5
  206. package/dist/progress/service.js +23 -8
  207. package/dist/progress/service.js.map +1 -1
  208. package/dist/reporting/admin/index.d.ts +6 -4
  209. package/dist/reporting/admin/model.d.ts +36 -3
  210. package/dist/reporting/admin/ports.d.ts +39 -14
  211. package/dist/reporting/admin/service.d.ts +32 -11
  212. package/dist/reporting/admin/service.js +102 -18
  213. package/dist/reporting/admin/service.js.map +1 -1
  214. package/dist/reporting/events/index.d.ts +27 -0
  215. package/dist/reporting/events/index.js +20 -0
  216. package/dist/reporting/events/index.js.map +1 -0
  217. package/dist/reporting/member/calendar.d.ts +11 -0
  218. package/dist/reporting/member/calendar.js +75 -0
  219. package/dist/reporting/member/calendar.js.map +1 -0
  220. package/dist/reporting/member/index.d.ts +3 -2
  221. package/dist/reporting/member/model.d.ts +80 -26
  222. package/dist/reporting/member/ports.d.ts +50 -28
  223. package/dist/reporting/member/service.d.ts +81 -35
  224. package/dist/reporting/member/service.js +438 -145
  225. package/dist/reporting/member/service.js.map +1 -1
  226. package/dist/shared/actor.d.ts +22 -3
  227. package/dist/shared/errors.d.ts +15 -2
  228. package/dist/shared/errors.js +21 -2
  229. package/dist/shared/errors.js.map +1 -1
  230. package/dist/shared/id.d.ts +7 -0
  231. package/dist/shared/id.js +8 -1
  232. package/dist/shared/id.js.map +1 -1
  233. package/dist/shared/logger.d.ts +2 -1
  234. package/dist/shared/logger.js +10 -1
  235. package/dist/shared/logger.js.map +1 -1
  236. package/dist/shared/mailer.d.ts +11 -1
  237. package/dist/shared/mailer.js +32 -5
  238. package/dist/shared/mailer.js.map +1 -1
  239. package/dist/shared/notification-categories.d.ts +16 -0
  240. package/dist/shared/notification-categories.js +94 -0
  241. package/dist/shared/notification-categories.js.map +1 -0
  242. package/dist/shared/notification-settings.d.ts +14 -0
  243. package/dist/shared/notification-settings.js +61 -0
  244. package/dist/shared/notification-settings.js.map +1 -0
  245. package/dist/shared/ports.d.ts +68 -2
  246. package/dist/shared/settings.d.ts +2 -1
  247. package/dist/shared/settings.js +1 -0
  248. package/dist/shared/settings.js.map +1 -1
  249. package/dist/types/automations.d.ts +8 -2
  250. package/dist/types/content.d.ts +2 -2
  251. package/dist/types/discussion.d.ts +4 -5
  252. package/dist/types/email-templates.d.ts +119 -6
  253. package/dist/types/entitlements.d.ts +2 -2
  254. package/dist/types/identity.d.ts +14 -17
  255. package/dist/types/index.d.ts +13 -12
  256. package/dist/types/index.js +2 -1
  257. package/dist/types/notifications.d.ts +157 -0
  258. package/dist/types/notifications.js +1 -0
  259. package/dist/types/organizations.d.ts +3 -2
  260. package/dist/types/ports.d.ts +8 -1
  261. package/dist/types/ports.js +7 -1
  262. package/dist/types/ports.js.map +1 -0
  263. package/dist/types/schemas/assets.d.ts +5 -5
  264. package/dist/types/schemas/assets.js +4 -3
  265. package/dist/types/schemas/assets.js.map +1 -1
  266. package/dist/types/schemas/automations.d.ts +1 -10
  267. package/dist/types/schemas/automations.js +1 -3
  268. package/dist/types/schemas/automations.js.map +1 -1
  269. package/dist/types/schemas/content.d.ts +596 -11
  270. package/dist/types/schemas/content.js +225 -9
  271. package/dist/types/schemas/content.js.map +1 -1
  272. package/dist/types/schemas/discussion.d.ts +362 -66
  273. package/dist/types/schemas/discussion.js +123 -15
  274. package/dist/types/schemas/discussion.js.map +1 -1
  275. package/dist/types/schemas/entitlements.d.ts +14 -1
  276. package/dist/types/schemas/entitlements.js +11 -4
  277. package/dist/types/schemas/entitlements.js.map +1 -1
  278. package/dist/types/schemas/identity.d.ts +1 -85
  279. package/dist/types/schemas/identity.js +1 -42
  280. package/dist/types/schemas/identity.js.map +1 -1
  281. package/dist/types/schemas/index.d.ts +8 -7
  282. package/dist/types/schemas/index.js +8 -7
  283. package/dist/types/schemas/notifications.d.ts +96 -0
  284. package/dist/types/schemas/notifications.js +57 -0
  285. package/dist/types/schemas/notifications.js.map +1 -0
  286. package/dist/types/schemas/organizations.d.ts +96 -60
  287. package/dist/types/schemas/organizations.js +78 -27
  288. package/dist/types/schemas/organizations.js.map +1 -1
  289. package/dist/types/schemas/progress.d.ts +0 -1
  290. package/dist/types/schemas/progress.js +0 -1
  291. package/dist/types/schemas/progress.js.map +1 -1
  292. package/package.json +1 -1
  293. package/dist/automations/community-notifications.d.ts +0 -16
  294. package/dist/automations/community-notifications.js +0 -84
  295. package/dist/automations/community-notifications.js.map +0 -1
  296. package/dist/automations/seeds.d.ts +0 -17
  297. package/dist/automations/seeds.js +0 -235
  298. package/dist/automations/seeds.js.map +0 -1
  299. package/dist/identity/notifications.d.ts +0 -27
  300. package/dist/identity/notifications.js +0 -101
  301. package/dist/identity/notifications.js.map +0 -1
  302. package/dist/organizations/access-scopes.d.ts +0 -19
  303. package/dist/organizations/access-scopes.js +0 -89
  304. package/dist/organizations/access-scopes.js.map +0 -1
  305. package/dist/organizations/roles.d.ts +0 -19
  306. package/dist/organizations/roles.js +0 -82
  307. 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,6 @@
1
+ import { RecipientRule } from "../../ports.js";
2
+ //#region src/automations/notifications/rules/content.d.ts
3
+ declare const CONTENT_RULES: Record<string, RecipientRule>;
4
+ //#endregion
5
+ export { CONTENT_RULES };
6
+ //# sourceMappingURL=content.d.ts.map
@@ -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":";;;;;AAOA,MAAM,oBAAmC,OAAO,OAAO,UAAU;CAC/D,MAAM,SAAS,cAAc,kBAAkB,UAAU,KAAK;CAC9D,IAAI,CAAC,OAAO,SACV,OAAO,CAAC;CAEV,MAAM,EAAE,YAAY,aAAa,OAAO,KAAK;CAC7C,MAAM,QAAQ,MAAM;CACpB,MAAM,WAAW,MAAM,MAAM,SAAS,OAAO,UAAU;CACvD,IAAI,CAAC,UACH,OAAO,CAAC;CAEV,OAAO,OACL;EACE,MAAM;EACN,OAAO;EACP,SAAS,QAAQ,OAAO,IAAI;EAC5B,aAAa;EACb,SAAS;EACT,SAAS;GAAE,MAAM;GAAY,IAAI;EAAW;EAC5C,QAAQ;GAAE,MAAM;GAAU,IAAI;EAAS;EACvC,SAAS;GACP,aAAa,SAAS;GACtB,aAAa,SAAS;GACtB,eAAe,SAAS;EAC1B;CACF,GACA,MAAM,aACJ,OACA,OACA,IAAI,QAAQ,CAAC,CAAC,IAAI,MAAM,MAAM,cAAc,OAAO,QAAQ,GAAG,eAAe,GAC7E;EAAE,SAAS,CAAC,QAAQ,OAAO,IAAI,CAAC;EAAG,IAAI,QAAQ,KAAK;CAAE,CACxD,CACF;AACF;AAEA,MAAM,YAAY;CAAE,MAAM;CAAS,MAAM;AAAS;AAElD,SAAS,aAAa,OAAoB,KAAiC;CACzE,MAAM,QAAQ,MAAM,WAAW;CAC/B,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAS,YACP,OACA,MACkD;CAClD,MAAM,OACJ,aAAa,OAAO,kBAAkB,MAAM,KAAK,SAAS,YAAY,KACtE,aAAa,OAAO,gBAAgB,MAAM,KAAK,OAAO,YAAY;CACpE,MAAM,WAAW,MAAM,UAAU,oBAAoB;CACrD,IAAI,QAAQ,UACV,OAAO;CAET,OAAO,OAAO,SAAS,WAAW,aAAa;AACjD;AAEA,eAAe,YACb,OACA,aACA,OACA,MACA,SACA,SACA;CACA,OAAO,OACL;EAAE,GAAG,UAAU,MAAM,OAAO,OAAO;EAAG;CAAQ,GAC9C,MAAM,aACJ,OACA,YAAY,OACZ,IAAI,QAAQ,CAAC,CAAC,IAAI,MAAM,MAAM,eAAe,YAAY,OAAO,MAAM,EAAE,GAAG,cAAc,GACzF;EAAE,SAAS,CAAC,OAAO;EAAG,IAAI,QAAQ,WAAW;CAAE,CACjD,CACF;AACF;AAEA,MAAM,eAA8B,OAAO,aAAa,UAAU;CAChE,MAAM,SAAS,cAAc,aAAa,UAAU,WAAW;CAC/D,IAAI,CAAC,OAAO,SACV,OAAO,CAAC;CAEV,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,QAAQ,MAAM,MAAM,MAAM,YAAY,OAAO,KAAK,EAAE;CAC1D,IAAI,CAAC,OAAO,SAAS,eACnB,OAAO,CAAC;CAEV,MAAM,QAAQ,QAAQ,aAAa,IAAI;CACvC,MAAM,iBAAiB,iBAAiB,WAAW;CACnD,IAAI,KAAK,WAAW,cAAc,mBAAmB,YACnD,OAAO,YAAY,OAAO,aAAa,OAAO,kBAAkB,OAAO,aAAa,IAAI,CAAC;CAE3F,IACE,mBAAmB,eACnB,KAAK,WAAW,eAChB,KAAK,OAAO,QAAQ,KAAK,QAAQ,WAAW,CAAC,CAAC,QAAQ,GAEtD,OAAO,CAAC;CAEV,MAAM,UAAU,YAAY,aAAa,IAAI;CAC7C,IAAI,CAAC,SACH,OAAO,CAAC;CAEV,OAAO,YAAY,OAAO,aAAa,OAAO,iBAAiB,OAAO;EACpE,GAAG,aAAa,IAAI;EACpB;CACF,CAAC;AACH;AAEA,MAAM,oBAAmC,OAAO,aAAa,UAAU;CACrE,MAAM,SAAS,cAAc,kBAAkB,UAAU,WAAW;CACpE,IAAI,CAAC,OAAO,SACV,OAAO,CAAC;CAEV,MAAM,EAAE,SAAS,SAAS,OAAO,KAAK;CACtC,MAAM,QAAQ,MAAM,MAAM,MAAM,YAAY,OAAO,OAAO;CAC1D,IAAI,CAAC,OAAO,SAAS,iBAAiB,MAAM,WAAW,aACrD,OAAO,CAAC;CAEV,OAAO,YAAY,OAAO,aAAa,OAAO,uBAAuB,MAAM;EACzE,GAAG,aAAa,KAAK;EACrB,UAAU,UAAU;CACtB,CAAC;AACH;AAEA,MAAa,gBAA+C;CAC1D,qCAAqC;CACrC,yBAAyB;CACzB,+BAA+B;AACjC"}
@@ -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