@things-factory/announcement 10.1.258 → 10.1.260
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/client/index.ts +1 -0
- package/client/pages/announcement-list.ts +28 -43
- package/client/utils/announcement-list-query.ts +8 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/announcement-list.d.ts +0 -4
- package/dist-client/pages/announcement-list.js +28 -48
- package/dist-client/pages/announcement-list.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/announcement-list-query.d.ts +5 -0
- package/dist-client/utils/announcement-list-query.js +8 -0
- package/dist-client/utils/announcement-list-query.js.map +1 -0
- package/dist-server/service/announcement/announcement-rules.d.ts +0 -29
- package/dist-server/service/announcement/announcement-rules.js +1 -19
- package/dist-server/service/announcement/announcement-rules.js.map +1 -1
- package/dist-server/service/announcement/announcement-service.js +2 -14
- package/dist-server/service/announcement/announcement-service.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/server/service/announcement/announcement-rules.ts +0 -20
- package/server/service/announcement/announcement-service.ts +4 -15
- package/test/announcement-persistence.test.ts +3 -1
- package/test/announcement-rules.test.ts +0 -15
- package/test/list-query-state.test.ts +25 -0
- package/translations/en.json +3 -3
- package/translations/es.json +3 -3
- package/translations/ja.json +3 -3
- package/translations/ko.json +3 -3
- package/translations/ms.json +3 -3
- package/translations/zh.json +3 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The announcement list's query as its grid keeps it (FOOTER-BUTTON-REVIEW G09). Pure - no imports, so a test holds it.
|
|
3
|
+
*/
|
|
4
|
+
/** The period the grid's `period` filter names - past, else current (a cleared choice is the current ones). Pure. */
|
|
5
|
+
export function announcementPeriodOf(filters) {
|
|
6
|
+
return filters?.find(one => one.name === 'period')?.value === 'past' ? 'past' : 'current';
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=announcement-list-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announcement-list-query.js","sourceRoot":"","sources":["../../client/utils/announcement-list-query.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qHAAqH;AACrH,MAAM,UAAU,oBAAoB,CAAC,OAA6D;IAChG,OAAO,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3F,CAAC","sourcesContent":["/*\n * The announcement list's query as its grid keeps it (FOOTER-BUTTON-REVIEW G09). Pure - no imports, so a test holds it.\n */\n\n/** The period the grid's `period` filter names - past, else current (a cleared choice is the current ones). Pure. */\nexport function announcementPeriodOf(filters: readonly { name: string; value?: any }[] | undefined): 'current' | 'past' {\n return filters?.find(one => one.name === 'period')?.value === 'past' ? 'past' : 'current'\n}\n"]}
|
|
@@ -118,33 +118,4 @@ export declare function noticeWordsOf(one: {
|
|
|
118
118
|
action: string;
|
|
119
119
|
note: string;
|
|
120
120
|
};
|
|
121
|
-
export type NoticeCount = {
|
|
122
|
-
receivers: number;
|
|
123
|
-
email: {
|
|
124
|
-
sent: number;
|
|
125
|
-
failed: number;
|
|
126
|
-
};
|
|
127
|
-
teams: {
|
|
128
|
-
sent: number;
|
|
129
|
-
failed: number;
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
/** One telling as the announcement keeps it - when, why, and what went out. A send that could not go at all counts its receivers, nothing sent. */
|
|
133
|
-
export declare function deliveryOf(kind: NoticeKind, result: NoticeCount | undefined, receivers: number, at?: Date): {
|
|
134
|
-
failed?: boolean;
|
|
135
|
-
at: string;
|
|
136
|
-
kind: NoticeKind;
|
|
137
|
-
receivers: number;
|
|
138
|
-
email: {
|
|
139
|
-
sent: number;
|
|
140
|
-
failed: number;
|
|
141
|
-
};
|
|
142
|
-
teams: {
|
|
143
|
-
sent: number;
|
|
144
|
-
failed: number;
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
|
-
/** The record kept on an announcement - the newest tellings, at most `MAX_DELIVERIES`. */
|
|
148
|
-
export declare const MAX_DELIVERIES = 50;
|
|
149
|
-
export declare function withDelivery<T>(deliveries: T[] | null | undefined, one: T): T[];
|
|
150
121
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.viewAddressOf = exports.READER_STATES = exports.PERIODS = exports.MAX_LINK_LABEL = exports.MAX_LINKS = exports.MAX_TITLE = exports.IMPORTANCES = exports.ANNOUNCEMENT_CATEGORIES = void 0;
|
|
4
4
|
exports.isCurrent = isCurrent;
|
|
5
5
|
exports.periodOf = periodOf;
|
|
6
6
|
exports.readerNarrowingOf = readerNarrowingOf;
|
|
@@ -15,8 +15,6 @@ exports.owedReminder = owedReminder;
|
|
|
15
15
|
exports.plainSummary = plainSummary;
|
|
16
16
|
exports.broadcastOf = broadcastOf;
|
|
17
17
|
exports.noticeWordsOf = noticeWordsOf;
|
|
18
|
-
exports.deliveryOf = deliveryOf;
|
|
19
|
-
exports.withDelivery = withDelivery;
|
|
20
18
|
/*
|
|
21
19
|
* The rules of an announcement (ADR-0163 decisions 3-5) - what may be written, where it stands, what a reader owes it,
|
|
22
20
|
* when it pops up, and what the notice of it says.
|
|
@@ -208,20 +206,4 @@ function noticeWordsOf(one, kind, say) {
|
|
|
208
206
|
note: say('announcement.notice.note', 'Open Announcements in the application to read it.')
|
|
209
207
|
};
|
|
210
208
|
}
|
|
211
|
-
/** One telling as the announcement keeps it - when, why, and what went out. A send that could not go at all counts its receivers, nothing sent. */
|
|
212
|
-
function deliveryOf(kind, result, receivers, at = new Date()) {
|
|
213
|
-
return {
|
|
214
|
-
at: at.toISOString(),
|
|
215
|
-
kind,
|
|
216
|
-
receivers: result?.receivers ?? receivers,
|
|
217
|
-
email: result?.email ?? { sent: 0, failed: 0 },
|
|
218
|
-
teams: result?.teams ?? { sent: 0, failed: 0 },
|
|
219
|
-
...(result ? {} : { failed: true })
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
/** The record kept on an announcement - the newest tellings, at most `MAX_DELIVERIES`. */
|
|
223
|
-
exports.MAX_DELIVERIES = 50;
|
|
224
|
-
function withDelivery(deliveries, one) {
|
|
225
|
-
return [...(deliveries ?? []), one].slice(-exports.MAX_DELIVERIES);
|
|
226
|
-
}
|
|
227
209
|
//# sourceMappingURL=announcement-rules.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement-rules.js","sourceRoot":"","sources":["../../../server/service/announcement/announcement-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The rules of an announcement (ADR-0163 decisions 3-5) - what may be written, where it stands, what a reader owes it,\n * when it pops up, and what the notice of it says.\n *\n * Pure - the rules are tested without a database. The doors (announcement-resolver.ts) ask them.\n */\n/** The kinds of announcement - declared by this module, named in its translations (`announcement.category.<code>`). */\nexport const ANNOUNCEMENT_CATEGORIES = ['GENERAL', 'HR', 'WELFARE', 'EVENT', 'SYSTEM', 'SECURITY'] as const\n\nexport const IMPORTANCES = ['NORMAL', 'IMPORTANT', 'URGENT'] as const\nexport const MAX_TITLE = 200\nexport const MAX_LINKS = 10\nexport const MAX_LINK_LABEL = 100\n\nexport interface AnnouncementFacts {\n status: string\n importance?: string\n pinned?: boolean\n popup?: boolean\n requiresAcknowledgement?: boolean\n startsAt?: Date | string | null\n endsAt?: Date | string | null\n}\n\nexport interface ReceiptFacts {\n readAt?: Date | string | null\n acknowledgedAt?: Date | string | null\n}\n\nconst time = (value: Date | string | null | undefined) => (value ? new Date(value).getTime() : undefined)\n\n/** Out to its audience and not past its end. */\nexport function isCurrent(one: AnnouncementFacts, now = new Date()): boolean {\n const end = time(one.endsAt)\n return one.status === 'PUBLISHED' && (end === undefined || end > now.getTime())\n}\n\n/** Whether an announcement that is out is still current or past - a reader's list and a dashboard card ask it. */\nexport function periodOf(one: AnnouncementFacts, now = new Date()): 'current' | 'past' | undefined {\n if (one.status !== 'PUBLISHED') return undefined\n return isCurrent(one, now) ? 'current' : 'past'\n}\n\nexport const PERIODS = ['current', 'past'] as const\nexport const READER_STATES = ['unread', 'read', 'to-acknowledge', 'acknowledged'] as const\n\ntype ListFilter = { name: string; operator?: string; value?: unknown }\n\n/**\n * The reader's own narrowing in a list's filters - `period` (current | past) and `readerState` (unread, read,\n * to-acknowledge, acknowledged; one or several) - taken out of the column filters, so the screen and the assistant\n * narrow the one list the same way. Absent, a list is not narrowed by them. An unknown value is refused, not ignored.\n */\nexport function readerNarrowingOf(filters: ListFilter[] | null | undefined): { rest: ListFilter[]; period?: 'current' | 'past'; states?: string[]; refusal?: string } {\n const rest: ListFilter[] = []\n let period: 'current' | 'past' | undefined\n let states: string[] | undefined\n for (const filter of filters ?? []) {\n if (filter?.name === 'period') {\n if (!(PERIODS as readonly string[]).includes(String(filter.value))) return { rest, refusal: 'unknown-period' }\n period = filter.value as 'current' | 'past'\n } else if (filter?.name === 'readerState') {\n const values = [filter.value].flat().map(String)\n if (!values.length || values.some(one => !(READER_STATES as readonly string[]).includes(one))) return { rest, refusal: 'unknown-reader-state' }\n states = values\n } else rest.push(filter)\n }\n return { rest, period, states }\n}\n\n/** Publishing now goes out at once, or waits for a start still to come. */\nexport function publishedStatusOf(startsAt: Date | string | null | undefined, now = new Date()): 'PUBLISHED' | 'SCHEDULED' {\n const start = time(startsAt)\n return start !== undefined && start > now.getTime() ? 'SCHEDULED' : 'PUBLISHED'\n}\n\n/**\n * Where it stands for one reader. Reading and acknowledging are apart: one that asks to be acknowledged stays\n * 'to-acknowledge' after it was read.\n */\nexport function readerStateOf(one: AnnouncementFacts, receipt: ReceiptFacts | undefined): 'unread' | 'to-acknowledge' | 'acknowledged' | 'read' {\n if (one.requiresAcknowledgement) {\n if (receipt?.acknowledgedAt) return 'acknowledged'\n return receipt?.readAt ? 'to-acknowledge' : 'unread'\n }\n return receipt?.readAt ? 'read' : 'unread'\n}\n\n/**\n * Whether it pops up for the reader (the user, 2026-10-26: 「공지 사항이 있을 때 자동 팝업」): a current popup\n * announcement shows once - seeing it reads it - and one that asks to be acknowledged shows at every sign-in until it\n * is acknowledged.\n */\nexport function popupDue(one: AnnouncementFacts, receipt: ReceiptFacts | undefined, now = new Date()): boolean {\n if (!one.popup || !isCurrent(one, now)) return false\n return one.requiresAcknowledgement ? !receipt?.acknowledgedAt : !receipt?.readAt\n}\n\n/**\n * The place in the readers' list - kept as a column so the list is ordered by the database: pinned first, then urgent,\n * important, normal. The newest comes first within each (the list's second order).\n */\nexport function rankOf(one: Pick<AnnouncementFacts, 'pinned' | 'importance'>): number {\n const importance = one.importance === 'URGENT' ? 0 : one.importance === 'IMPORTANT' ? 1 : 2\n return (one.pinned ? 0 : 10) + importance\n}\n\n/**\n * A link a reader may follow: a screen of the application by its address (`contract-list?id=…`) or an https page.\n * Nothing that runs (`javascript:`), no other scheme, nothing that leaves through `//`.\n */\nexport function linkRefusal(link: { label?: string; url?: string } | null | undefined): string | undefined {\n const label = String(link?.label ?? '').trim()\n const url = String(link?.url ?? '').trim()\n if (!label || !url) return 'link-incomplete'\n if (label.length > MAX_LINK_LABEL) return 'link-label-too-long'\n if (/\\s/.test(url) || url.includes('\\\\')) return 'bad-link'\n if (/^https:\\/\\/[^/]/i.test(url)) return undefined\n if (url.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(url)) return 'bad-link'\n return undefined\n}\n\nexport interface AnnouncementInputFacts {\n title?: string\n category?: string\n importance?: string\n startsAt?: Date | string | null\n endsAt?: Date | string | null\n requiresAcknowledgement?: boolean\n acknowledgeBy?: Date | string | null\n links?: { label?: string; url?: string }[] | null\n}\n\n/** Why what was written cannot be kept, or nothing. Asked on every save - a draft is kept to the same rules. */\nexport function inputRefusal(input: AnnouncementInputFacts): string | undefined {\n const title = String(input.title ?? '').trim()\n if (!title) return 'no-title'\n if (title.length > MAX_TITLE) return 'title-too-long'\n if (input.category && !(ANNOUNCEMENT_CATEGORIES as readonly string[]).includes(input.category)) return 'unknown-category'\n if (input.importance && !(IMPORTANCES as readonly string[]).includes(input.importance)) return 'unknown-importance'\n const start = time(input.startsAt)\n const end = time(input.endsAt)\n if (start !== undefined && end !== undefined && end <= start) return 'ends-before-start'\n if (input.acknowledgeBy && !input.requiresAcknowledgement) return 'acknowledge-by-without-ask'\n const by = time(input.acknowledgeBy)\n if (by !== undefined && start !== undefined && by < start) return 'acknowledge-by-before-start'\n if ((input.links ?? []).length > MAX_LINKS) return 'too-many-links'\n for (const link of input.links ?? []) {\n const refusal = linkRefusal(link)\n if (refusal) return refusal\n }\n return undefined\n}\n\n/** Why it cannot go out, or nothing - it is written to the rules and names someone. */\nexport function publishRefusal(status: string, input: AnnouncementInputFacts, audienceCount: number): string | undefined {\n if (status !== 'DRAFT') return 'not-a-draft'\n return inputRefusal(input) ?? (audienceCount > 0 ? undefined : 'no-audience')\n}\n\n/** Who on the roster is still owed a reminder: not read yet, or not acknowledged when it asks for that. */\nexport function owedReminder(one: Pick<AnnouncementFacts, 'requiresAcknowledgement'>, receipt: ReceiptFacts & { listed?: boolean }): boolean {\n if (!receipt.listed) return false\n return one.requiresAcknowledgement ? !receipt.acknowledgedAt : !receipt.readAt\n}\n\n/** The body as one line of plain words - the notice's text, and the list's preview. */\nexport function plainSummary(body: string | null | undefined, max = 120): string {\n const text = String(body ?? '')\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/!\\[[^\\]]*\\]\\([^)]*\\)/g, ' ')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/^\\s{0,3}(#{1,6}|>|[-*+]|\\d+\\.)\\s+/gm, '')\n /* Only the marks that wrap words - a lone ~ (22시~24시), _ or | is part of the text. */\n .replace(/(\\*\\*|__|~~|`)/g, '')\n .replace(/(^|\\s)\\*(\\S[^*]*?)\\*(?=\\s|$|[.,!?])/g, '$1$2')\n .replace(/\\s+/g, ' ')\n .trim()\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\n/** The screen an announcement opens on. */\nexport const viewAddressOf = (id: string) => `announcement-view?id=${id}`\n\n/**\n * Whether its notice is said once to the whole domain - a Teams channel everyone reads (notification `broadcast`).\n * Only an announcement addressed to the whole domain: one for a department or a role must not land where everyone\n * reads it.\n */\nexport function broadcastOf(audiences: readonly { scope: string; targetId: string }[], domainId: string | undefined): boolean {\n return !!domainId && audiences.some(one => one.scope === 'DOMAIN' && one.targetId === domainId)\n}\n\n/** Why the people are told: it went out, it changed and they are told again, or they still owe it. */\nexport type NoticeKind = 'published' | 'changed' | 'reminder'\n\n/** A sentence in one language, with the English one when that language has none. */\nexport type Say = (key: string, english: string, params?: Record<string, unknown>) => string\n\n/**\n * What the notice of an announcement says to one person (notification's `NoticeWords`): its title - marked when it is\n * a change or a reminder - the start of its body for the inbox and web push, and the whole body, Markdown, for the mail\n * (ADR-0163 decision 6: the mail carries the announcement, files stay behind the sign-in).\n */\nexport function noticeWordsOf(one: { title: string; body?: string | null }, kind: NoticeKind, say: Say) {\n const subject =\n kind === 'changed'\n ? say('announcement.notice.changed-subject', 'Changed: {title}', { title: one.title })\n : kind === 'reminder'\n ? say('announcement.notice.reminder-subject', 'Reminder: {title}', { title: one.title })\n : one.title\n return {\n subject,\n summary: plainSummary(one.body),\n body: one.body ?? '',\n action: say('announcement.notice.action', 'Open the announcement'),\n note: say('announcement.notice.note', 'Open Announcements in the application to read it.')\n }\n}\n\nexport type NoticeCount = { receivers: number; email: { sent: number; failed: number }; teams: { sent: number; failed: number } }\n\n/** One telling as the announcement keeps it - when, why, and what went out. A send that could not go at all counts its receivers, nothing sent. */\nexport function deliveryOf(kind: NoticeKind, result: NoticeCount | undefined, receivers: number, at = new Date()) {\n return {\n at: at.toISOString(),\n kind,\n receivers: result?.receivers ?? receivers,\n email: result?.email ?? { sent: 0, failed: 0 },\n teams: result?.teams ?? { sent: 0, failed: 0 },\n ...(result ? {} : { failed: true })\n }\n}\n\n/** The record kept on an announcement - the newest tellings, at most `MAX_DELIVERIES`. */\nexport const MAX_DELIVERIES = 50\nexport function withDelivery<T>(deliveries: T[] | null | undefined, one: T): T[] {\n return [...(deliveries ?? []), one].slice(-MAX_DELIVERIES)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"announcement-rules.js","sourceRoot":"","sources":["../../../server/service/announcement/announcement-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The rules of an announcement (ADR-0163 decisions 3-5) - what may be written, where it stands, what a reader owes it,\n * when it pops up, and what the notice of it says.\n *\n * Pure - the rules are tested without a database. The doors (announcement-resolver.ts) ask them.\n */\n/** The kinds of announcement - declared by this module, named in its translations (`announcement.category.<code>`). */\nexport const ANNOUNCEMENT_CATEGORIES = ['GENERAL', 'HR', 'WELFARE', 'EVENT', 'SYSTEM', 'SECURITY'] as const\n\nexport const IMPORTANCES = ['NORMAL', 'IMPORTANT', 'URGENT'] as const\nexport const MAX_TITLE = 200\nexport const MAX_LINKS = 10\nexport const MAX_LINK_LABEL = 100\n\nexport interface AnnouncementFacts {\n status: string\n importance?: string\n pinned?: boolean\n popup?: boolean\n requiresAcknowledgement?: boolean\n startsAt?: Date | string | null\n endsAt?: Date | string | null\n}\n\nexport interface ReceiptFacts {\n readAt?: Date | string | null\n acknowledgedAt?: Date | string | null\n}\n\nconst time = (value: Date | string | null | undefined) => (value ? new Date(value).getTime() : undefined)\n\n/** Out to its audience and not past its end. */\nexport function isCurrent(one: AnnouncementFacts, now = new Date()): boolean {\n const end = time(one.endsAt)\n return one.status === 'PUBLISHED' && (end === undefined || end > now.getTime())\n}\n\n/** Whether an announcement that is out is still current or past - a reader's list and a dashboard card ask it. */\nexport function periodOf(one: AnnouncementFacts, now = new Date()): 'current' | 'past' | undefined {\n if (one.status !== 'PUBLISHED') return undefined\n return isCurrent(one, now) ? 'current' : 'past'\n}\n\nexport const PERIODS = ['current', 'past'] as const\nexport const READER_STATES = ['unread', 'read', 'to-acknowledge', 'acknowledged'] as const\n\ntype ListFilter = { name: string; operator?: string; value?: unknown }\n\n/**\n * The reader's own narrowing in a list's filters - `period` (current | past) and `readerState` (unread, read,\n * to-acknowledge, acknowledged; one or several) - taken out of the column filters, so the screen and the assistant\n * narrow the one list the same way. Absent, a list is not narrowed by them. An unknown value is refused, not ignored.\n */\nexport function readerNarrowingOf(filters: ListFilter[] | null | undefined): { rest: ListFilter[]; period?: 'current' | 'past'; states?: string[]; refusal?: string } {\n const rest: ListFilter[] = []\n let period: 'current' | 'past' | undefined\n let states: string[] | undefined\n for (const filter of filters ?? []) {\n if (filter?.name === 'period') {\n if (!(PERIODS as readonly string[]).includes(String(filter.value))) return { rest, refusal: 'unknown-period' }\n period = filter.value as 'current' | 'past'\n } else if (filter?.name === 'readerState') {\n const values = [filter.value].flat().map(String)\n if (!values.length || values.some(one => !(READER_STATES as readonly string[]).includes(one))) return { rest, refusal: 'unknown-reader-state' }\n states = values\n } else rest.push(filter)\n }\n return { rest, period, states }\n}\n\n/** Publishing now goes out at once, or waits for a start still to come. */\nexport function publishedStatusOf(startsAt: Date | string | null | undefined, now = new Date()): 'PUBLISHED' | 'SCHEDULED' {\n const start = time(startsAt)\n return start !== undefined && start > now.getTime() ? 'SCHEDULED' : 'PUBLISHED'\n}\n\n/**\n * Where it stands for one reader. Reading and acknowledging are apart: one that asks to be acknowledged stays\n * 'to-acknowledge' after it was read.\n */\nexport function readerStateOf(one: AnnouncementFacts, receipt: ReceiptFacts | undefined): 'unread' | 'to-acknowledge' | 'acknowledged' | 'read' {\n if (one.requiresAcknowledgement) {\n if (receipt?.acknowledgedAt) return 'acknowledged'\n return receipt?.readAt ? 'to-acknowledge' : 'unread'\n }\n return receipt?.readAt ? 'read' : 'unread'\n}\n\n/**\n * Whether it pops up for the reader (the user, 2026-10-26: 「공지 사항이 있을 때 자동 팝업」): a current popup\n * announcement shows once - seeing it reads it - and one that asks to be acknowledged shows at every sign-in until it\n * is acknowledged.\n */\nexport function popupDue(one: AnnouncementFacts, receipt: ReceiptFacts | undefined, now = new Date()): boolean {\n if (!one.popup || !isCurrent(one, now)) return false\n return one.requiresAcknowledgement ? !receipt?.acknowledgedAt : !receipt?.readAt\n}\n\n/**\n * The place in the readers' list - kept as a column so the list is ordered by the database: pinned first, then urgent,\n * important, normal. The newest comes first within each (the list's second order).\n */\nexport function rankOf(one: Pick<AnnouncementFacts, 'pinned' | 'importance'>): number {\n const importance = one.importance === 'URGENT' ? 0 : one.importance === 'IMPORTANT' ? 1 : 2\n return (one.pinned ? 0 : 10) + importance\n}\n\n/**\n * A link a reader may follow: a screen of the application by its address (`contract-list?id=…`) or an https page.\n * Nothing that runs (`javascript:`), no other scheme, nothing that leaves through `//`.\n */\nexport function linkRefusal(link: { label?: string; url?: string } | null | undefined): string | undefined {\n const label = String(link?.label ?? '').trim()\n const url = String(link?.url ?? '').trim()\n if (!label || !url) return 'link-incomplete'\n if (label.length > MAX_LINK_LABEL) return 'link-label-too-long'\n if (/\\s/.test(url) || url.includes('\\\\')) return 'bad-link'\n if (/^https:\\/\\/[^/]/i.test(url)) return undefined\n if (url.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(url)) return 'bad-link'\n return undefined\n}\n\nexport interface AnnouncementInputFacts {\n title?: string\n category?: string\n importance?: string\n startsAt?: Date | string | null\n endsAt?: Date | string | null\n requiresAcknowledgement?: boolean\n acknowledgeBy?: Date | string | null\n links?: { label?: string; url?: string }[] | null\n}\n\n/** Why what was written cannot be kept, or nothing. Asked on every save - a draft is kept to the same rules. */\nexport function inputRefusal(input: AnnouncementInputFacts): string | undefined {\n const title = String(input.title ?? '').trim()\n if (!title) return 'no-title'\n if (title.length > MAX_TITLE) return 'title-too-long'\n if (input.category && !(ANNOUNCEMENT_CATEGORIES as readonly string[]).includes(input.category)) return 'unknown-category'\n if (input.importance && !(IMPORTANCES as readonly string[]).includes(input.importance)) return 'unknown-importance'\n const start = time(input.startsAt)\n const end = time(input.endsAt)\n if (start !== undefined && end !== undefined && end <= start) return 'ends-before-start'\n if (input.acknowledgeBy && !input.requiresAcknowledgement) return 'acknowledge-by-without-ask'\n const by = time(input.acknowledgeBy)\n if (by !== undefined && start !== undefined && by < start) return 'acknowledge-by-before-start'\n if ((input.links ?? []).length > MAX_LINKS) return 'too-many-links'\n for (const link of input.links ?? []) {\n const refusal = linkRefusal(link)\n if (refusal) return refusal\n }\n return undefined\n}\n\n/** Why it cannot go out, or nothing - it is written to the rules and names someone. */\nexport function publishRefusal(status: string, input: AnnouncementInputFacts, audienceCount: number): string | undefined {\n if (status !== 'DRAFT') return 'not-a-draft'\n return inputRefusal(input) ?? (audienceCount > 0 ? undefined : 'no-audience')\n}\n\n/** Who on the roster is still owed a reminder: not read yet, or not acknowledged when it asks for that. */\nexport function owedReminder(one: Pick<AnnouncementFacts, 'requiresAcknowledgement'>, receipt: ReceiptFacts & { listed?: boolean }): boolean {\n if (!receipt.listed) return false\n return one.requiresAcknowledgement ? !receipt.acknowledgedAt : !receipt.readAt\n}\n\n/** The body as one line of plain words - the notice's text, and the list's preview. */\nexport function plainSummary(body: string | null | undefined, max = 120): string {\n const text = String(body ?? '')\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/!\\[[^\\]]*\\]\\([^)]*\\)/g, ' ')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/^\\s{0,3}(#{1,6}|>|[-*+]|\\d+\\.)\\s+/gm, '')\n /* Only the marks that wrap words - a lone ~ (22시~24시), _ or | is part of the text. */\n .replace(/(\\*\\*|__|~~|`)/g, '')\n .replace(/(^|\\s)\\*(\\S[^*]*?)\\*(?=\\s|$|[.,!?])/g, '$1$2')\n .replace(/\\s+/g, ' ')\n .trim()\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\n/** The screen an announcement opens on. */\nexport const viewAddressOf = (id: string) => `announcement-view?id=${id}`\n\n/**\n * Whether its notice is said once to the whole domain - a Teams channel everyone reads (notification `broadcast`).\n * Only an announcement addressed to the whole domain: one for a department or a role must not land where everyone\n * reads it.\n */\nexport function broadcastOf(audiences: readonly { scope: string; targetId: string }[], domainId: string | undefined): boolean {\n return !!domainId && audiences.some(one => one.scope === 'DOMAIN' && one.targetId === domainId)\n}\n\n/** Why the people are told: it went out, it changed and they are told again, or they still owe it. */\nexport type NoticeKind = 'published' | 'changed' | 'reminder'\n\n/** A sentence in one language, with the English one when that language has none. */\nexport type Say = (key: string, english: string, params?: Record<string, unknown>) => string\n\n/**\n * What the notice of an announcement says to one person (notification's `NoticeWords`): its title - marked when it is\n * a change or a reminder - the start of its body for the inbox and web push, and the whole body, Markdown, for the mail\n * (ADR-0163 decision 6: the mail carries the announcement, files stay behind the sign-in).\n */\nexport function noticeWordsOf(one: { title: string; body?: string | null }, kind: NoticeKind, say: Say) {\n const subject =\n kind === 'changed'\n ? say('announcement.notice.changed-subject', 'Changed: {title}', { title: one.title })\n : kind === 'reminder'\n ? say('announcement.notice.reminder-subject', 'Reminder: {title}', { title: one.title })\n : one.title\n return {\n subject,\n summary: plainSummary(one.body),\n body: one.body ?? '',\n action: say('announcement.notice.action', 'Open the announcement'),\n note: say('announcement.notice.note', 'Open Announcements in the application to read it.')\n }\n}\n"]}
|
|
@@ -178,18 +178,6 @@ function noticeOf(one, kind, receivers, broadcast, link) {
|
|
|
178
178
|
...(broadcast ? { broadcast: true } : {})
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
-
/** Keeps what one telling sent on the announcement - in its own transaction, the request's being over. */
|
|
182
|
-
async function recordDelivery(id, kind, result, receivers) {
|
|
183
|
-
try {
|
|
184
|
-
const repository = (0, shell_1.getRepository)(announcement_1.Announcement);
|
|
185
|
-
const one = await repository.findOneBy({ id });
|
|
186
|
-
if (one)
|
|
187
|
-
await repository.update(id, { deliveries: (0, announcement_rules_1.withDelivery)(one.deliveries, (0, announcement_rules_1.deliveryOf)(kind, result, receivers)) });
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
env_1.logger.error(`announcement ${id}: what was sent could not be kept`, error);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
181
|
/**
|
|
194
182
|
* Tells `userIds` of it once the request's transaction has committed - a reader told before it would not find it yet -
|
|
195
183
|
* and keeps what went out. A notice that does not arrive does not undo the announcement: its list, badge and popup
|
|
@@ -200,7 +188,7 @@ async function tellWhenCommitted(context, one, kind, userIds, toEveryone = false
|
|
|
200
188
|
if (!receivers.length)
|
|
201
189
|
return;
|
|
202
190
|
const broadcast = toEveryone && (0, announcement_rules_1.broadcastOf)(await audiencesOf(one.id, context.state.tx), context.state.domain?.id);
|
|
203
|
-
(0, notification_1.sendNoticeWhenCommitted)(context, noticeOf(one, kind, receivers, broadcast, (0, notification_1.noticeLinkOf)(context, (0, announcement_rules_1.viewAddressOf)(one.id))), result =>
|
|
191
|
+
(0, notification_1.sendNoticeWhenCommitted)(context, noticeOf(one, kind, receivers, broadcast, (0, notification_1.noticeLinkOf)(context, (0, announcement_rules_1.viewAddressOf)(one.id))), result => (0, notification_1.keepNoticeDelivery)(announcement_1.Announcement, one.id, 'deliveries', (0, notification_1.noticeDeliveryOf)(kind, result, receivers.length)));
|
|
204
192
|
}
|
|
205
193
|
/**
|
|
206
194
|
* Publishes the scheduled announcements whose start has come. Each is taken by one update from SCHEDULED, so a server
|
|
@@ -229,7 +217,7 @@ async function publishDue(now = new Date()) {
|
|
|
229
217
|
const people = await receiversOf(receivers);
|
|
230
218
|
const broadcast = (0, announcement_rules_1.broadcastOf)(await audiencesOf(one.id), one.domain?.id);
|
|
231
219
|
if (people.length)
|
|
232
|
-
await
|
|
220
|
+
await (0, notification_1.keepNoticeDelivery)(announcement_1.Announcement, one.id, 'deliveries', (0, notification_1.noticeDeliveryOf)('published', await (0, notification_1.sendNotice)(one.domain, noticeOf(one, 'published', people, broadcast)), people.length));
|
|
233
221
|
}
|
|
234
222
|
catch (error) {
|
|
235
223
|
env_1.logger.error(`announcement ${one.id}: publishing at its start failed`, error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement-service.js","sourceRoot":"","sources":["../../../server/service/announcement/announcement-service.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the doors of announcements do with the database (ADR-0163 decisions 1-5): who may read one, who is on its roster,\n * sending it out, telling people of it, and publishing the scheduled ones when their start comes.\n */\nimport { EntityManager, In, IsNull, LessThanOrEqual } from 'typeorm'\n\nimport { checkPermission, User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { noticeLinkOf, sendNotice, sendNoticeWhenCommitted, type Notice, type NoticeResult } from '@things-factory/notification'\nimport { audienceMemberOf, audienceUserIds, type AudienceMember } from '@things-factory/organization'\nimport { Domain, getDataSource, getRepository } from '@things-factory/shell'\n\nimport { Announcement, AnnouncementAudience, AnnouncementReceipt, AnnouncementRevision, AnnouncementStatus } from './announcement'\nimport { broadcastOf, deliveryOf, noticeWordsOf, viewAddressOf, withDelivery, type NoticeKind, type Say } from './announcement-rules'\n\n/** The attachments of an announcement hang on it under this `refType` (attachment-base). */\nexport const ANNOUNCEMENT_REF_TYPE = 'Announcement'\n\n/** Whether the request writes announcements - the one privilege of the module (`announcement.writing`). */\nexport async function isWriter(context: any): Promise<boolean> {\n const state = context?.state ?? {}\n const { domain, user, unsafeIP, prohibitedPrivileges } = state\n if (!domain || !user) return false\n /* Asked once per request - a list asks it for each row. */\n state.__announcementWriter ??= checkPermission({ category: 'announcement', privilege: 'mutation', owner: true } as any, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean)\n return await state.__announcementWriter\n}\n\n/** The request's person as an audience sees them - asked once per request. */\nexport async function memberOf(context: any): Promise<AudienceMember> {\n const state = context.state\n if (!state.__announcementMember) {\n const { ownDepartments, ...member } = await audienceMemberOf(state.domain, state.user ?? {})\n state.__announcementMember = member\n }\n return state.__announcementMember\n}\n\n/** The announcements of the domain whose audience names `member` - by their targets, whatever their status. */\nexport async function reachingIds(member: AudienceMember, tx?: EntityManager): Promise<string[]> {\n if (!member.userId || !member.domainId) return []\n const domain = { id: member.domainId }\n const rows = await getRepository(AnnouncementAudience, tx).find({\n where: [\n { domain, scope: 'DOMAIN', targetId: member.domainId },\n { domain, scope: 'USER', targetId: member.userId },\n ...(member.departmentIds.length ? [{ domain, scope: 'DEPARTMENT', targetId: In([...member.departmentIds]) }] : []),\n ...(member.roleIds.length ? [{ domain, scope: 'ROLE', targetId: In([...member.roleIds]) }] : [])\n ] as any\n })\n return [...new Set(rows.map(one => one.announcementId!))]\n}\n\nexport async function audiencesOf(announcementId: string, tx?: EntityManager): Promise<AnnouncementAudience[]> {\n return await getRepository(AnnouncementAudience, tx).find({ where: { announcement: { id: announcementId } } as any, order: { createdAt: 'ASC' } as any })\n}\n\n/**\n * The announcement as the request may see it, or nothing - one answer for \"none\", \"not yours\" and \"not out yet\".\n * Its audience reads it once it is out; a writer reads every one that is not somebody else's draft.\n */\nexport async function visibleAnnouncement(id: string, context: any, tx?: EntityManager): Promise<{ announcement: Announcement; reader: boolean; writer: boolean } | undefined> {\n const { domain, user } = context?.state ?? {}\n if (!id || !domain || !user || !/^[0-9a-f-]{36}$/i.test(id)) return undefined\n const announcement = await getRepository(Announcement, tx).findOne({ where: { id, domain: { id: domain.id } } as any })\n if (!announcement) return undefined\n const reader = announcement.status === AnnouncementStatus.PUBLISHED && (await reachingIds(await memberOf(context), tx)).includes(id)\n const writer = (announcement.status !== AnnouncementStatus.DRAFT || announcement.creatorId === user.id) && (await isWriter(context))\n return reader || writer ? { announcement, reader, writer } : undefined\n}\n\n/**\n * Puts the roster in line with the audience as it stands: everyone it names now is listed, one it no longer names is\n * not (their reading stays). Answers those newly listed - the ones not yet told.\n */\nexport async function alignRoster(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<string[]> {\n const named = await audienceUserIds(domain, await audiencesOf(announcement.id, tx), tx)\n const repository = getRepository(AnnouncementReceipt, tx)\n const receipts = await repository.find({ where: { announcement: { id: announcement.id } } as any })\n const byUser = new Map(receipts.map(one => [one.userId, one]))\n const now = new Date()\n const added: string[] = []\n for (const userId of named) {\n const receipt = byUser.get(userId)\n if (receipt?.listed) continue\n added.push(userId)\n if (receipt) await repository.update(receipt.id, { listed: true, notifiedAt: now, updater: by } as any)\n else await repository.save({ domain, announcement: { id: announcement.id }, user: { id: userId }, listed: true, notifiedAt: now, creator: by, updater: by } as any)\n }\n const gone = receipts.filter(one => one.listed && !named.includes(one.userId!))\n if (gone.length) await repository.update({ id: In(gone.map(one => one.id)) } as any, { listed: false, updater: by } as any)\n await getRepository(Announcement, tx).update(announcement.id, { audienceSize: named.length } as any)\n return added\n}\n\n/** The version as it stands, kept - the one that went out, and each change after. */\nexport async function keepRevision(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<void> {\n const audiences = await audiencesOf(announcement.id, tx)\n const { title, body, category, importance, startsAt, endsAt, pinned, requiresAcknowledgement, acknowledgeBy, popup, links } = announcement\n await getRepository(AnnouncementRevision, tx).save({\n domain,\n announcement: { id: announcement.id },\n version: announcement.version,\n title,\n body,\n rest: {\n category,\n importance,\n startsAt,\n endsAt,\n pinned,\n requiresAcknowledgement,\n acknowledgeBy,\n popup,\n links,\n audiences: audiences.map(({ scope, targetId, targetName }) => ({ scope, targetId, targetName }))\n },\n creator: by,\n updater: by\n } as any)\n}\n\n/**\n * Sends it out: published now, the roster counted, this version kept. Answers the people to tell - the publisher is not\n * told of their own announcement.\n */\nexport async function sendOut(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<string[]> {\n const now = new Date()\n await getRepository(Announcement, tx).update(announcement.id, {\n status: AnnouncementStatus.PUBLISHED,\n publishedAt: now,\n startsAt: announcement.startsAt ?? now,\n publisher: by,\n updater: by\n } as any)\n const fresh = await getRepository(Announcement, tx).findOneBy({ id: announcement.id })\n await keepRevision(fresh!, domain, by, tx)\n return (await alignRoster(fresh!, domain, by, tx)).filter(id => id !== by.id)\n}\n\n/** The sentences in `lng` from this module's translations, the English one where `lng` has none. */\nconst sayIn =\n (lng: string): Say =>\n (key, english, params = {}) =>\n translatorFor(lng)(key, { ...params, defaultValue: english.replace(/\\{(\\w+)\\}/g, (_, name) => String(params[name] ?? '')) })\n\n/** The people to tell, as notification takes them - only those who can sign in, each with their mail and language. */\nasync function receiversOf(userIds: string[], tx?: EntityManager) {\n if (!userIds.length) return []\n const people = await getRepository(User, tx).find({ where: { id: In(userIds) } as any })\n return people.filter(one => one.status === UserStatus.ACTIVATED).map(one => ({ id: one.id, email: one.email, locale: (one as any).locale }))\n}\n\n/**\n * The notice of an announcement (notification ADR-0164): in the app and by web push always - the `announcement` type is\n * what pops it up - and by mail when the announcement asks for it and the domain has mail on.\n */\nfunction noticeOf(one: Announcement, kind: NoticeKind, receivers: Notice['receivers'], broadcast: boolean, link?: string): Notice {\n return {\n type: 'announcement',\n level: one.importance === 'URGENT' ? 'warn' : 'info',\n receivers,\n words: lng => noticeWordsOf(one, kind, sayIn(lng)),\n address: viewAddressOf(one.id),\n link,\n label: `announcement ${kind} ${one.id}`,\n /* The mail carries the whole body, drawn from its Markdown; the inbox and push show the summary. */\n format: 'markdown',\n channels: { email: !!one.sendEmail },\n /* Said once to the domain's Teams channel too - only what is addressed to everyone, and only when it goes out or all are told again. */\n ...(broadcast ? { broadcast: true } : {})\n }\n}\n\n/** Keeps what one telling sent on the announcement - in its own transaction, the request's being over. */\nasync function recordDelivery(id: string, kind: NoticeKind, result: NoticeResult | undefined, receivers: number): Promise<void> {\n try {\n const repository = getRepository(Announcement)\n const one = await repository.findOneBy({ id })\n if (one) await repository.update(id, { deliveries: withDelivery(one.deliveries, deliveryOf(kind, result, receivers)) } as any)\n } catch (error) {\n logger.error(`announcement ${id}: what was sent could not be kept`, error)\n }\n}\n\n/**\n * Tells `userIds` of it once the request's transaction has committed - a reader told before it would not find it yet -\n * and keeps what went out. A notice that does not arrive does not undo the announcement: its list, badge and popup\n * still reach them.\n */\nexport async function tellWhenCommitted(context: any, one: Announcement, kind: NoticeKind, userIds: string[], toEveryone = false): Promise<void> {\n const receivers = await receiversOf(userIds, context.state.tx)\n if (!receivers.length) return\n const broadcast = toEveryone && broadcastOf(await audiencesOf(one.id, context.state.tx), context.state.domain?.id)\n sendNoticeWhenCommitted(context, noticeOf(one, kind, receivers, broadcast, noticeLinkOf(context, viewAddressOf(one.id))), result =>\n recordDelivery(one.id, kind, result, receivers.length)\n )\n}\n\n/**\n * Publishes the scheduled announcements whose start has come. Each is taken by one update from SCHEDULED, so a server\n * among several that loses the race does not send it twice.\n */\nexport async function publishDue(now = new Date()): Promise<number> {\n const due = await getRepository(Announcement).find({\n where: { status: AnnouncementStatus.SCHEDULED, startsAt: LessThanOrEqual(now), deletedAt: IsNull() } as any,\n relations: ['domain', 'publisher', 'creator'],\n take: 50\n })\n let published = 0\n for (const one of due) {\n try {\n const by = { id: one.publisherId ?? one.creatorId! }\n const receivers = await getDataSource().transaction(async tx => {\n const taken = await tx.getRepository(Announcement).update({ id: one.id, status: AnnouncementStatus.SCHEDULED } as any, { status: AnnouncementStatus.DRAFT } as any)\n if (!taken.affected) return undefined\n return await sendOut({ ...one, status: AnnouncementStatus.DRAFT } as Announcement, one.domain!, by, tx)\n })\n if (!receivers) continue\n published++\n /* No request, so no address to put on a mail button - the mail says where to find it instead. */\n const people = await receiversOf(receivers)\n const broadcast = broadcastOf(await audiencesOf(one.id), one.domain?.id)\n if (people.length) await recordDelivery(one.id, 'published', await sendNotice(one.domain, noticeOf(one, 'published', people, broadcast)), people.length)\n } catch (error) {\n logger.error(`announcement ${one.id}: publishing at its start failed`, error)\n }\n }\n return published\n}\n\nlet timer: ReturnType<typeof setInterval> | undefined\n\n/** Looks for scheduled announcements every minute. The timer never keeps the process alive. */\nexport function schedulePublishing(): void {\n if (timer) return\n timer = setInterval(() => void publishDue().catch(error => logger.error('scheduled announcements', error)), 60 * 1000)\n timer.unref?.()\n}\n"]}
|
|
1
|
+
{"version":3,"file":"announcement-service.js","sourceRoot":"","sources":["../../../server/service/announcement/announcement-service.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the doors of announcements do with the database (ADR-0163 decisions 1-5): who may read one, who is on its roster,\n * sending it out, telling people of it, and publishing the scheduled ones when their start comes.\n */\nimport { EntityManager, In, IsNull, LessThanOrEqual } from 'typeorm'\n\nimport { checkPermission, User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { keepNoticeDelivery, noticeDeliveryOf, noticeLinkOf, sendNotice, sendNoticeWhenCommitted, type Notice } from '@things-factory/notification'\nimport { audienceMemberOf, audienceUserIds, type AudienceMember } from '@things-factory/organization'\nimport { Domain, getDataSource, getRepository } from '@things-factory/shell'\n\nimport { Announcement, AnnouncementAudience, AnnouncementReceipt, AnnouncementRevision, AnnouncementStatus } from './announcement'\nimport { broadcastOf, noticeWordsOf, viewAddressOf, type NoticeKind, type Say } from './announcement-rules'\n\n/** The attachments of an announcement hang on it under this `refType` (attachment-base). */\nexport const ANNOUNCEMENT_REF_TYPE = 'Announcement'\n\n/** Whether the request writes announcements - the one privilege of the module (`announcement.writing`). */\nexport async function isWriter(context: any): Promise<boolean> {\n const state = context?.state ?? {}\n const { domain, user, unsafeIP, prohibitedPrivileges } = state\n if (!domain || !user) return false\n /* Asked once per request - a list asks it for each row. */\n state.__announcementWriter ??= checkPermission({ category: 'announcement', privilege: 'mutation', owner: true } as any, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean)\n return await state.__announcementWriter\n}\n\n/** The request's person as an audience sees them - asked once per request. */\nexport async function memberOf(context: any): Promise<AudienceMember> {\n const state = context.state\n if (!state.__announcementMember) {\n const { ownDepartments, ...member } = await audienceMemberOf(state.domain, state.user ?? {})\n state.__announcementMember = member\n }\n return state.__announcementMember\n}\n\n/** The announcements of the domain whose audience names `member` - by their targets, whatever their status. */\nexport async function reachingIds(member: AudienceMember, tx?: EntityManager): Promise<string[]> {\n if (!member.userId || !member.domainId) return []\n const domain = { id: member.domainId }\n const rows = await getRepository(AnnouncementAudience, tx).find({\n where: [\n { domain, scope: 'DOMAIN', targetId: member.domainId },\n { domain, scope: 'USER', targetId: member.userId },\n ...(member.departmentIds.length ? [{ domain, scope: 'DEPARTMENT', targetId: In([...member.departmentIds]) }] : []),\n ...(member.roleIds.length ? [{ domain, scope: 'ROLE', targetId: In([...member.roleIds]) }] : [])\n ] as any\n })\n return [...new Set(rows.map(one => one.announcementId!))]\n}\n\nexport async function audiencesOf(announcementId: string, tx?: EntityManager): Promise<AnnouncementAudience[]> {\n return await getRepository(AnnouncementAudience, tx).find({ where: { announcement: { id: announcementId } } as any, order: { createdAt: 'ASC' } as any })\n}\n\n/**\n * The announcement as the request may see it, or nothing - one answer for \"none\", \"not yours\" and \"not out yet\".\n * Its audience reads it once it is out; a writer reads every one that is not somebody else's draft.\n */\nexport async function visibleAnnouncement(id: string, context: any, tx?: EntityManager): Promise<{ announcement: Announcement; reader: boolean; writer: boolean } | undefined> {\n const { domain, user } = context?.state ?? {}\n if (!id || !domain || !user || !/^[0-9a-f-]{36}$/i.test(id)) return undefined\n const announcement = await getRepository(Announcement, tx).findOne({ where: { id, domain: { id: domain.id } } as any })\n if (!announcement) return undefined\n const reader = announcement.status === AnnouncementStatus.PUBLISHED && (await reachingIds(await memberOf(context), tx)).includes(id)\n const writer = (announcement.status !== AnnouncementStatus.DRAFT || announcement.creatorId === user.id) && (await isWriter(context))\n return reader || writer ? { announcement, reader, writer } : undefined\n}\n\n/**\n * Puts the roster in line with the audience as it stands: everyone it names now is listed, one it no longer names is\n * not (their reading stays). Answers those newly listed - the ones not yet told.\n */\nexport async function alignRoster(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<string[]> {\n const named = await audienceUserIds(domain, await audiencesOf(announcement.id, tx), tx)\n const repository = getRepository(AnnouncementReceipt, tx)\n const receipts = await repository.find({ where: { announcement: { id: announcement.id } } as any })\n const byUser = new Map(receipts.map(one => [one.userId, one]))\n const now = new Date()\n const added: string[] = []\n for (const userId of named) {\n const receipt = byUser.get(userId)\n if (receipt?.listed) continue\n added.push(userId)\n if (receipt) await repository.update(receipt.id, { listed: true, notifiedAt: now, updater: by } as any)\n else await repository.save({ domain, announcement: { id: announcement.id }, user: { id: userId }, listed: true, notifiedAt: now, creator: by, updater: by } as any)\n }\n const gone = receipts.filter(one => one.listed && !named.includes(one.userId!))\n if (gone.length) await repository.update({ id: In(gone.map(one => one.id)) } as any, { listed: false, updater: by } as any)\n await getRepository(Announcement, tx).update(announcement.id, { audienceSize: named.length } as any)\n return added\n}\n\n/** The version as it stands, kept - the one that went out, and each change after. */\nexport async function keepRevision(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<void> {\n const audiences = await audiencesOf(announcement.id, tx)\n const { title, body, category, importance, startsAt, endsAt, pinned, requiresAcknowledgement, acknowledgeBy, popup, links } = announcement\n await getRepository(AnnouncementRevision, tx).save({\n domain,\n announcement: { id: announcement.id },\n version: announcement.version,\n title,\n body,\n rest: {\n category,\n importance,\n startsAt,\n endsAt,\n pinned,\n requiresAcknowledgement,\n acknowledgeBy,\n popup,\n links,\n audiences: audiences.map(({ scope, targetId, targetName }) => ({ scope, targetId, targetName }))\n },\n creator: by,\n updater: by\n } as any)\n}\n\n/**\n * Sends it out: published now, the roster counted, this version kept. Answers the people to tell - the publisher is not\n * told of their own announcement.\n */\nexport async function sendOut(announcement: Announcement, domain: Domain, by: { id: string }, tx: EntityManager): Promise<string[]> {\n const now = new Date()\n await getRepository(Announcement, tx).update(announcement.id, {\n status: AnnouncementStatus.PUBLISHED,\n publishedAt: now,\n startsAt: announcement.startsAt ?? now,\n publisher: by,\n updater: by\n } as any)\n const fresh = await getRepository(Announcement, tx).findOneBy({ id: announcement.id })\n await keepRevision(fresh!, domain, by, tx)\n return (await alignRoster(fresh!, domain, by, tx)).filter(id => id !== by.id)\n}\n\n/** The sentences in `lng` from this module's translations, the English one where `lng` has none. */\nconst sayIn =\n (lng: string): Say =>\n (key, english, params = {}) =>\n translatorFor(lng)(key, { ...params, defaultValue: english.replace(/\\{(\\w+)\\}/g, (_, name) => String(params[name] ?? '')) })\n\n/** The people to tell, as notification takes them - only those who can sign in, each with their mail and language. */\nasync function receiversOf(userIds: string[], tx?: EntityManager) {\n if (!userIds.length) return []\n const people = await getRepository(User, tx).find({ where: { id: In(userIds) } as any })\n return people.filter(one => one.status === UserStatus.ACTIVATED).map(one => ({ id: one.id, email: one.email, locale: (one as any).locale }))\n}\n\n/**\n * The notice of an announcement (notification ADR-0164): in the app and by web push always - the `announcement` type is\n * what pops it up - and by mail when the announcement asks for it and the domain has mail on.\n */\nfunction noticeOf(one: Announcement, kind: NoticeKind, receivers: Notice['receivers'], broadcast: boolean, link?: string): Notice {\n return {\n type: 'announcement',\n level: one.importance === 'URGENT' ? 'warn' : 'info',\n receivers,\n words: lng => noticeWordsOf(one, kind, sayIn(lng)),\n address: viewAddressOf(one.id),\n link,\n label: `announcement ${kind} ${one.id}`,\n /* The mail carries the whole body, drawn from its Markdown; the inbox and push show the summary. */\n format: 'markdown',\n channels: { email: !!one.sendEmail },\n /* Said once to the domain's Teams channel too - only what is addressed to everyone, and only when it goes out or all are told again. */\n ...(broadcast ? { broadcast: true } : {})\n }\n}\n\n/**\n * Tells `userIds` of it once the request's transaction has committed - a reader told before it would not find it yet -\n * and keeps what went out. A notice that does not arrive does not undo the announcement: its list, badge and popup\n * still reach them.\n */\nexport async function tellWhenCommitted(context: any, one: Announcement, kind: NoticeKind, userIds: string[], toEveryone = false): Promise<void> {\n const receivers = await receiversOf(userIds, context.state.tx)\n if (!receivers.length) return\n const broadcast = toEveryone && broadcastOf(await audiencesOf(one.id, context.state.tx), context.state.domain?.id)\n sendNoticeWhenCommitted(context, noticeOf(one, kind, receivers, broadcast, noticeLinkOf(context, viewAddressOf(one.id))), result =>\n keepNoticeDelivery(Announcement, one.id, 'deliveries', noticeDeliveryOf(kind, result, receivers.length))\n )\n}\n\n/**\n * Publishes the scheduled announcements whose start has come. Each is taken by one update from SCHEDULED, so a server\n * among several that loses the race does not send it twice.\n */\nexport async function publishDue(now = new Date()): Promise<number> {\n const due = await getRepository(Announcement).find({\n where: { status: AnnouncementStatus.SCHEDULED, startsAt: LessThanOrEqual(now), deletedAt: IsNull() } as any,\n relations: ['domain', 'publisher', 'creator'],\n take: 50\n })\n let published = 0\n for (const one of due) {\n try {\n const by = { id: one.publisherId ?? one.creatorId! }\n const receivers = await getDataSource().transaction(async tx => {\n const taken = await tx.getRepository(Announcement).update({ id: one.id, status: AnnouncementStatus.SCHEDULED } as any, { status: AnnouncementStatus.DRAFT } as any)\n if (!taken.affected) return undefined\n return await sendOut({ ...one, status: AnnouncementStatus.DRAFT } as Announcement, one.domain!, by, tx)\n })\n if (!receivers) continue\n published++\n /* No request, so no address to put on a mail button - the mail says where to find it instead. */\n const people = await receiversOf(receivers)\n const broadcast = broadcastOf(await audiencesOf(one.id), one.domain?.id)\n if (people.length) await keepNoticeDelivery(Announcement, one.id, 'deliveries', noticeDeliveryOf('published', await sendNotice(one.domain, noticeOf(one, 'published', people, broadcast)), people.length))\n } catch (error) {\n logger.error(`announcement ${one.id}: publishing at its start failed`, error)\n }\n }\n return published\n}\n\nlet timer: ReturnType<typeof setInterval> | undefined\n\n/** Looks for scheduled announcements every minute. The timer never keeps the process alive. */\nexport function schedulePublishing(): void {\n if (timer) return\n timer = setInterval(() => void publishDue().catch(error => logger.error('scheduled announcements', error)), 60 * 1000)\n timer.unref?.()\n}\n"]}
|