@headless-lms/core 0.4.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 (249) hide show
  1. package/dist/assets/events.d.ts +27 -21
  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 +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +230 -56
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +69 -4
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +3 -3
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +13 -13
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +91 -62
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +413 -72
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +99 -67
  72. package/dist/content/service.js +680 -139
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +393 -161
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +77 -39
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +268 -154
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +252 -188
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +29 -13
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +20 -20
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +370 -163
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +7 -5
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +2 -2
  127. package/dist/identity/index.js +2 -2
  128. package/dist/identity/notification-settings.d.ts +1 -1
  129. package/dist/identity/notification-settings.js +3 -2
  130. package/dist/identity/notification-settings.js.map +1 -1
  131. package/dist/identity/ports.d.ts +7 -7
  132. package/dist/identity/service.d.ts +8 -7
  133. package/dist/identity/service.js +17 -13
  134. package/dist/identity/service.js.map +1 -1
  135. package/dist/integrations/events.d.ts +3 -3
  136. package/dist/integrations/ports.d.ts +9 -8
  137. package/dist/integrations/service.d.ts +9 -8
  138. package/dist/integrations/service.js +33 -16
  139. package/dist/integrations/service.js.map +1 -1
  140. package/dist/organizations/default-roles.d.ts +22 -0
  141. package/dist/organizations/default-roles.js +89 -0
  142. package/dist/organizations/default-roles.js.map +1 -0
  143. package/dist/organizations/effective-permissions.d.ts +45 -0
  144. package/dist/organizations/effective-permissions.js +113 -0
  145. package/dist/organizations/effective-permissions.js.map +1 -0
  146. package/dist/organizations/events.d.ts +320 -181
  147. package/dist/organizations/events.js +90 -18
  148. package/dist/organizations/events.js.map +1 -1
  149. package/dist/organizations/index.d.ts +14 -10
  150. package/dist/organizations/index.js +7 -3
  151. package/dist/organizations/members.d.ts +66 -8
  152. package/dist/organizations/members.js.map +1 -1
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +184 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1245 -328
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +23 -8
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +26 -5
  193. package/dist/reporting/member/ports.d.ts +48 -29
  194. package/dist/reporting/member/service.d.ts +56 -29
  195. package/dist/reporting/member/service.js +328 -113
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +22 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +537 -13
  224. package/dist/types/schemas/content.js +201 -7
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +282 -59
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +14 -1
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +13 -12
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +96 -60
  238. package/dist/types/schemas/organizations.js +78 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { noopLogger } from "../shared/logger.js";
2
+ import { PERMISSIONS, withImplied } from "../organizations/permissions.js";
2
3
  //#region src/automations/community-mail.ts
3
4
  const MESSAGE_EXCERPT = 140;
4
5
  const POST_EXCERPT = 280;
