Four event verbs and the announce leg, per PLAN.md §29: - rust.participation.open / .collect: the plugin counts who takes part (seconds, kills or both, in a zone this run opened or the whole server) and collect files them as the run's participants, keyed by Steam id. - rust.kit.entitle: the five recipient modes (D101), rows in the new rust_perm_run_grants (D84) unioned into the permission push, one extra use of the kit per reward as site-held credits on perm.sync (D103), and the rust.kit.entitled notice deferred from phase 10 (D64). - rust.announce: one server or every server (D105). - rust.chat announce leg, speaking only on servers whose new news switch is on (D104) - a card on Admin -> Rust visibility (D106). Budgets rust.grants and rust.announcements; the kit source and four fixed-choice sources (core has no enum param type). rust_perm_run_grants carries core's idempotency key so a revert of a lost answer can find its rows. Protocol 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
// ── 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 }
|