// ── What the notifications read like, and the rules that use them ───────── // // `registerEngagementSeeds` (MODULE_API.md §2.4 and §1.1 under 1.9.0; PLAN.md // §25.2). Data only: nothing here names a recipient, and nothing here turns a // rule on. // // ── Two bespoke bodies, and why only two ────────────────────────────────── // // A body earns its place when the message has something to say that core's // structural projection cannot. The raid alert does — it is the one message // here somebody acts on at 3am, and it must say WHERE and WHAT in the first // line. The wipe does — it is the one broadcast a whole community waits for. // Everything else is "this happened, here is the link", which is exactly what // core's `notify.event` / `inapp.event` already say, so it points at those and // authors nothing (§4.6.1 property 1). // // The register is plain, not in-universe. Rust has no court or herald to write // in the voice of, and a raid alert dressed as fiction is a raid alert read a // second later than it should be. // // ── Three rules for editing a body ──────────────────────────────────────── // // 1. **No conditionals, and never an optional inside a clause.** An unset // optional interpolates to the EMPTY STRING. `atGrid` is a fragment that // carries its own leading space for exactly that reason; `grid` on its own // belongs on a line of its own or nowhere. // 2. **No brand.** `siteName` and friends are supplied by the renderer, so one // image mails as whichever site it is running as. // 3. **Bump `seedVersion` when a body changes, never for a comment.** It is how // a better default reaches deployments whose operators did not edit it. // // ── One rule group per family ───────────────────────────────────────────── // // A group is seeded ONCE (per deployment, per key), so a rule appended to a // group in a later version reaches fresh installs only. Eight families, eight // keys: a future raid rule takes `raid-v2` without disturbing anybody's clan // rules. Every rule is disabled — core ignores `enabled` rather than trusting it // — so installing this module mails nobody until an operator decides it should. // ── Block helpers ────────────────────────────────────────────────────────── const text = (id, body, opts = {}) => ({ id, type: 'email.text', props: opts.muted ? { text: body, muted: true } : { text: body }, }) const heading = (id, body, level = 'h1') => ({ id, type: 'email.heading', props: { level, text: body } }) const button = (id, label, url, textLead) => ({ id, type: 'email.button', props: textLead ? { label, url, textLead } : { label, url }, }) const divider = (id) => ({ id, type: 'email.divider', props: {} }) // Every email ends with the unsubscribe pair; `unsubscribeUrl` is core's // per-delivery variable, not something a trigger declares. const unsubscribe = () => [ divider('rule'), button('unsub', 'Unsubscribe', '{{unsubscribeUrl}}', 'To stop these messages, use this link:'), ] const email = (key, name, triggerId, subject, blocks) => ({ key, name, channel: 'email', triggerId, triggerVersion: 1, seedVersion: 1, subject, blocks: [...blocks, ...unsubscribe()], }) /** In-app: heading = the row's title, button = its one action, the rest = its body. */ const inapp = (key, name, triggerId, title, body, action, url) => ({ key, name, channel: 'inapp', triggerId, triggerVersion: 1, seedVersion: 1, subject: null, blocks: [heading('h', title, 'h3'), text('intro', body), button('cta', action, url)], }) const TEMPLATES = Object.freeze([ email( 'rust.base.destroyed', 'Rust — your base was raided', 'rust.base.destroyed', 'Your base on {{server}} is being raided', [ heading('h', 'Your base is being raided'), text('p1', 'A {{structure}} of a base you are authorised on was destroyed{{atGrid}} on {{server}}.'), text('p2', 'You are getting this because you are on the base\'s tool cupboard. Further damage to the ' + 'same base will not send another alert for a while.', { muted: true }), button('cta', 'Open the server page', '{{serverUrl}}'), ], ), inapp( 'rust.base.destroyed-inapp', 'Rust — your base was raided (in-app)', 'rust.base.destroyed', 'Your base is being raided', 'A {{structure}} was destroyed{{atGrid}} on {{server}}.', 'Open the server', '{{serverUrl}}', ), email( 'rust.wipe.started', 'Rust — a server wiped', 'rust.wipe.started', '{{server}} has wiped', [ heading('h', '{{server}} has wiped'), text('p1', 'A new wipe has started on {{server}}: a fresh map, and a fresh start for everyone.'), button('cta', 'Open the server page', '{{serverUrl}}'), ], ), inapp( 'rust.wipe.started-inapp', 'Rust — a server wiped (in-app)', 'rust.wipe.started', '{{server}} has wiped', 'A new wipe has started: a fresh map, and a fresh start for everyone.', 'Open the server', '{{serverUrl}}', ), ]) // ── The rules — every one of them off ────────────────────────────────────── /** Core's generic bodies (§4.6.1 property 1). */ const GENERIC = { email: 'notify.event', inapp: 'inapp.event', digest: 'notify.digest' } /** This module's bodies for a trigger, and core's digest. */ const bodies = (key) => ({ email: key, inapp: `${key}-inapp`, digest: 'notify.digest' }) const RULE_GROUPS = Object.freeze([ { key: 'raid-v1', note: 'module-rust: the raid alert (disabled)', rules: [ { trigger_id: 'rust.base.destroyed', name: 'Raid alert — offline owners', audience: 'owner', // Push is allowed because this trigger is also a stream (D65); the // tickle carries no content, and the app pulls the inbox row. channels: ['email', 'inapp', 'push'], template_keys: bodies('rust.base.destroyed'), // Per BUILDING (the subjectKey): a raid is dozens of walls and one alert. cooldown_seconds: 1800, max_sends_per_hour: 500, // D61: "offline raid alert" is this condition, not code. An operator who // wants online raids too deletes it. conditions: { variable: 'ownerOnline', cmp: 'eq', value: false }, }, ], }, { key: 'wipe-v1', note: 'module-rust: wipe announcements (disabled)', rules: [ { trigger_id: 'rust.wipe.started', name: 'Server wiped', audience: 'subscribers', channels: ['email', 'inapp', 'push'], template_keys: bodies('rust.wipe.started'), cooldown_seconds: 6 * 3600, max_sends_per_hour: 2000, }, ], }, { key: 'server-v1', note: 'module-rust: server up and down (disabled)', rules: [ { trigger_id: 'rust.server.online', name: 'Server came online', audience: 'subscribers', channels: ['inapp', 'push'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 3600, max_sends_per_hour: 2000, }, { trigger_id: 'rust.server.offline', name: 'Server went offline', audience: 'subscribers', channels: ['inapp', 'push'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 3600, max_sends_per_hour: 2000, // A plugin reload, or a restart that is back within five minutes, is not // an outage anybody needs to hear about. `cancel_on` withdraws the // pending notice when the server comes back inside the window. delay_seconds: 300, cancel_on: ['rust.server.online'], }, ], }, { key: 'leaderboard-v1', note: 'module-rust: a new kills leader (disabled)', rules: [ { trigger_id: 'rust.leaderboard.topped', name: 'New kills leader', audience: 'subscribers', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 3600, max_sends_per_hour: 2000, }, ], }, { key: 'account-v1', note: 'module-rust: a Steam account was linked (disabled)', rules: [ { trigger_id: 'rust.player.linked', name: 'Steam account linked', audience: 'owner', // Email as well as in-app: the case this exists for is a link the person // did NOT make, and they will not be looking at the site's inbox for it. channels: ['email', 'inapp'], template_keys: GENERIC, cooldown_seconds: 0, max_sends_per_hour: 200, }, ], }, { // Its own group, not a rule appended to `account-v1`: a group is seeded once, // so an appended rule would reach fresh installs only (R7). key: 'rewards-v1', note: 'module-rust: an event rewarded you a kit (disabled)', rules: [ { trigger_id: 'rust.kit.entitled', name: 'Kit reward earned', audience: 'owner', // Email as well: a reward granted at 03:00 is news the person reads the // next morning, before they are next in game or on the site. channels: ['email', 'inapp'], template_keys: GENERIC, cooldown_seconds: 0, max_sends_per_hour: 200, }, ], }, { key: 'clans-v1', note: 'module-rust: clan departures and disbands (disabled)', rules: [ { trigger_id: 'rust.clan.member.left', name: 'Clan — a member left', audience: 'members', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 0, max_sends_per_hour: 500, }, { trigger_id: 'rust.clan.member.kicked', name: 'Clan — a member was removed', audience: 'members', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 0, max_sends_per_hour: 500, }, { trigger_id: 'rust.clan.disbanded', name: 'Clan — disbanded', audience: 'members', channels: ['email', 'inapp'], template_keys: GENERIC, cooldown_seconds: 0, max_sends_per_hour: 500, }, ], }, { key: 'moderation-v1', note: 'module-rust: reports, bans and unapproved logins, to staff (disabled)', rules: [ { trigger_id: 'rust.player.reported', name: 'Player reported', audience: 'staff', channels: ['email', 'inapp'], template_keys: GENERIC, // Per REPORTED player: a pile-on of ten reports is one notice an hour. cooldown_seconds: 3600, max_sends_per_hour: 200, }, { trigger_id: 'rust.player.banned', name: 'Player banned', audience: 'staff', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 0, max_sends_per_hour: 200, }, { trigger_id: 'rust.player.unbanned', name: 'Player unbanned', audience: 'staff', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 0, max_sends_per_hour: 200, }, { trigger_id: 'rust.login.denied', name: 'Login not approved', audience: 'staff', channels: ['inapp'], template_keys: { inapp: GENERIC.inapp }, cooldown_seconds: 3600, max_sends_per_hour: 200, }, ], }, ]) module.exports = { TEMPLATES, RULE_GROUPS }