@@ -11,9 +12,13 @@ const emptyCommunityMail = {
11
12
  communityMembersAmong: async () => [],
12
13
  spaceOpenersAmong: async () => [],
13
14
  spaceOpeners: async () => [],
14
- moderationAudience: async () => [],
15
- staffIds: async () => [],
15
+ holders: async () => [],
16
+ role: async () => null,
17
+ heldOnOrg: async () => [],
18
+ orgName: async () => null,
19
+ contentInfo: async () => null,
16
20
  displayName: async () => "A member",
21
+ postText: (body) => body,
17
22
  followerIds: async () => [],
18
23
  comment: async () => null,
19
24
  post: async () => null,
@@ -24,8 +29,31 @@ const emptyCommunityMail = {
24
29
  memberCommunityTitles: async () => [],
25
30
  recentMessageRecipients: async () => []
26
31
  };
32
+ async function communityModerators(reads, orgId, communityId) {
33
+ const holders = await reads.holders(orgId, "communities:moderate", communityId);
34
+ return holders.length === 0 ? [] : reads.communityMembersAmong(orgId, communityId, holders);
35
+ }
36
+ function contributes(permissions, contentType) {
37
+ const expanded = [...withImplied(permissions)];
38
+ return new Set(contentType === null ? expanded : expanded.filter((permission) => PERMISSIONS[permission].about === contentType));
39
+ }
40
+ async function roleGain(reads, orgId, assigned) {
41
+ const next = await reads.role(orgId, assigned.roleKey);
42
+ if (!next) return null;
43
+ const before = contributes((assigned.previousRoleKey ? await reads.role(orgId, assigned.previousRoleKey) : null)?.permissions ?? [], assigned.contentType);
44
+ const offered = [...contributes(next.permissions, assigned.contentType)].filter((permission) => !before.has(permission));
45
+ const fromOrg = assigned.contentId === null || offered.length === 0 ? [] : await reads.heldOnOrg(orgId, assigned.orgUserId, offered);
46
+ if (!offered.some((permission) => !fromOrg.includes(permission))) return null;
47
+ const place = assigned.contentId === null ? `in ${await reads.orgName(orgId) ?? "the organization"}` : `of ${(await reads.contentInfo(orgId, { contentId: assigned.contentId }))?.title ?? "an item"}`;
48
+ return {
49
+ roleName: next.name,
50
+ place,
51
+ contentId: assigned.contentId,
52
+ contentType: assigned.contentType
53
+ };
54
+ }
27
55
  function actorOf(event) {
28
- const value = event.metadata?.actorOrgUserId;
56
+ const value = event.metadata?.actorId;
29
57
  return typeof value === "string" ? value : void 0;
30
58
  }
31
59
  function previousStatusOf(event) {
@@ -65,6 +93,6 @@ async function address(deps, event, orgId, orgUserIds, params) {
65
93
  return recipients;
66
94
  }
67
95
  //#endregion
68
- export { MESSAGE_BATCH_MS, MESSAGE_EXCERPT, POST_EXCERPT, actorOf, address, emptyCommunityMail, excerpt, previousStatusOf, seededCommunityId };
96
+ export { MESSAGE_BATCH_MS, MESSAGE_EXCERPT, POST_EXCERPT, actorOf, address, communityModerators, emptyCommunityMail, excerpt, previousStatusOf, roleGain, seededCommunityId };
69
97
 
70
98
  //# sourceMappingURL=community-mail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"community-mail.js","names":[],"sources":["../../src/automations/community-mail.ts"],"sourcesContent":["import type { GrantEntitlementInput } from '../entitlements/index.js';\nimport type {\n ChatStatus,\n EmailTemplateId,\n EmailTemplateParams,\n JsonValue,\n} from '../types/index.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { CredentialStore, DomainEvent, Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { WebhookSender } from './ports.js';\n\nexport const MESSAGE_EXCERPT = 140;\nexport const POST_EXCERPT = 280;\nexport const MESSAGE_BATCH_MS = 15 * 60 * 1000;\n\nexport interface EmailRecipient<K extends EmailTemplateId = EmailTemplateId> {\n to: string;\n orgUserId: string;\n userId: string;\n params: EmailTemplateParams[K];\n}\n\nexport interface CommunityMailReads {\n communityTitle(orgId: string, communityId: string): Promise<string | null>;\n space(\n orgId: string,\n spaceId: string,\n ): Promise<{ name: string; communityId: string; capacity: number | null } | null>;\n openSpaceNames(orgId: string, orgUserId: string, communityId: string): Promise<string[]>;\n communitiesOpenedBy(\n orgId: string,\n grant: { contentId: string | null; bundleId: string | null },\n ): Promise<string[]>;\n communityMembersAmong(orgId: string, communityId: string, ids: string[]): Promise<string[]>;\n spaceOpenersAmong(orgId: string, spaceId: string, ids: string[]): Promise<string[]>;\n spaceOpeners(orgId: string, spaceId: string): Promise<string[]>;\n moderationAudience(orgId: string, communityId: string): Promise<string[]>;\n staffIds(orgId: string): Promise<string[]>;\n displayName(orgId: string, orgUserId: string): Promise<string>;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n comment(\n orgId: string,\n commentId: string,\n ): Promise<{\n orgUserId: string;\n postId: string | null;\n activityId: string | null;\n parentId: string | null;\n body: string;\n } | null>;\n post(\n orgId: string,\n postId: string,\n ): Promise<{\n title: string;\n body: string;\n spaceId: string;\n orgUserId: string;\n communityId: string;\n } | null>;\n chat(\n orgId: string,\n chatId: string,\n ): Promise<{\n communityId: string;\n kind: 'direct' | 'group';\n status: ChatStatus;\n createdBy: string | null;\n participants: {\n orgUserId: string;\n muted: boolean;\n mutedUntil: Date | null;\n leftAt: Date | null;\n }[];\n } | null>;\n latestMessageBody(orgId: string, chatId: string): Promise<string | null>;\n pairBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n activeMemberCount(orgId: string, spaceId: string): Promise<number>;\n memberCommunityTitles(orgId: string, orgUserId: string): Promise<{ id: string; title: string }[]>;\n recentMessageRecipients(\n orgId: string,\n automationId: string,\n chatId: string,\n since: Date,\n ): Promise<string[]>;\n}\n\nexport interface GrantWriter {\n grant(orgId: string, input: GrantEntitlementInput): Promise<unknown>;\n}\n\nexport interface ActionDeps {\n mailer: Pick<Mailer, 'send'>;\n lookups: MailerLookups;\n credentials: Pick<CredentialStore, 'reveal'>;\n webhooks: WebhookSender;\n mail: CommunityMailReads;\n grants: GrantWriter;\n automationId: string;\n actionInput: Record<string, JsonValue>;\n logger?: Logger;\n}\n\nexport const emptyCommunityMail: CommunityMailReads = {\n communityTitle: async () => null,\n space: async () => null,\n openSpaceNames: async () => [],\n communitiesOpenedBy: async () => [],\n communityMembersAmong: async () => [],\n spaceOpenersAmong: async () => [],\n spaceOpeners: async () => [],\n moderationAudience: async () => [],\n staffIds: async () => [],\n displayName: async () => 'A member',\n followerIds: async () => [],\n comment: async () => null,\n post: async () => null,\n chat: async () => null,\n latestMessageBody: async () => null,\n pairBlocked: async () => false,\n activeMemberCount: async () => 0,\n memberCommunityTitles: async () => [],\n recentMessageRecipients: async () => [],\n};\n\nexport function actorOf(event: DomainEvent): string | undefined {\n const value = event.metadata?.actorOrgUserId;\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function previousStatusOf(event: DomainEvent): string | undefined {\n const value = event.metadata?.previousStatus;\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function excerpt(value: string, max: number): string {\n const flat = value.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max);\n}\n\nexport function seededCommunityId(deps: ActionDeps): string | null {\n const id = deps.actionInput['communityId'];\n return typeof id === 'string' ? id : null;\n}\n\nexport async function address<K extends EmailTemplateId>(\n deps: ActionDeps,\n event: DomainEvent,\n orgId: string,\n orgUserIds: string[],\n params: EmailTemplateParams[K],\n): Promise<EmailRecipient<K>[] | undefined> {\n const actor = actorOf(event);\n const wanted = [...new Set(orgUserIds)].filter((id) => id !== actor);\n if (wanted.length === 0) {\n return [];\n }\n const recipients: EmailRecipient<K>[] = [];\n const logger = deps.logger ?? noopLogger;\n for (const orgUserId of wanted) {\n const person = await deps.lookups.orgUserPerson(orgId, orgUserId);\n if (!person) {\n logger.warn('email skipped: recipient has no address', { orgId, orgUserId });\n continue;\n }\n recipients.push({ to: person.email, orgUserId, userId: person.userId, params });\n }\n return recipients;\n}\n"],"mappings":";;AAYA,MAAa,kBAAkB;AAC/B,MAAa,eAAe;AAC5B,MAAa,mBAAmB,MAAU;AA0F1C,MAAa,qBAAyC;CACpD,gBAAgB,YAAY;CAC5B,OAAO,YAAY;CACnB,gBAAgB,YAAY,CAAC;CAC7B,qBAAqB,YAAY,CAAC;CAClC,uBAAuB,YAAY,CAAC;CACpC,mBAAmB,YAAY,CAAC;CAChC,cAAc,YAAY,CAAC;CAC3B,oBAAoB,YAAY,CAAC;CACjC,UAAU,YAAY,CAAC;CACvB,aAAa,YAAY;CACzB,aAAa,YAAY,CAAC;CAC1B,SAAS,YAAY;CACrB,MAAM,YAAY;CAClB,MAAM,YAAY;CAClB,mBAAmB,YAAY;CAC/B,aAAa,YAAY;CACzB,mBAAmB,YAAY;CAC/B,uBAAuB,YAAY,CAAC;CACpC,yBAAyB,YAAY,CAAC;AACxC;AAEA,SAAgB,QAAQ,OAAwC;CAC9D,MAAM,QAAQ,MAAM,UAAU;CAC9B,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAgB,iBAAiB,OAAwC;CACvE,MAAM,QAAQ,MAAM,UAAU;CAC9B,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAgB,QAAQ,OAAe,KAAqB;CAC1D,MAAM,OAAO,MAAM,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;CAC7C,OAAO,KAAK,UAAU,MAAM,OAAO,KAAK,MAAM,GAAG,GAAG;AACtD;AAEA,SAAgB,kBAAkB,MAAiC;CACjE,MAAM,KAAK,KAAK,YAAY;CAC5B,OAAO,OAAO,OAAO,WAAW,KAAK;AACvC;AAEA,eAAsB,QACpB,MACA,OACA,OACA,YACA,QAC0C;CAC1C,MAAM,QAAQ,QAAQ,KAAK;CAC3B,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,QAAQ,OAAO,OAAO,KAAK;CACnE,IAAI,OAAO,WAAW,GACpB,OAAO,CAAC;CAEV,MAAM,aAAkC,CAAC;CACzC,MAAM,SAAS,KAAK,UAAU;CAC9B,KAAK,MAAM,aAAa,QAAQ;EAC9B,MAAM,SAAS,MAAM,KAAK,QAAQ,cAAc,OAAO,SAAS;EAChE,IAAI,CAAC,QAAQ;GACX,OAAO,KAAK,2CAA2C;IAAE;IAAO;GAAU,CAAC;GAC3E;EACF;EACA,WAAW,KAAK;GAAE,IAAI,OAAO;GAAO;GAAW,QAAQ,OAAO;GAAQ;EAAO,CAAC;CAChF;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"community-mail.js","names":[],"sources":["../../src/automations/community-mail.ts"],"sourcesContent":["import type { GrantEntitlementInput } from '../entitlements/index.js';\nimport type {\n ChatStatus,\n ContentType,\n EmailTemplateId,\n EmailTemplateParams,\n JsonValue,\n Permission,\n} from '../types/index.js';\nimport { PERMISSIONS, withImplied } from '../organizations/index.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { CredentialStore, DomainEvent, Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { WebhookSender } from './ports.js';\n\nexport const MESSAGE_EXCERPT = 140;\nexport const POST_EXCERPT = 280;\nexport const MESSAGE_BATCH_MS = 15 * 60 * 1000;\n\nexport interface EmailRecipient<K extends EmailTemplateId = EmailTemplateId> {\n to: string;\n orgUserId: string;\n userId: string;\n params: EmailTemplateParams[K];\n}\n\nexport interface CommunityMailReads {\n communityTitle(orgId: string, communityId: string): Promise<string | null>;\n space(\n orgId: string,\n spaceId: string,\n ): Promise<{ name: string; communityId: string; capacity: number | null } | null>;\n openSpaceNames(orgId: string, orgUserId: string, communityId: string): Promise<string[]>;\n communitiesOpenedBy(\n orgId: string,\n grant: { contentId: string | null; bundleId: string | null },\n ): Promise<string[]>;\n communityMembersAmong(orgId: string, communityId: string, ids: string[]): Promise<string[]>;\n spaceOpenersAmong(orgId: string, spaceId: string, ids: string[]): Promise<string[]>;\n spaceOpeners(orgId: string, spaceId: string): Promise<string[]>;\n holders(orgId: string, permission: Permission, on?: string): Promise<string[]>;\n role(orgId: string, roleKey: string): Promise<{ name: string; permissions: Permission[] } | null>;\n heldOnOrg(orgId: string, orgUserId: string, permissions: Permission[]): Promise<Permission[]>;\n orgName(orgId: string): Promise<string | null>;\n contentInfo(\n orgId: string,\n item: { contentId: string } | { bundleId: string },\n ): Promise<{ title: string; type: string } | null>;\n displayName(orgId: string, orgUserId: string): Promise<string>;\n postText(body: string): string;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n comment(\n orgId: string,\n commentId: string,\n ): Promise<{\n orgUserId: string;\n postId: string | null;\n activityId: string | null;\n parentId: string | null;\n body: string;\n } | null>;\n post(\n orgId: string,\n postId: string,\n ): Promise<{\n title: string;\n body: string;\n spaceId: string;\n orgUserId: string;\n communityId: string;\n } | null>;\n chat(\n orgId: string,\n chatId: string,\n ): Promise<{\n communityId: string;\n kind: 'direct' | 'group';\n status: ChatStatus;\n createdBy: string | null;\n participants: {\n orgUserId: string;\n muted: boolean;\n mutedUntil: Date | null;\n leftAt: Date | null;\n }[];\n } | null>;\n latestMessageBody(orgId: string, chatId: string): Promise<string | null>;\n pairBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n activeMemberCount(orgId: string, spaceId: string): Promise<number>;\n memberCommunityTitles(orgId: string, orgUserId: string): Promise<{ id: string; title: string }[]>;\n recentMessageRecipients(\n orgId: string,\n automationId: string,\n chatId: string,\n since: Date,\n ): Promise<string[]>;\n}\n\nexport interface GrantWriter {\n grant(orgId: string, input: GrantEntitlementInput): Promise<unknown>;\n}\n\nexport interface ActionDeps {\n mailer: Pick<Mailer, 'send'>;\n lookups: MailerLookups;\n credentials: Pick<CredentialStore, 'reveal'>;\n webhooks: WebhookSender;\n mail: CommunityMailReads;\n grants: GrantWriter;\n automationId: string;\n actionInput: Record<string, JsonValue>;\n logger?: Logger;\n}\n\nexport const emptyCommunityMail: CommunityMailReads = {\n communityTitle: async () => null,\n space: async () => null,\n openSpaceNames: async () => [],\n communitiesOpenedBy: async () => [],\n communityMembersAmong: async () => [],\n spaceOpenersAmong: async () => [],\n spaceOpeners: async () => [],\n holders: async () => [],\n role: async () => null,\n heldOnOrg: async () => [],\n orgName: async () => null,\n contentInfo: async () => null,\n displayName: async () => 'A member',\n postText: (body) => body,\n followerIds: async () => [],\n comment: async () => null,\n post: async () => null,\n chat: async () => null,\n latestMessageBody: async () => null,\n pairBlocked: async () => false,\n activeMemberCount: async () => 0,\n memberCommunityTitles: async () => [],\n recentMessageRecipients: async () => [],\n};\n\nexport async function communityModerators(\n reads: Pick<CommunityMailReads, 'holders' | 'communityMembersAmong'>,\n orgId: string,\n communityId: string,\n): Promise<string[]> {\n const holders = await reads.holders(orgId, 'communities:moderate', communityId);\n return holders.length === 0 ? [] : reads.communityMembersAmong(orgId, communityId, holders);\n}\n\nexport interface RoleGain {\n roleName: string;\n place: string;\n contentId: string | null;\n contentType: ContentType | null;\n}\n\nfunction contributes(\n permissions: readonly Permission[],\n contentType: ContentType | null,\n): Set<Permission> {\n const expanded = [...withImplied(permissions)];\n return new Set(\n contentType === null\n ? expanded\n : expanded.filter((permission) => PERMISSIONS[permission].about === contentType),\n );\n}\n\nexport async function roleGain(\n reads: Pick<CommunityMailReads, 'role' | 'orgName' | 'contentInfo' | 'heldOnOrg'>,\n orgId: string,\n assigned: {\n orgUserId: string;\n roleKey: string;\n previousRoleKey: string | null;\n contentId: string | null;\n contentType: ContentType | null;\n },\n): Promise<RoleGain | null> {\n const next = await reads.role(orgId, assigned.roleKey);\n if (!next) {\n return null;\n }\n const previous = assigned.previousRoleKey\n ? await reads.role(orgId, assigned.previousRoleKey)\n : null;\n const before = contributes(previous?.permissions ?? [], assigned.contentType);\n const offered = [...contributes(next.permissions, assigned.contentType)].filter(\n (permission) => !before.has(permission),\n );\n const fromOrg =\n assigned.contentId === null || offered.length === 0\n ? []\n : await reads.heldOnOrg(orgId, assigned.orgUserId, offered);\n const gained = offered.some((permission) => !fromOrg.includes(permission));\n if (!gained) {\n return null;\n }\n const place =\n assigned.contentId === null\n ? `in ${(await reads.orgName(orgId)) ?? 'the organization'}`\n : `of ${(await reads.contentInfo(orgId, { contentId: assigned.contentId }))?.title ?? 'an item'}`;\n return {\n roleName: next.name,\n place,\n contentId: assigned.contentId,\n contentType: assigned.contentType,\n };\n}\n\nexport function actorOf(event: DomainEvent): string | undefined {\n const value = event.metadata?.actorId;\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function previousStatusOf(event: DomainEvent): string | undefined {\n const value = event.metadata?.previousStatus;\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function excerpt(value: string, max: number): string {\n const flat = value.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max);\n}\n\nexport function seededCommunityId(deps: ActionDeps): string | null {\n const id = deps.actionInput['communityId'];\n return typeof id === 'string' ? id : null;\n}\n\nexport async function address<K extends EmailTemplateId>(\n deps: ActionDeps,\n event: DomainEvent,\n orgId: string,\n orgUserIds: string[],\n params: EmailTemplateParams[K],\n): Promise<EmailRecipient<K>[] | undefined> {\n const actor = actorOf(event);\n const wanted = [...new Set(orgUserIds)].filter((id) => id !== actor);\n if (wanted.length === 0) {\n return [];\n }\n const recipients: EmailRecipient<K>[] = [];\n const logger = deps.logger ?? noopLogger;\n for (const orgUserId of wanted) {\n const person = await deps.lookups.orgUserPerson(orgId, orgUserId);\n if (!person) {\n logger.warn('email skipped: recipient has no address', { orgId, orgUserId });\n continue;\n }\n recipients.push({ to: person.email, orgUserId, userId: person.userId, params });\n }\n return recipients;\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Permission } from "../types/schemas/organizations.js";
2
+ import "../types/index.js";
3
+ import { Actor } from "../shared/actor.js";
4
+ //#region src/automations/event-permissions.d.ts
5
+ declare function eventReadPermissions(type: string): readonly Permission[];
6
+ declare function assertReadsEvents(actor: Actor, types: readonly string[]): void;
7
+ declare function assertGrantsAllowed(actor: Actor, actions: readonly {
8
+ type: string;
9
+ }[]): void;
10
+ //#endregion
11
+ export { assertGrantsAllowed, assertReadsEvents, eventReadPermissions };
12
+ //# sourceMappingURL=event-permissions.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { ForbiddenError } from "../shared/errors.js";
2
+ //#region src/automations/event-permissions.ts
3
+ const BY_PREFIX = [
4
+ ["organization.role.", ["roles:read", "people:read"]],
5
+ ["organization.ownership.", ["people:read"]],
6
+ ["organization.created", ["organizations:read"]],
7
+ ["organization.updated", ["organizations:read"]],
8
+ ["organization.deleted", ["organizations:read"]],
9
+ ["organization.", ["people:read"]],
10
+ ["identity.", ["people:read"]],
11
+ ["entitlement.space_member.", ["communities:moderate"]],
12
+ ["entitlement.", ["access:read"]],
13
+ ["progress.", ["progress:read"]],
14
+ ["content.course.", ["courses:read"]],
15
+ ["content.download.", ["downloads:read"]],
16
+ ["content.bundle.", ["bundles:read"]],
17
+ ["content.community.", ["communities:read"]],
18
+ ["content.event.", ["events:read"]],
19
+ ["discussion.comment.", ["courses:moderate", "communities:moderate"]],
20
+ ["discussion.", ["communities:moderate"]],
21
+ ["asset.", ["media:read"]],
22
+ ["integration.", ["integrations:read"]],
23
+ ["automation.", ["automations:read"]]
24
+ ];
25
+ function eventReadPermissions(type) {
26
+ return BY_PREFIX.find(([prefix]) => type.startsWith(prefix))?.[1] ?? ["people:read"];
27
+ }
28
+ function assertReadsEvents(actor, types) {
29
+ const missing = [...new Set(types.flatMap((type) => eventReadPermissions(type)))].filter((permission) => !actor.permissions.has(permission));
30
+ if (missing.length > 0) throw Object.assign(new ForbiddenError(`You need ${missing.join(", ")} to receive these events`), { permission: missing[0] });
31
+ }
32
+ function assertGrantsAllowed(actor, actions) {
33
+ if (actions.some((action) => action.type === "grantAccess")) actor.permissions.require("access:write");
34
+ }
35
+ //#endregion
36
+ export { assertGrantsAllowed, assertReadsEvents, eventReadPermissions };
37
+
38
+ //# sourceMappingURL=event-permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-permissions.js","names":[],"sources":["../../src/automations/event-permissions.ts"],"sourcesContent":["import type { Permission } from '../types/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport { ForbiddenError } from '../shared/errors.js';\n\nconst BY_PREFIX: ReadonlyArray<[string, readonly Permission[]]> = [\n ['organization.role.', ['roles:read', 'people:read']],\n ['organization.ownership.', ['people:read']],\n ['organization.created', ['organizations:read']],\n ['organization.updated', ['organizations:read']],\n ['organization.deleted', ['organizations:read']],\n ['organization.', ['people:read']],\n ['identity.', ['people:read']],\n ['entitlement.space_member.', ['communities:moderate']],\n ['entitlement.', ['access:read']],\n ['progress.', ['progress:read']],\n ['content.course.', ['courses:read']],\n ['content.download.', ['downloads:read']],\n ['content.bundle.', ['bundles:read']],\n ['content.community.', ['communities:read']],\n ['content.event.', ['events:read']],\n ['discussion.comment.', ['courses:moderate', 'communities:moderate']],\n ['discussion.', ['communities:moderate']],\n ['asset.', ['media:read']],\n ['integration.', ['integrations:read']],\n ['automation.', ['automations:read']],\n];\n\nexport function eventReadPermissions(type: string): readonly Permission[] {\n return BY_PREFIX.find(([prefix]) => type.startsWith(prefix))?.[1] ?? ['people:read'];\n}\n\nexport function assertReadsEvents(actor: Actor, types: readonly string[]): void {\n const missing = [...new Set(types.flatMap((type) => eventReadPermissions(type)))].filter(\n (permission) => !actor.permissions.has(permission),\n );\n if (missing.length > 0) {\n throw Object.assign(\n new ForbiddenError(`You need ${missing.join(', ')} to receive these events`),\n {\n permission: missing[0],\n },\n );\n }\n}\n\nexport function assertGrantsAllowed(actor: Actor, actions: readonly { type: string }[]): void {\n if (actions.some((action) => action.type === 'grantAccess')) {\n actor.permissions.require('access:write');\n }\n}\n"],"mappings":";;AAIA,MAAM,YAA4D;CAChE,CAAC,sBAAsB,CAAC,cAAc,aAAa,CAAC;CACpD,CAAC,2BAA2B,CAAC,aAAa,CAAC;CAC3C,CAAC,wBAAwB,CAAC,oBAAoB,CAAC;CAC/C,CAAC,wBAAwB,CAAC,oBAAoB,CAAC;CAC/C,CAAC,wBAAwB,CAAC,oBAAoB,CAAC;CAC/C,CAAC,iBAAiB,CAAC,aAAa,CAAC;CACjC,CAAC,aAAa,CAAC,aAAa,CAAC;CAC7B,CAAC,6BAA6B,CAAC,sBAAsB,CAAC;CACtD,CAAC,gBAAgB,CAAC,aAAa,CAAC;CAChC,CAAC,aAAa,CAAC,eAAe,CAAC;CAC/B,CAAC,mBAAmB,CAAC,cAAc,CAAC;CACpC,CAAC,qBAAqB,CAAC,gBAAgB,CAAC;CACxC,CAAC,mBAAmB,CAAC,cAAc,CAAC;CACpC,CAAC,sBAAsB,CAAC,kBAAkB,CAAC;CAC3C,CAAC,kBAAkB,CAAC,aAAa,CAAC;CAClC,CAAC,uBAAuB,CAAC,oBAAoB,sBAAsB,CAAC;CACpE,CAAC,eAAe,CAAC,sBAAsB,CAAC;CACxC,CAAC,UAAU,CAAC,YAAY,CAAC;CACzB,CAAC,gBAAgB,CAAC,mBAAmB,CAAC;CACtC,CAAC,eAAe,CAAC,kBAAkB,CAAC;AACtC;AAEA,SAAgB,qBAAqB,MAAqC;CACxE,OAAO,UAAU,MAAM,CAAC,YAAY,KAAK,WAAW,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,aAAa;AACrF;AAEA,SAAgB,kBAAkB,OAAc,OAAgC;CAC9E,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,MAAM,SAAS,SAAS,qBAAqB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAC/E,eAAe,CAAC,MAAM,YAAY,IAAI,UAAU,CACnD;CACA,IAAI,QAAQ,SAAS,GACnB,MAAM,OAAO,OACX,IAAI,eAAe,YAAY,QAAQ,KAAK,IAAI,EAAE,yBAAyB,GAC3E,EACE,YAAY,QAAQ,GACtB,CACF;AAEJ;AAEA,SAAgB,oBAAoB,OAAc,SAA4C;CAC5F,IAAI,QAAQ,MAAM,WAAW,OAAO,SAAS,aAAa,GACxD,MAAM,YAAY,QAAQ,cAAc;AAE5C"}
@@ -15,4 +15,5 @@ import { CHAT_QUIET_MS, FLUSH_JOB, SchedulerChatDebounce, flushKey, notification
15
15
  import { RECIPIENT_RULES } from "./notifications/rules/index.js";
16
16
  import { NOTIFICATION_TYPES } from "./notifications/types.js";
17
17
  import { DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES } from "./notifications/defaults.js";
18
- export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, automationEvents, cronMatches, flushKey, notificationJobs };
18
+ import { applicableCategories } from "./notifications/applicable.js";
19
+ export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
@@ -9,4 +9,5 @@ import { CHAT_QUIET_MS, FLUSH_JOB, SchedulerChatDebounce, flushKey, notification
9
9
  import { RECIPIENT_RULES } from "./notifications/rules/index.js";
10
10
  import { NOTIFICATION_TYPES } from "./notifications/types.js";
11
11
  import { DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES } from "./notifications/defaults.js";
12
- export { ACTION_FAILED, ActionFailedError, AutomationKindMismatchError, AutomationsServiceImpl, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, FLUSH_JOB, IN_APP, InvalidTriggerError, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery, NotificationProcessor, NotificationsServiceImpl, PUSH, RECIPIENT_RULES, ReservedActionError, SchedulerChatDebounce, actionFailureMessage, automationEvents, cronMatches, flushKey, notificationJobs };
12
+ import { applicableCategories } from "./notifications/applicable.js";
13
+ export { ACTION_FAILED, ActionFailedError, AutomationKindMismatchError, AutomationsServiceImpl, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, FLUSH_JOB, IN_APP, InvalidTriggerError, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery, NotificationProcessor, NotificationsServiceImpl, PUSH, RECIPIENT_RULES, ReservedActionError, SchedulerChatDebounce, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
@@ -0,0 +1,8 @@
1
+ import { NotificationCategory, SettingsScope } from "../../types/notifications.js";
2
+ import "../../types/index.js";
3
+ import { EffectivePermissions } from "../../shared/actor.js";
4
+ //#region src/automations/notifications/applicable.d.ts
5
+ declare function applicableCategories(categories: NotificationCategory[], permissions: EffectivePermissions, scope: SettingsScope): NotificationCategory[];
6
+ //#endregion
7
+ export { applicableCategories };
8
+ //# sourceMappingURL=applicable.d.ts.map
@@ -0,0 +1,12 @@
1
+ //#region src/automations/notifications/applicable.ts
2
+ function reasonApplies(reason, permissions, scope) {
3
+ if (reason !== "moderator") return true;
4
+ return scope === "org" ? permissions.anywhere("courses:moderate") : permissions.has("communities:moderate", scope.communityId);
5
+ }
6
+ function applicableCategories(categories, permissions, scope) {
7
+ return categories.filter((category) => category.covers.some((cover) => cover.reasons.some((reason) => reasonApplies(reason, permissions, scope))));
8
+ }
9
+ //#endregion
10
+ export { applicableCategories };
11
+
12
+ //# sourceMappingURL=applicable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applicable.js","names":[],"sources":["../../../src/automations/notifications/applicable.ts"],"sourcesContent":["import type { EffectivePermissions } from '../../shared/actor.js';\nimport type { NotificationCategory, Reason, SettingsScope } from '../../types/index.js';\n\nfunction reasonApplies(\n reason: Reason,\n permissions: EffectivePermissions,\n scope: SettingsScope,\n): boolean {\n if (reason !== 'moderator') {\n return true;\n }\n return scope === 'org'\n ? permissions.anywhere('courses:moderate')\n : permissions.has('communities:moderate', scope.communityId);\n}\n\nexport function applicableCategories(\n categories: NotificationCategory[],\n permissions: EffectivePermissions,\n scope: SettingsScope,\n): NotificationCategory[] {\n return categories.filter((category) =>\n category.covers.some((cover) =>\n cover.reasons.some((reason) => reasonApplies(reason, permissions, scope)),\n ),\n );\n}\n"],"mappings":";AAGA,SAAS,cACP,QACA,aACA,OACS;CACT,IAAI,WAAW,aACb,OAAO;CAET,OAAO,UAAU,QACb,YAAY,SAAS,kBAAkB,IACvC,YAAY,IAAI,wBAAwB,MAAM,WAAW;AAC/D;AAEA,SAAgB,qBACd,YACA,aACA,OACwB;CACxB,OAAO,WAAW,QAAQ,aACxB,SAAS,OAAO,MAAM,UACpB,MAAM,QAAQ,MAAM,WAAW,cAAc,QAAQ,aAAa,KAAK,CAAC,CAC1E,CACF;AACF"}
@@ -61,7 +61,7 @@ function notificationJobs(deps) {
61
61
  await deps.processor.file(orgId, `chat-flush:${chatId}:${latest.messageId}`, [{
62
62
  type: "message.sent",
63
63
  scope: "chat",
64
- actorOrgUserId: latest.senderOrgUserId,
64
+ actorId: latest.senderOrgUserId,
65
65
  communityId: chat.communityId,
66
66
  spaceId: null,
67
67
  subject: {
@@ -1 +1 @@
1
- {"version":3,"file":"debounce.js","names":[],"sources":["../../../src/automations/notifications/debounce.ts"],"sourcesContent":["import { z } from 'zod';\nimport { idSchema } from '../../types/schemas/index.js';\nimport type { RecipientList } from '../../types/index.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport type { Logger, ScheduledJob, Scheduler } from '../../shared/ports.js';\nimport { MESSAGE_EXCERPT } from '../community-mail.js';\nimport type { ChatDebounce, NotificationReads } from '../ports.js';\nimport { chatOf, type NotificationProcessor } from './processor.js';\nimport { messageSummary } from './rules/discussion.js';\nimport { chatReach, chatTitle, Reasons, recipientsOf, textExcerpt } from './rules/recipients.js';\n\nexport const FLUSH_JOB = 'automations.notifications.flush';\nexport const CHAT_QUIET_MS = 2 * 60 * 1000;\n\nconst flushPayload = z\n .object({ orgId: idSchema, userId: idSchema, orgUserId: idSchema, chatId: idSchema })\n .strict();\n\nexport function flushKey(userId: string, chatId: string): string {\n return `${userId}:${chatId}`;\n}\n\nexport class SchedulerChatDebounce implements ChatDebounce {\n constructor(private readonly scheduler: Pick<Scheduler, 'at' | 'cancel'>) {}\n\n async arm(orgId: string, list: RecipientList, occurredAt: Date): Promise<void> {\n const chatId = chatOf(list);\n if (!chatId) {\n return;\n }\n const runAt = new Date(occurredAt.getTime() + CHAT_QUIET_MS);\n for (const recipient of list.recipients ?? []) {\n await this.scheduler.at(FLUSH_JOB, runAt, flushKey(recipient.userId, chatId), {\n orgId,\n userId: recipient.userId,\n orgUserId: recipient.orgUserId,\n chatId,\n });\n }\n }\n\n async mentioned(_orgId: string, userId: string, chatId: string): Promise<void> {\n await this.scheduler.cancel(FLUSH_JOB, flushKey(userId, chatId));\n }\n}\n\nexport interface NotificationJobDeps {\n processor: Pick<NotificationProcessor, 'file'>;\n reads: NotificationReads;\n logger?: Logger;\n}\n\nexport function notificationJobs(deps: NotificationJobDeps): ScheduledJob[] {\n const logger = deps.logger ?? noopLogger;\n return [\n {\n job: FLUSH_JOB,\n async run(payload, fire) {\n const parsed = flushPayload.safeParse(payload);\n if (!parsed.success) {\n logger.error('chat flush payload refused', { job: FLUSH_JOB, fireId: fire.id });\n return;\n }\n const { orgId, orgUserId, chatId } = parsed.data;\n const unread = await deps.reads.unreadInChat(orgId, chatId, orgUserId);\n if (unread.count === 0 || !unread.latest) {\n return;\n }\n const chat = await deps.reads.chat(orgId, chatId);\n if (!chat) {\n return;\n }\n const latest = unread.latest;\n const recipients = await recipientsOf(\n deps.reads,\n orgId,\n new Reasons().add([orgUserId], 'participant'),\n chatReach(deps.reads, orgId, chat, chatId, latest.senderOrgUserId, fire.startedAt),\n );\n if (recipients.length === 0) {\n return;\n }\n await deps.processor.file(\n orgId,\n `chat-flush:${chatId}:${latest.messageId}`,\n [\n {\n type: 'message.sent',\n scope: 'chat',\n actorOrgUserId: latest.senderOrgUserId,\n communityId: chat.communityId,\n spaceId: null,\n subject: { type: 'message', id: latest.messageId },\n parent: { type: 'chat', id: chatId },\n display: {\n senderName: await deps.reads.displayName(orgId, latest.senderOrgUserId),\n chatTitle: await chatTitle(deps.reads, orgId, chat, latest.senderOrgUserId),\n excerpt: await textExcerpt(deps.reads, orgId, latest.body, MESSAGE_EXCERPT),\n count: String(unread.count),\n summary: messageSummary(unread.count),\n },\n recipients,\n },\n ],\n { occurredAt: fire.startedAt, debounce: false },\n );\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;AAWA,MAAa,YAAY;AACzB,MAAa,gBAAgB,MAAS;AAEtC,MAAM,eAAe,EAClB,OAAO;CAAE,OAAO;CAAU,QAAQ;CAAU,WAAW;CAAU,QAAQ;AAAS,CAAC,CAAC,CACpF,OAAO;AAEV,SAAgB,SAAS,QAAgB,QAAwB;CAC/D,OAAO,GAAG,OAAO,GAAG;AACtB;AAEA,IAAa,wBAAb,MAA2D;CAC5B;CAA7B,YAAY,WAA8D;EAA7C,KAAA,YAAA;CAA8C;CAE3E,MAAM,IAAI,OAAe,MAAqB,YAAiC;EAC7E,MAAM,SAAS,OAAO,IAAI;EAC1B,IAAI,CAAC,QACH;EAEF,MAAM,QAAQ,IAAI,KAAK,WAAW,QAAQ,IAAI,aAAa;EAC3D,KAAK,MAAM,aAAa,KAAK,cAAc,CAAC,GAC1C,MAAM,KAAK,UAAU,GAAG,WAAW,OAAO,SAAS,UAAU,QAAQ,MAAM,GAAG;GAC5E;GACA,QAAQ,UAAU;GAClB,WAAW,UAAU;GACrB;EACF,CAAC;CAEL;CAEA,MAAM,UAAU,QAAgB,QAAgB,QAA+B;EAC7E,MAAM,KAAK,UAAU,OAAO,WAAW,SAAS,QAAQ,MAAM,CAAC;CACjE;AACF;AAQA,SAAgB,iBAAiB,MAA2C;CAC1E,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO,CACL;EACE,KAAK;EACL,MAAM,IAAI,SAAS,MAAM;GACvB,MAAM,SAAS,aAAa,UAAU,OAAO;GAC7C,IAAI,CAAC,OAAO,SAAS;IACnB,OAAO,MAAM,8BAA8B;KAAE,KAAK;KAAW,QAAQ,KAAK;IAAG,CAAC;IAC9E;GACF;GACA,MAAM,EAAE,OAAO,WAAW,WAAW,OAAO;GAC5C,MAAM,SAAS,MAAM,KAAK,MAAM,aAAa,OAAO,QAAQ,SAAS;GACrE,IAAI,OAAO,UAAU,KAAK,CAAC,OAAO,QAChC;GAEF,MAAM,OAAO,MAAM,KAAK,MAAM,KAAK,OAAO,MAAM;GAChD,IAAI,CAAC,MACH;GAEF,MAAM,SAAS,OAAO;GACtB,MAAM,aAAa,MAAM,aACvB,KAAK,OACL,OACA,IAAI,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,aAAa,GAC5C,UAAU,KAAK,OAAO,OAAO,MAAM,QAAQ,OAAO,iBAAiB,KAAK,SAAS,CACnF;GACA,IAAI,WAAW,WAAW,GACxB;GAEF,MAAM,KAAK,UAAU,KACnB,OACA,cAAc,OAAO,GAAG,OAAO,aAC/B,CACE;IACE,MAAM;IACN,OAAO;IACP,gBAAgB,OAAO;IACvB,aAAa,KAAK;IAClB,SAAS;IACT,SAAS;KAAE,MAAM;KAAW,IAAI,OAAO;IAAU;IACjD,QAAQ;KAAE,MAAM;KAAQ,IAAI;IAAO;IACnC,SAAS;KACP,YAAY,MAAM,KAAK,MAAM,YAAY,OAAO,OAAO,eAAe;KACtE,WAAW,MAAM,UAAU,KAAK,OAAO,OAAO,MAAM,OAAO,eAAe;KAC1E,SAAS,MAAM,YAAY,KAAK,OAAO,OAAO,OAAO,MAAA,GAAqB;KAC1E,OAAO,OAAO,OAAO,KAAK;KAC1B,SAAS,eAAe,OAAO,KAAK;IACtC;IACA;GACF,CACF,GACA;IAAE,YAAY,KAAK;IAAW,UAAU;GAAM,CAChD;EACF;CACF,CACF;AACF"}
1
+ {"version":3,"file":"debounce.js","names":[],"sources":["../../../src/automations/notifications/debounce.ts"],"sourcesContent":["import { z } from 'zod';\nimport { idSchema } from '../../types/schemas/index.js';\nimport type { RecipientList } from '../../types/index.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport type { Logger, ScheduledJob, Scheduler } from '../../shared/ports.js';\nimport { MESSAGE_EXCERPT } from '../community-mail.js';\nimport type { ChatDebounce, NotificationReads } from '../ports.js';\nimport { chatOf, type NotificationProcessor } from './processor.js';\nimport { messageSummary } from './rules/discussion.js';\nimport { chatReach, chatTitle, Reasons, recipientsOf, textExcerpt } from './rules/recipients.js';\n\nexport const FLUSH_JOB = 'automations.notifications.flush';\nexport const CHAT_QUIET_MS = 2 * 60 * 1000;\n\nconst flushPayload = z\n .object({ orgId: idSchema, userId: idSchema, orgUserId: idSchema, chatId: idSchema })\n .strict();\n\nexport function flushKey(userId: string, chatId: string): string {\n return `${userId}:${chatId}`;\n}\n\nexport class SchedulerChatDebounce implements ChatDebounce {\n constructor(private readonly scheduler: Pick<Scheduler, 'at' | 'cancel'>) {}\n\n async arm(orgId: string, list: RecipientList, occurredAt: Date): Promise<void> {\n const chatId = chatOf(list);\n if (!chatId) {\n return;\n }\n const runAt = new Date(occurredAt.getTime() + CHAT_QUIET_MS);\n for (const recipient of list.recipients ?? []) {\n await this.scheduler.at(FLUSH_JOB, runAt, flushKey(recipient.userId, chatId), {\n orgId,\n userId: recipient.userId,\n orgUserId: recipient.orgUserId,\n chatId,\n });\n }\n }\n\n async mentioned(_orgId: string, userId: string, chatId: string): Promise<void> {\n await this.scheduler.cancel(FLUSH_JOB, flushKey(userId, chatId));\n }\n}\n\nexport interface NotificationJobDeps {\n processor: Pick<NotificationProcessor, 'file'>;\n reads: NotificationReads;\n logger?: Logger;\n}\n\nexport function notificationJobs(deps: NotificationJobDeps): ScheduledJob[] {\n const logger = deps.logger ?? noopLogger;\n return [\n {\n job: FLUSH_JOB,\n async run(payload, fire) {\n const parsed = flushPayload.safeParse(payload);\n if (!parsed.success) {\n logger.error('chat flush payload refused', { job: FLUSH_JOB, fireId: fire.id });\n return;\n }\n const { orgId, orgUserId, chatId } = parsed.data;\n const unread = await deps.reads.unreadInChat(orgId, chatId, orgUserId);\n if (unread.count === 0 || !unread.latest) {\n return;\n }\n const chat = await deps.reads.chat(orgId, chatId);\n if (!chat) {\n return;\n }\n const latest = unread.latest;\n const recipients = await recipientsOf(\n deps.reads,\n orgId,\n new Reasons().add([orgUserId], 'participant'),\n chatReach(deps.reads, orgId, chat, chatId, latest.senderOrgUserId, fire.startedAt),\n );\n if (recipients.length === 0) {\n return;\n }\n await deps.processor.file(\n orgId,\n `chat-flush:${chatId}:${latest.messageId}`,\n [\n {\n type: 'message.sent',\n scope: 'chat',\n actorId: latest.senderOrgUserId,\n communityId: chat.communityId,\n spaceId: null,\n subject: { type: 'message', id: latest.messageId },\n parent: { type: 'chat', id: chatId },\n display: {\n senderName: await deps.reads.displayName(orgId, latest.senderOrgUserId),\n chatTitle: await chatTitle(deps.reads, orgId, chat, latest.senderOrgUserId),\n excerpt: await textExcerpt(deps.reads, orgId, latest.body, MESSAGE_EXCERPT),\n count: String(unread.count),\n summary: messageSummary(unread.count),\n },\n recipients,\n },\n ],\n { occurredAt: fire.startedAt, debounce: false },\n );\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;AAWA,MAAa,YAAY;AACzB,MAAa,gBAAgB,MAAS;AAEtC,MAAM,eAAe,EAClB,OAAO;CAAE,OAAO;CAAU,QAAQ;CAAU,WAAW;CAAU,QAAQ;AAAS,CAAC,CAAC,CACpF,OAAO;AAEV,SAAgB,SAAS,QAAgB,QAAwB;CAC/D,OAAO,GAAG,OAAO,GAAG;AACtB;AAEA,IAAa,wBAAb,MAA2D;CAC5B;CAA7B,YAAY,WAA8D;EAA7C,KAAA,YAAA;CAA8C;CAE3E,MAAM,IAAI,OAAe,MAAqB,YAAiC;EAC7E,MAAM,SAAS,OAAO,IAAI;EAC1B,IAAI,CAAC,QACH;EAEF,MAAM,QAAQ,IAAI,KAAK,WAAW,QAAQ,IAAI,aAAa;EAC3D,KAAK,MAAM,aAAa,KAAK,cAAc,CAAC,GAC1C,MAAM,KAAK,UAAU,GAAG,WAAW,OAAO,SAAS,UAAU,QAAQ,MAAM,GAAG;GAC5E;GACA,QAAQ,UAAU;GAClB,WAAW,UAAU;GACrB;EACF,CAAC;CAEL;CAEA,MAAM,UAAU,QAAgB,QAAgB,QAA+B;EAC7E,MAAM,KAAK,UAAU,OAAO,WAAW,SAAS,QAAQ,MAAM,CAAC;CACjE;AACF;AAQA,SAAgB,iBAAiB,MAA2C;CAC1E,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO,CACL;EACE,KAAK;EACL,MAAM,IAAI,SAAS,MAAM;GACvB,MAAM,SAAS,aAAa,UAAU,OAAO;GAC7C,IAAI,CAAC,OAAO,SAAS;IACnB,OAAO,MAAM,8BAA8B;KAAE,KAAK;KAAW,QAAQ,KAAK;IAAG,CAAC;IAC9E;GACF;GACA,MAAM,EAAE,OAAO,WAAW,WAAW,OAAO;GAC5C,MAAM,SAAS,MAAM,KAAK,MAAM,aAAa,OAAO,QAAQ,SAAS;GACrE,IAAI,OAAO,UAAU,KAAK,CAAC,OAAO,QAChC;GAEF,MAAM,OAAO,MAAM,KAAK,MAAM,KAAK,OAAO,MAAM;GAChD,IAAI,CAAC,MACH;GAEF,MAAM,SAAS,OAAO;GACtB,MAAM,aAAa,MAAM,aACvB,KAAK,OACL,OACA,IAAI,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,aAAa,GAC5C,UAAU,KAAK,OAAO,OAAO,MAAM,QAAQ,OAAO,iBAAiB,KAAK,SAAS,CACnF;GACA,IAAI,WAAW,WAAW,GACxB;GAEF,MAAM,KAAK,UAAU,KACnB,OACA,cAAc,OAAO,GAAG,OAAO,aAC/B,CACE;IACE,MAAM;IACN,OAAO;IACP,SAAS,OAAO;IAChB,aAAa,KAAK;IAClB,SAAS;IACT,SAAS;KAAE,MAAM;KAAW,IAAI,OAAO;IAAU;IACjD,QAAQ;KAAE,MAAM;KAAQ,IAAI;IAAO;IACnC,SAAS;KACP,YAAY,MAAM,KAAK,MAAM,YAAY,OAAO,OAAO,eAAe;KACtE,WAAW,MAAM,UAAU,KAAK,OAAO,OAAO,MAAM,OAAO,eAAe;KAC1E,SAAS,MAAM,YAAY,KAAK,OAAO,OAAO,OAAO,MAAA,GAAqB;KAC1E,OAAO,OAAO,OAAO,KAAK;KAC1B,SAAS,eAAe,OAAO,KAAK;IACtC;IACA;GACF,CACF,GACA;IAAE,YAAY,KAAK;IAAW,UAAU;GAAM,CAChD;EACF;CACF,CACF;AACF"}
@@ -75,7 +75,7 @@ const DEFAULT_COMMUNITY_CATEGORIES = [
75
75
  },
76
76
  {
77
77
  type: "message.flagged",
78
- reasons: ["staff"]
78
+ reasons: ["moderator"]
79
79
  }
80
80
  ],
81
81
  channels: {
@@ -204,7 +204,7 @@ const DEFAULT_ORG_CATEGORIES = [
204
204
  {
205
205
  key: "about_you",
206
206
  covers: [{
207
- type: "moderator.assigned",
207
+ type: "role.assigned",
208
208
  reasons: ["subject"]
209
209
  }, {
210
210
  type: "comment.removed",
@@ -220,7 +220,7 @@ const DEFAULT_ORG_CATEGORIES = [
220
220
  key: "review",
221
221
  covers: [{
222
222
  type: "content.flagged",
223
- reasons: ["staff"]
223
+ reasons: ["moderator"]
224
224
  }],
225
225
  channels: {
226
226
  inApp: on,
@@ -297,6 +297,36 @@ const DEFAULT_ORG_CATEGORIES = [
297
297
  email: lockedOn
298
298
  }
299
299
  },
300
+ {
301
+ key: "events",
302
+ covers: [
303
+ {
304
+ type: "event.granted",
305
+ reasons: ["event_member"]
306
+ },
307
+ {
308
+ type: "event.starting_soon",
309
+ reasons: ["event_member"]
310
+ },
311
+ {
312
+ type: "event.changed",
313
+ reasons: ["event_member"]
314
+ },
315
+ {
316
+ type: "event.canceled",
317
+ reasons: ["event_member"]
318
+ },
319
+ {
320
+ type: "event.revoked",
321
+ reasons: ["event_member"]
322
+ }
323
+ ],
324
+ channels: {
325
+ inApp: lockedOn,
326
+ email: on,
327
+ push: on
328
+ }
329
+ },
300
330
  {
301
331
  key: "course_content",
302
332
  covers: [{
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.js","names":[],"sources":["../../../src/automations/notifications/defaults.ts"],"sourcesContent":["import type { NotificationCategory } from '../../types/index.js';\n\nconst on = { default: true, locked: false };\nconst off = { default: false, locked: false };\nconst lockedOn = { default: true, locked: true };\n\nexport const DEFAULT_COMMUNITY_CATEGORIES: NotificationCategory[] = [\n {\n key: 'about_you',\n covers: [\n { type: 'post.approved', reasons: ['subject'] },\n { type: 'post.removed', reasons: ['subject'] },\n { type: 'comment.removed', reasons: ['subject'] },\n { type: 'space.invited', reasons: ['subject'] },\n { type: 'space.request_approved', reasons: ['subject'] },\n { type: 'space.request_declined', reasons: ['subject'] },\n { type: 'community.welcome', reasons: ['subject'] },\n { type: 'community.ending_soon', reasons: ['subject'] },\n { type: 'chat.added', reasons: ['subject'] },\n ],\n channels: { inApp: lockedOn, email: on, push: on },\n },\n {\n key: 'review',\n covers: [\n { type: 'post.awaiting_review', reasons: ['moderator'] },\n { type: 'space.join_requested', reasons: ['moderator'] },\n { type: 'content.flagged', reasons: ['moderator'] },\n { type: 'message.flagged', reasons: ['staff'] },\n ],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'mentions',\n covers: [{ type: 'mention', reasons: ['mentioned'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'comments_on_my_posts',\n covers: [{ type: 'comment.created', reasons: ['post_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'replies_to_my_comments',\n covers: [{ type: 'comment.created', reasons: ['comment_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'followed_posts',\n covers: [{ type: 'comment.created', reasons: ['follower', 'thread_participant'] }],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'messages',\n covers: [\n { type: 'message.sent', reasons: ['participant'] },\n { type: 'chat.request', reasons: ['participant'] },\n { type: 'message.reacted', reasons: ['message_author'] },\n ],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'likes_on_my_posts',\n covers: [{ type: 'post.reacted', reasons: ['post_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'likes_on_my_comments',\n covers: [{ type: 'comment.reacted', reasons: ['comment_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'new_posts',\n perSpace: true,\n covers: [{ type: 'post.published', reasons: ['space_member'] }],\n channels: { inApp: off, email: off, push: off },\n },\n {\n key: 'digest',\n covers: [{ type: 'digest', reasons: ['community_member'] }],\n channels: { email: on },\n },\n];\n\nexport const DEFAULT_ORG_CATEGORIES: NotificationCategory[] = [\n {\n key: 'about_you',\n covers: [\n { type: 'moderator.assigned', reasons: ['subject'] },\n { type: 'comment.removed', reasons: ['subject'] },\n ],\n channels: { inApp: lockedOn, email: on, push: on },\n },\n {\n key: 'review',\n covers: [{ type: 'content.flagged', reasons: ['staff'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'mentions',\n covers: [{ type: 'mention', reasons: ['mentioned'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'replies_to_my_comments',\n covers: [{ type: 'comment.created', reasons: ['comment_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'followed_threads',\n covers: [{ type: 'comment.created', reasons: ['thread_participant'] }],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'likes_on_my_comments',\n covers: [{ type: 'comment.reacted', reasons: ['comment_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'access',\n covers: [\n { type: 'access.granted', reasons: ['subject'] },\n { type: 'access.revoked', reasons: ['subject'] },\n { type: 'community.membership_ended', reasons: ['subject'] },\n ],\n channels: { inApp: on, email: lockedOn },\n },\n {\n key: 'course_content',\n covers: [{ type: 'course.activity_added', reasons: ['course_member'] }],\n channels: { inApp: on, email: off, push: off },\n },\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"defaults.js","names":[],"sources":["../../../src/automations/notifications/defaults.ts"],"sourcesContent":["import type { NotificationCategory } from '../../types/index.js';\n\nconst on = { default: true, locked: false };\nconst off = { default: false, locked: false };\nconst lockedOn = { default: true, locked: true };\n\nexport const DEFAULT_COMMUNITY_CATEGORIES: NotificationCategory[] = [\n {\n key: 'about_you',\n covers: [\n { type: 'post.approved', reasons: ['subject'] },\n { type: 'post.removed', reasons: ['subject'] },\n { type: 'comment.removed', reasons: ['subject'] },\n { type: 'space.invited', reasons: ['subject'] },\n { type: 'space.request_approved', reasons: ['subject'] },\n { type: 'space.request_declined', reasons: ['subject'] },\n { type: 'community.welcome', reasons: ['subject'] },\n { type: 'community.ending_soon', reasons: ['subject'] },\n { type: 'chat.added', reasons: ['subject'] },\n ],\n channels: { inApp: lockedOn, email: on, push: on },\n },\n {\n key: 'review',\n covers: [\n { type: 'post.awaiting_review', reasons: ['moderator'] },\n { type: 'space.join_requested', reasons: ['moderator'] },\n { type: 'content.flagged', reasons: ['moderator'] },\n { type: 'message.flagged', reasons: ['moderator'] },\n ],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'mentions',\n covers: [{ type: 'mention', reasons: ['mentioned'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'comments_on_my_posts',\n covers: [{ type: 'comment.created', reasons: ['post_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'replies_to_my_comments',\n covers: [{ type: 'comment.created', reasons: ['comment_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'followed_posts',\n covers: [{ type: 'comment.created', reasons: ['follower', 'thread_participant'] }],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'messages',\n covers: [\n { type: 'message.sent', reasons: ['participant'] },\n { type: 'chat.request', reasons: ['participant'] },\n { type: 'message.reacted', reasons: ['message_author'] },\n ],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'likes_on_my_posts',\n covers: [{ type: 'post.reacted', reasons: ['post_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'likes_on_my_comments',\n covers: [{ type: 'comment.reacted', reasons: ['comment_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'new_posts',\n perSpace: true,\n covers: [{ type: 'post.published', reasons: ['space_member'] }],\n channels: { inApp: off, email: off, push: off },\n },\n {\n key: 'digest',\n covers: [{ type: 'digest', reasons: ['community_member'] }],\n channels: { email: on },\n },\n];\n\nexport const DEFAULT_ORG_CATEGORIES: NotificationCategory[] = [\n {\n key: 'about_you',\n covers: [\n { type: 'role.assigned', reasons: ['subject'] },\n { type: 'comment.removed', reasons: ['subject'] },\n ],\n channels: { inApp: lockedOn, email: on, push: on },\n },\n {\n key: 'review',\n covers: [{ type: 'content.flagged', reasons: ['moderator'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'mentions',\n covers: [{ type: 'mention', reasons: ['mentioned'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'replies_to_my_comments',\n covers: [{ type: 'comment.created', reasons: ['comment_author'] }],\n channels: { inApp: on, email: on, push: on },\n },\n {\n key: 'followed_threads',\n covers: [{ type: 'comment.created', reasons: ['thread_participant'] }],\n channels: { inApp: on, email: off, push: on },\n },\n {\n key: 'likes_on_my_comments',\n covers: [{ type: 'comment.reacted', reasons: ['comment_author'] }],\n channels: { inApp: on, email: off, push: off },\n },\n {\n key: 'access',\n covers: [\n { type: 'access.granted', reasons: ['subject'] },\n { type: 'access.revoked', reasons: ['subject'] },\n { type: 'community.membership_ended', reasons: ['subject'] },\n ],\n channels: { inApp: on, email: lockedOn },\n },\n {\n key: 'events',\n covers: [\n { type: 'event.granted', reasons: ['event_member'] },\n { type: 'event.starting_soon', reasons: ['event_member'] },\n { type: 'event.changed', reasons: ['event_member'] },\n { type: 'event.canceled', reasons: ['event_member'] },\n { type: 'event.revoked', reasons: ['event_member'] },\n ],\n channels: { inApp: lockedOn, email: on, push: on },\n },\n {\n key: 'course_content',\n covers: [{ type: 'course.activity_added', reasons: ['course_member'] }],\n channels: { inApp: on, email: off, push: off },\n },\n];\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { genId } from "../../shared/id.js";
2
- import { NotFoundError } from "../../shared/errors.js";
3
1
  import { noopLogger } from "../../shared/logger.js";
2
+ import { NotFoundError } from "../../shared/errors.js";
3
+ import { genId } from "../../shared/id.js";
4
4
  import { resolveChannels } from "../../shared/notification-settings.js";
5
5
  //#region src/automations/notifications/processor.ts
6
6
  const IN_APP = "inApp";
@@ -223,7 +223,7 @@ var NotificationProcessor = class {
223
223
  category: category.key,
224
224
  communityId,
225
225
  spaceId: list.spaceId,
226
- actorOrgUserId: list.actorOrgUserId,
226
+ actorId: list.actorId,
227
227
  subject: list.subject,
228
228
  parent: list.parent,
229
229
  display: list.display,
@@ -1 +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 actorOrgUserId: list.actorOrgUserId,\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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"}
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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"}
@@ -1,5 +1,7 @@
1
1
  import { contentEvents } from "../../../content/events.js";
2
+ import { previousStatusOf } from "../../community-mail.js";
2
3
  import { Reasons, actorOr, eventAt, listOf, recipientsOf } from "./recipients.js";
4
+ import { eventBase, eventDisplay } from "./events.js";
3
5
  //#region src/automations/notifications/rules/content.ts
4
6
  const activityPublished = async (event, reads) => {
5
7
  const parsed = contentEvents.activityPublished.safeParse(event);
@@ -11,7 +13,7 @@ const activityPublished = async (event, reads) => {
11
13
  return listOf({
12
14
  type: "course.activity_added",
13
15
  scope: "org",
14
- actorOrgUserId: actorOr(event, null),
16
+ actorId: actorOr(event, null),
15
17
  communityId: null,
16
18
  spaceId: null,
17
19
  subject: {
@@ -27,12 +29,67 @@ const activityPublished = async (event, reads) => {
27
29
  moduleTitle: activity.moduleTitle,
28
30
  activityTitle: activity.title
29
31
  }
30
- }, await recipientsOf(reads, orgId, new Reasons().add(await reads.courseHolders(orgId, courseId), "course_member"), {
32
+ }, await recipientsOf(reads, orgId, new Reasons().add(await reads.courseMembers(orgId, courseId), "course_member"), {
31
33
  exclude: [actorOr(event, null)],
32
34
  at: eventAt(event)
33
35
  }));
34
36
  };
35
- const CONTENT_RULES = { "content.course.activity.published": activityPublished };
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
+ };
36
93
  //#endregion
37
94
  export { CONTENT_RULES };
38
95