@things-factory/notification 10.1.247 → 10.1.250
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-server/controllers/notice/deliver-notice.d.ts +1 -1
- package/dist-server/controllers/notice/deliver-notice.js +4 -4
- package/dist-server/controllers/notice/deliver-notice.js.map +1 -1
- package/dist-server/controllers/notice/index.d.ts +0 -2
- package/dist-server/controllers/notice/index.js +0 -2
- package/dist-server/controllers/notice/index.js.map +1 -1
- package/dist-server/controllers/notice/notice-channels.d.ts +2 -6
- package/dist-server/controllers/notice/notice-channels.js +2 -9
- package/dist-server/controllers/notice/notice-channels.js.map +1 -1
- package/dist-server/controllers/notice/send-notice.js +1 -2
- package/dist-server/controllers/notice/send-notice.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/tests/notice-delivery.test.ts +1 -103
- package/dist-server/controllers/notice/app-brand.d.ts +0 -17
- package/dist-server/controllers/notice/app-brand.js +0 -61
- package/dist-server/controllers/notice/app-brand.js.map +0 -1
- package/dist-server/controllers/notice/notice-email.d.ts +0 -32
- package/dist-server/controllers/notice/notice-email.js +0 -129
- package/dist-server/controllers/notice/notice-email.js.map +0 -1
|
@@ -5,8 +5,8 @@ exports.deliverNotice = deliverNotice;
|
|
|
5
5
|
* Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and
|
|
6
6
|
* sees what each one was given; send-notice.ts hands in the real ones.
|
|
7
7
|
*/
|
|
8
|
+
const email_base_1 = require("@things-factory/email-base");
|
|
8
9
|
const notice_channels_js_1 = require("./notice-channels.js");
|
|
9
|
-
const notice_email_js_1 = require("./notice-email.js");
|
|
10
10
|
/** The domain's own address - the notice's full link without its page. */
|
|
11
11
|
function baseOf(notice) {
|
|
12
12
|
return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined;
|
|
@@ -30,7 +30,7 @@ async function deliverNotice(notice, channels) {
|
|
|
30
30
|
const email = turnedOn.email && notice.channels?.email !== false;
|
|
31
31
|
const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined;
|
|
32
32
|
for (const receiver of notice.receivers) {
|
|
33
|
-
const lng = (0,
|
|
33
|
+
const lng = (0, email_base_1.languageOf)(receiver.locale, notice.defaultLanguage);
|
|
34
34
|
let words;
|
|
35
35
|
try {
|
|
36
36
|
words = notice.words(lng);
|
|
@@ -64,7 +64,7 @@ async function deliverNotice(notice, channels) {
|
|
|
64
64
|
const sent = await channels.mail({
|
|
65
65
|
receiver: receiver.email,
|
|
66
66
|
subject: words.subject,
|
|
67
|
-
content: (0,
|
|
67
|
+
content: (0, email_base_1.mailForm)({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })
|
|
68
68
|
});
|
|
69
69
|
if (sent) {
|
|
70
70
|
result.email.sent++;
|
|
@@ -82,7 +82,7 @@ async function deliverNotice(notice, channels) {
|
|
|
82
82
|
}
|
|
83
83
|
if (teamsChannelWebhook) {
|
|
84
84
|
try {
|
|
85
|
-
const answer = await channels.teams(teamsChannelWebhook, (0, notice_channels_js_1.teamsMessage)({ words: notice.words((0,
|
|
85
|
+
const answer = await channels.teams(teamsChannelWebhook, (0, notice_channels_js_1.teamsMessage)({ words: notice.words((0, email_base_1.languageOf)(undefined, notice.defaultLanguage)), link: notice.link }));
|
|
86
86
|
if (answer.ok) {
|
|
87
87
|
result.teams.sent++;
|
|
88
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\nimport { noticeEmailForm } from './notice-email.js'\nimport type { AppBrand } from './app-brand.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice opens. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n /** The application whose name, tagline and colour the mail carries; absent, the default. */\n brand?: AppBrand\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: noticeEmailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
|
|
1
|
+
{"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, mailForm, type AppBrand } from '@things-factory/email-base'\n\nimport { noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice opens. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n /** The application whose name, tagline and colour the mail carries; absent, the default. */\n brand?: AppBrand\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: mailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
tslib_1.__exportStar(require("./notice-channels.js"), exports);
|
|
5
|
-
tslib_1.__exportStar(require("./notice-email.js"), exports);
|
|
6
5
|
tslib_1.__exportStar(require("./deliver-notice.js"), exports);
|
|
7
6
|
tslib_1.__exportStar(require("./send-notice.js"), exports);
|
|
8
|
-
tslib_1.__exportStar(require("./app-brand.js"), exports);
|
|
9
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/notice/index.ts"],"names":[],"mappings":";;;AAAA,+DAAoC;AACpC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/controllers/notice/index.ts"],"names":[],"mappings":";;;AAAA,+DAAoC;AACpC,8DAAmC;AACnC,2DAAgC","sourcesContent":["export * from './notice-channels.js'\nexport * from './deliver-notice.js'\nexport * from './send-notice.js'\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MailWords } from '@things-factory/email-base';
|
|
1
2
|
/** The domain attributes (EnvVar) that turn the channels on. Web push needs none. */
|
|
2
3
|
export declare const NOTICE_EMAIL = "Notice::email";
|
|
3
4
|
/** The Workflows webhook that posts to one Teams channel - where a notice to everyone in the domain goes. */
|
|
@@ -13,17 +14,12 @@ export declare function noticeChannels(attributes: Record<string, string | undef
|
|
|
13
14
|
email: boolean;
|
|
14
15
|
teamsChannelWebhook?: string;
|
|
15
16
|
};
|
|
16
|
-
/** `ko-KR` → `ko`: the server's translations are kept by language, not by region. */
|
|
17
|
-
export declare function languageOf(locale: string | null | undefined, fallback: string | null | undefined): string;
|
|
18
17
|
/**
|
|
19
18
|
* What a notice says to one person, in their language. `note` stands in for the button when there is no link.
|
|
20
19
|
* `summary` is what the in-app notice and web push show when the body is long (an announcement's whole text goes by
|
|
21
20
|
* mail); left out, they show the body.
|
|
22
21
|
*/
|
|
23
|
-
export type NoticeWords = {
|
|
24
|
-
subject: string;
|
|
25
|
-
body: string;
|
|
26
|
-
action: string;
|
|
22
|
+
export type NoticeWords = MailWords & {
|
|
27
23
|
note: string;
|
|
28
24
|
summary?: string;
|
|
29
25
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* The channels a notice goes down (ADR-0164) - which ones a domain has turned on, the
|
|
4
|
-
*
|
|
3
|
+
* The channels a notice goes down (ADR-0164) - which ones a domain has turned on, and the Teams message. Plain
|
|
4
|
+
* functions over plain values, so a test holds them without a database.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.NOTICE_ATTRIBUTE_PREFIX = exports.NOTICE_TEAMS_CHANNEL = exports.NOTICE_EMAIL = void 0;
|
|
8
8
|
exports.noticeChannels = noticeChannels;
|
|
9
|
-
exports.languageOf = languageOf;
|
|
10
9
|
exports.teamsMessage = teamsMessage;
|
|
11
10
|
/** The domain attributes (EnvVar) that turn the channels on. Web push needs none. */
|
|
12
11
|
exports.NOTICE_EMAIL = 'Notice::email';
|
|
@@ -27,12 +26,6 @@ function noticeChannels(attributes) {
|
|
|
27
26
|
};
|
|
28
27
|
return { email, teamsChannelWebhook: https(exports.NOTICE_TEAMS_CHANNEL) };
|
|
29
28
|
}
|
|
30
|
-
/** `ko-KR` → `ko`: the server's translations are kept by language, not by region. */
|
|
31
|
-
function languageOf(locale, fallback) {
|
|
32
|
-
return String(locale || fallback || 'en')
|
|
33
|
-
.split(/[-_]/)[0]
|
|
34
|
-
.toLowerCase();
|
|
35
|
-
}
|
|
36
29
|
/**
|
|
37
30
|
* The body posted to the Teams channel's Workflows webhook: a Teams message with one Adaptive Card, which the channel
|
|
38
31
|
* template posts as it is given.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notice-channels.js","sourceRoot":"","sources":["../../../server/controllers/notice/notice-channels.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;
|
|
1
|
+
{"version":3,"file":"notice-channels.js","sourceRoot":"","sources":["../../../server/controllers/notice/notice-channels.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAgBH,wCAWC;AAaD,oCAqBC;AAzDD,qFAAqF;AACxE,QAAA,YAAY,GAAG,eAAe,CAAA;AAC3C,6GAA6G;AAChG,QAAA,oBAAoB,GAAG,6BAA6B,CAAA;AACpD,QAAA,uBAAuB,GAAG,UAAU,CAAA;AAEjD;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,UAAiE;IAI9F,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,oBAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,IAAI,CAAA;IACpF,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACvD,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;IAC3D,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,CAAC,4BAAoB,CAAC,EAAE,CAAA;AACpE,CAAC;AASD;;;GAGG;AACH,SAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAyC;IACjF,OAAO;QACL,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,WAAW,EAAE;YACX;gBACE,WAAW,EAAE,yCAAyC;gBACtD,UAAU,EAAE,IAAI;gBAChB,OAAO,EAAE;oBACP,OAAO,EAAE,oDAAoD;oBAC7D,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,KAAK;oBACd,IAAI,EAAE;wBACJ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE;wBACxF,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;qBAC9F;oBACD,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;iBAClF;aACF;SACF;KACF,CAAA;AACH,CAAC","sourcesContent":["/*\n * The channels a notice goes down (ADR-0164) - which ones a domain has turned on, and the Teams message. Plain\n * functions over plain values, so a test holds them without a database.\n */\n\nimport type { MailWords } from '@things-factory/email-base'\n\n/** The domain attributes (EnvVar) that turn the channels on. Web push needs none. */\nexport const NOTICE_EMAIL = 'Notice::email'\n/** The Workflows webhook that posts to one Teams channel - where a notice to everyone in the domain goes. */\nexport const NOTICE_TEAMS_CHANNEL = 'Notice::teamsChannelWebhook'\nexport const NOTICE_ATTRIBUTE_PREFIX = 'Notice::'\n\n/**\n * Which channels a domain has turned on. Mail goes when the domain says `on` - a server carries several domains,\n * and a development server carries real accounts. Teams goes to one channel when the domain holds the address of the\n * channel's Workflows webhook; anything but an https address is not one. There is no Teams to each person: its flow\n * needed expressions typed into the Workflows designer, and the user dropped it (10-26) - a person is told by mail.\n */\nexport function noticeChannels(attributes: Record<string, string | undefined> | null | undefined): {\n email: boolean\n teamsChannelWebhook?: string\n} {\n const email = String(attributes?.[NOTICE_EMAIL] ?? '').trim().toLowerCase() === 'on'\n const https = (name: string) => {\n const webhook = String(attributes?.[name] ?? '').trim()\n return /^https:\\/\\//i.test(webhook) ? webhook : undefined\n }\n\n return { email, teamsChannelWebhook: https(NOTICE_TEAMS_CHANNEL) }\n}\n\n/**\n * What a notice says to one person, in their language. `note` stands in for the button when there is no link.\n * `summary` is what the in-app notice and web push show when the body is long (an announcement's whole text goes by\n * mail); left out, they show the body.\n */\nexport type NoticeWords = MailWords & { note: string; summary?: string }\n\n/**\n * The body posted to the Teams channel's Workflows webhook: a Teams message with one Adaptive Card, which the channel\n * template posts as it is given.\n */\nexport function teamsMessage({ words, link }: { words: NoticeWords; link?: string }) {\n return {\n type: 'message',\n summary: words.subject,\n attachments: [\n {\n contentType: 'application/vnd.microsoft.card.adaptive',\n contentUrl: null,\n content: {\n $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',\n type: 'AdaptiveCard',\n version: '1.4',\n body: [\n { type: 'TextBlock', text: words.subject, weight: 'Bolder', size: 'Medium', wrap: true },\n { type: 'TextBlock', text: link ? words.body : `${words.body}\\n\\n${words.note}`, wrap: true }\n ],\n actions: link ? [{ type: 'Action.OpenUrl', title: words.action, url: link }] : []\n }\n }\n ]\n }\n}\n"]}
|
|
@@ -15,7 +15,6 @@ const shell_1 = require("@things-factory/shell");
|
|
|
15
15
|
const notification_middleware_js_1 = require("../../middlewares/notification-middleware.js");
|
|
16
16
|
const deliver_notice_js_1 = require("./deliver-notice.js");
|
|
17
17
|
const notice_channels_js_1 = require("./notice-channels.js");
|
|
18
|
-
const app_brand_js_1 = require("./app-brand.js");
|
|
19
18
|
const report = (message, error) => env_1.logger.error(`[notification] ${message}`, error);
|
|
20
19
|
/** The real channels of a domain. */
|
|
21
20
|
function noticeChannelsOf(domain, push) {
|
|
@@ -28,7 +27,7 @@ function noticeChannelsOf(domain, push) {
|
|
|
28
27
|
return { ok: response.ok, status: response.status };
|
|
29
28
|
},
|
|
30
29
|
report,
|
|
31
|
-
brand: (0,
|
|
30
|
+
brand: (0, email_base_1.appBrand)()
|
|
32
31
|
};
|
|
33
32
|
}
|
|
34
33
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/send-notice.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"send-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/send-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The one way a module tells people of something (ADR-0164): in the app and by web push always, by mail and Teams\n * when the domain turned them on. The module settles who and what; this sends it.\n */\nimport { buildAuthURL } from '@things-factory/auth-base'\nimport { appBrand, sendEmail } from '@things-factory/email-base'\nimport { config, logger } from '@things-factory/env'\nimport { getContextPath, loadDomainAttributes, whenCommitted } from '@things-factory/shell'\n\nimport { notifierOf } from '../../middlewares/notification-middleware.js'\nimport { deliverNotice, type Notice, type NoticeChannelFunctions, type NoticeResult } from './deliver-notice.js'\nimport { NOTICE_ATTRIBUTE_PREFIX } from './notice-channels.js'\n\nconst report = (message: string, error?: unknown) => logger.error(`[notification] ${message}`, error)\n\n/** The real channels of a domain. */\nexport function noticeChannelsOf(domain: any, push?: (payload: any) => unknown): NoticeChannelFunctions {\n return {\n attributes: () => loadDomainAttributes(domain, NOTICE_ATTRIBUTE_PREFIX),\n push: push ?? notifierOf(domain),\n mail: mail => sendEmail(mail) as Promise<unknown>,\n teams: async (url, body) => {\n const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })\n return { ok: response.ok, status: response.status }\n },\n report,\n brand: appBrand()\n }\n}\n\n/**\n * Sends now and answers what went out - for work with no request (a scheduled publication), or a sender that records\n * the result. Never throws; a notice that could not be sent at all answers undefined.\n */\nexport async function sendNotice(domain: any, notice: Notice, push?: (payload: any) => unknown): Promise<NoticeResult | undefined> {\n try {\n return await deliverNotice({ defaultLanguage: config.get('i18n/defaultLanguage', 'en'), ...notice }, noticeChannelsOf(domain, push))\n } catch (error) {\n report(`${notice.label}: sending failed`, error)\n return undefined\n }\n}\n\n/**\n * Sends once the request's transaction has committed, and does not make the request wait for it - a slow mail server\n * does not hold the button that caused the notice. Rolled back, nothing goes. With no transaction list, it goes now.\n * `sent` hears what went out, for a sender that records it (in its own transaction - the request's is over).\n */\nexport function sendNoticeWhenCommitted(\n context: ResolverContext,\n notice: Notice,\n sent?: (result: NoticeResult | undefined) => unknown | Promise<unknown>\n): void {\n const { domain } = context.state\n const push = (context.state as any).notify\n const send = () => {\n void sendNotice(domain, notice, push)\n .then(result => sent?.(result))\n .catch(error => report(`${notice.label}: recording what was sent failed`, error))\n }\n\n if (!whenCommitted((context.state as any).afterCommit, notice.label, send)) send()\n}\n\n/** A page of the request's domain as a full address - where this request came in. None with no request. */\nexport function noticeLinkOf(context: ResolverContext, address: string): string | undefined {\n try {\n if (!(context as any)?.request) return undefined\n return `${buildAuthURL('/', context)}${getContextPath(context.state.domain)}${address}`\n } catch {\n return undefined\n }\n}\n"]}
|