|
|
|
@@ -262,6 +262,98 @@ Notifications.filternoitif = (tribe, tpl, alias, channel = "push", topics = [])
|
|
|
|
|
return ok;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Language of a recipient: the language of its own app (persons.lg, stamped by
|
|
|
|
|
* Persons.managetopic on each topic subscription) wins over the language of
|
|
|
|
|
* whoever triggered the event, which is only a fallback. Anything outside
|
|
|
|
|
* fr/en is forced to "en", the same way checkHeaders forces xlang.
|
|
|
|
|
*/
|
|
|
|
|
Notifications.recipientlang = (person, fallback) =>
|
|
|
|
|
person && ["fr", "en"].includes(person.lg)
|
|
|
|
|
? person.lg
|
|
|
|
|
: ["fr", "en"].includes(fallback)
|
|
|
|
|
? fallback
|
|
|
|
|
: "en";
|
|
|
|
|
|
|
|
|
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
|
|
|
const localdate_re = /\{\{#localdate\}\}\s*([^\s}]+)\s*\{\{\/localdate\}\}/g;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A date is never formatted by the caller: it sends the raw value (ISO) and the
|
|
|
|
|
* template declares how to display it.
|
|
|
|
|
*
|
|
|
|
|
* seekertemplate_fr.js : tpl.dateformat = { bookdate: "DD/MM/YYYY" }
|
|
|
|
|
* template : ... le {{#localdate}}bookdate{{/localdate}} ...
|
|
|
|
|
*
|
|
|
|
|
* The text of the section is the NAME of the field to display, and it is not
|
|
|
|
|
* "{{bookdate}}": mustache hands the raw inner text of a lambda, it does not
|
|
|
|
|
* render it, so the value is read in the view (closure) and not through a
|
|
|
|
|
* render callback. tpl.dateformat gives the dayjs format of that field.
|
|
|
|
|
*
|
|
|
|
|
* Anything that is not an ISO date (missing, empty, ...) is returned untouched,
|
|
|
|
|
* which also prevents an "Invalid Date" from reaching a recipient.
|
|
|
|
|
*
|
|
|
|
|
* The value MUST be a factory (() => function (text)): mustache calls a function
|
|
|
|
|
* found in the data with no argument during the lookup
|
|
|
|
|
* (mustache.js Context.lookup), so a plain (text) => ... lambda is silently
|
|
|
|
|
* ignored and the section renders its own content.
|
|
|
|
|
*/
|
|
|
|
|
Notifications.localdate = (tpl, view) => () => (text) => {
|
|
|
|
|
const field = text.trim();
|
|
|
|
|
const raw = view ? view[field] : undefined;
|
|
|
|
|
const value = raw === undefined || raw === null ? "" : String(raw).trim();
|
|
|
|
|
const fmt = (tpl.dateformat || {})[field];
|
|
|
|
|
if (!fmt || !ISO_DATE_RE.test(value)) return value;
|
|
|
|
|
return dayjs(value).format(fmt);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Single rendering point for every notification template (email subject/html/
|
|
|
|
|
* text and push notiftitle/notifdescription).
|
|
|
|
|
*
|
|
|
|
|
* Notifications.render(tpl, view, ["subject", "html", "text"])
|
|
|
|
|
*
|
|
|
|
|
* `keys` is explicit on purpose: a template without tpl.text must keep failing
|
|
|
|
|
* instead of silently producing a mail without its text part. `meta` is only
|
|
|
|
|
* used to make the warnings actionable ({ template, lg }).
|
|
|
|
|
*
|
|
|
|
|
* Warns on both directions of a mismatch between tpl.dateformat and the
|
|
|
|
|
* template: a declared format never used means the raw ISO date is displayed,
|
|
|
|
|
* and a {{#localdate}} without declaration means the same thing silently.
|
|
|
|
|
*/
|
|
|
|
|
Notifications.render = (tpl, view, keys, meta = {}) => {
|
|
|
|
|
view.localdate = Notifications.localdate(tpl, view);
|
|
|
|
|
const rendered = {};
|
|
|
|
|
try {
|
|
|
|
|
for (const k of keys) rendered[k] = Mustache.render(tpl[k], view);
|
|
|
|
|
} finally {
|
|
|
|
|
// view is the caller object (req.body, emailsto entry, push recipient) and
|
|
|
|
|
// is persisted as is in persons.notifications[]: do not leak the factory.
|
|
|
|
|
delete view.localdate;
|
|
|
|
|
}
|
|
|
|
|
const sources = ["subject", "html", "text", "notiftitle", "notifdescription"]
|
|
|
|
|
.map((k) => (typeof tpl[k] === "string" ? tpl[k] : ""))
|
|
|
|
|
.join("\n");
|
|
|
|
|
const used = new Set([...sources.matchAll(localdate_re)].map((m) => m[1]));
|
|
|
|
|
const declared = Object.keys(tpl.dateformat || {});
|
|
|
|
|
const where = `${meta.template || "?"}_${meta.lg || "?"}`;
|
|
|
|
|
for (const field of declared) {
|
|
|
|
|
if (!used.has(field))
|
|
|
|
|
console.warn(
|
|
|
|
|
currentmod,
|
|
|
|
|
`dateformat "${field}" declared but never used in ${where}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (const field of used) {
|
|
|
|
|
if (!declared.includes(field))
|
|
|
|
|
console.warn(
|
|
|
|
|
currentmod,
|
|
|
|
|
`{{#localdate}}${field}} used without dateformat in ${where}, raw value displayed`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return rendered;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Notifications.manageemail = async (
|
|
|
|
|
data,
|
|
|
|
|
templatename,
|
|
|
|
@@ -360,9 +452,13 @@ Notifications.manageemail = async (
|
|
|
|
|
emailtosend.to = emailtosend.to.slice(0, -1);
|
|
|
|
|
emailtosend.templatename = templatename;
|
|
|
|
|
emailtosend.lg = lg;
|
|
|
|
|
emailtosend.subject = Mustache.render(tplemail.subject, email);
|
|
|
|
|
emailtosend.html = Mustache.render(tplemail.html, email);
|
|
|
|
|
emailtosend.text = Mustache.render(tplemail.text, email);
|
|
|
|
|
Object.assign(
|
|
|
|
|
emailtosend,
|
|
|
|
|
Notifications.render(tplemail, email, ["subject", "html", "text"], {
|
|
|
|
|
template: templatename,
|
|
|
|
|
lg,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
if (email.cc) {
|
|
|
|
|
emailtosend.cc += emailtosend.cc && emailtosend.cc != "" ? "," : "";
|
|
|
|
|
emailtosend.cc += email.cc.join(",");
|
|
|
|
|