@headless-lms/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +1 -1
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +12 -12
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +90 -61
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +465 -88
- package/dist/content/events.js +34 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +103 -67
- package/dist/content/service.js +739 -152
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +266 -152
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +58 -58
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +2 -1
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +16 -12
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +31 -14
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +21 -6
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
|
@@ -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
|
-
|
|
15
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: ["
|
|
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: "
|
|
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: ["
|
|
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: ['
|
|
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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"}
|
|
@@ -223,7 +223,7 @@ var NotificationProcessor = class {
|
|
|
223
223
|
category: category.key,
|
|
224
224
|
communityId,
|
|
225
225
|
spaceId: list.spaceId,
|
|
226
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","names":[],"sources":["../../../../src/automations/notifications/rules/content.ts"],"sourcesContent":["import { contentEvents } from '../../../content/index.js';\nimport type { RecipientRule } from '../../ports.js';\nimport { actorOr, eventAt, listOf, Reasons, recipientsOf } from './recipients.js';\n\nconst activityPublished: RecipientRule = async (event, reads) => {\n const parsed = contentEvents.activityPublished.safeParse(event);\n if (!parsed.success) {\n return [];\n }\n const { activityId, courseId } = parsed.data.data;\n const orgId = event.orgId;\n const activity = await reads.activity(orgId, activityId);\n if (!activity) {\n return [];\n }\n return listOf(\n {\n type: 'course.activity_added',\n scope: 'org',\n
|
|
1
|
+
{"version":3,"file":"content.js","names":[],"sources":["../../../../src/automations/notifications/rules/content.ts"],"sourcesContent":["import { contentEvents, type Event } from '../../../content/index.js';\nimport type { DomainEvent } from '../../../shared/ports.js';\nimport type { NotificationReads, RecipientRule } from '../../ports.js';\nimport { previousStatusOf } from '../../community-mail.js';\nimport { eventBase, eventDisplay } from './events.js';\nimport { actorOr, eventAt, listOf, Reasons, recipientsOf } from './recipients.js';\n\nconst activityPublished: RecipientRule = async (event, reads) => {\n const parsed = contentEvents.activityPublished.safeParse(event);\n if (!parsed.success) {\n return [];\n }\n const { activityId, courseId } = parsed.data.data;\n const orgId = event.orgId;\n const activity = await reads.activity(orgId, activityId);\n if (!activity) {\n return [];\n }\n return listOf(\n {\n type: 'course.activity_added',\n scope: 'org',\n actorId: actorOr(event, null),\n communityId: null,\n spaceId: null,\n subject: { type: 'activity', id: activityId },\n parent: { type: 'course', id: courseId },\n display: {\n courseTitle: activity.courseTitle,\n moduleTitle: activity.moduleTitle,\n activityTitle: activity.title,\n },\n },\n await recipientsOf(\n reads,\n orgId,\n new Reasons().add(await reads.courseMembers(orgId, courseId), 'course_member'),\n { exclude: [actorOr(event, null)], at: eventAt(event) },\n ),\n );\n};\n\nconst LEAD_TEXT = { '1d': '1 day', '1h': '1 hour' } as const;\n\nfunction metadataText(event: DomainEvent, key: string): string | undefined {\n const value = event.metadata?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction whatChanged(\n event: DomainEvent,\n next: Event,\n): 'time' | 'location' | 'time_and_location' | null {\n const time =\n metadataText(event, 'previousStartsAt') !== next.startsAt.toISOString() ||\n metadataText(event, 'previousEndsAt') !== next.endsAt.toISOString();\n const location = event.metadata?.locationChanged === true;\n if (time && location) {\n return 'time_and_location';\n }\n return time ? 'time' : location ? 'location' : null;\n}\n\nasync function toAttendees(\n reads: NotificationReads,\n domainEvent: DomainEvent,\n event: Event,\n type: string,\n actorId: string | null,\n display: Record<string, string>,\n) {\n return listOf(\n { ...eventBase(type, event, actorId), display },\n await recipientsOf(\n reads,\n domainEvent.orgId,\n new Reasons().add(await reads.eventAttendees(domainEvent.orgId, event.id), 'event_member'),\n { exclude: [actorId], at: eventAt(domainEvent) },\n ),\n );\n}\n\nconst eventUpdated: RecipientRule = async (domainEvent, reads) => {\n const parsed = contentEvents.eventUpdated.safeParse(domainEvent);\n if (!parsed.success) {\n return [];\n }\n const next = parsed.data.data;\n const event = await reads.event(domainEvent.orgId, next.id);\n if (!event?.settings.notifications) {\n return [];\n }\n const actor = actorOr(domainEvent, null);\n const previousStatus = previousStatusOf(domainEvent);\n if (next.status === 'canceled' && previousStatus !== 'canceled') {\n return toAttendees(reads, domainEvent, event, 'event.canceled', actor, eventDisplay(next));\n }\n if (\n previousStatus !== 'published' ||\n next.status !== 'published' ||\n next.endsAt.getTime() <= eventAt(domainEvent).getTime()\n ) {\n return [];\n }\n const changed = whatChanged(domainEvent, next);\n if (!changed) {\n return [];\n }\n return toAttendees(reads, domainEvent, event, 'event.changed', actor, {\n ...eventDisplay(next),\n changed,\n });\n};\n\nconst eventStartingSoon: RecipientRule = async (domainEvent, reads) => {\n const parsed = contentEvents.eventStartingSoon.safeParse(domainEvent);\n if (!parsed.success) {\n return [];\n }\n const { eventId, lead } = parsed.data.data;\n const event = await reads.event(domainEvent.orgId, eventId);\n if (!event?.settings.notifications || event.status !== 'published') {\n return [];\n }\n return toAttendees(reads, domainEvent, event, 'event.starting_soon', null, {\n ...eventDisplay(event),\n leadText: LEAD_TEXT[lead],\n });\n};\n\nexport const CONTENT_RULES: Record<string, RecipientRule> = {\n 'content.course.activity.published': activityPublished,\n 'content.event.updated': eventUpdated,\n 'content.event.starting_soon': eventStartingSoon,\n};\n"],"mappings":"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"}
|