// ── module-uo's registered audiences ─────────────────────────────────────── // // ENGAGEMENT.md §5.1a, and this module's first three. An audience is a NAMED SET // OF PEOPLE an operator can point a rule at, or compose into a saved segment with // and/or/not — "the members of guild 1042", "the governors", "everyone who has // linked a game account". // // **This is a different mechanism from the `members` audience the guild triggers // use, and the difference is worth stating because the words are the same.** A // guild event is about the members of THAT guild, which is a different answer for // every firing; a segment's parameters are CONSTANTS, so it cannot express it, // and the access-checked set travels on the envelope as `recipientUserIds` // instead (Phase 6, decision 2). What is here answers the same question every // time it is asked, which is exactly what makes it composable and storable. // // **Four rules, all of them from §5.1a:** // // 1. **Core learns no game vocabulary.** It knows an id, a label, a parameter // list and a `resolve` it may call. It has never heard of a guild. // 2. **The resolver returns user ids and NOTHING else.** It is not handed a // template, a channel or an address and cannot enumerate them. A module still // cannot send mail, and this must not become the door that lets it — core // maps ids to addresses on its own side, after preferences, suppression and // the verification gate. // 3. **Composition narrows, never widens.** The `ceiling` below is the widest // this audience can EVER resolve to; a segment takes the narrowest ceiling it // contains, and the result is still checked against the trigger's own. // 4. **An uninstalled module's audience goes dormant**, resolving empty, rather // than erroring or silently reaching a different set of people. // // All three ceiling at `members`, and none higher. `members` is the lattice value // for "a module-declared list", and it is the honest one here: these sets are not // "everyone signed in" narrowed down, they are lists this module happens to know. // // Every resolver is bounded by `shardLinks.MAX_AUDIENCE` through the queries it // calls, and every one of them fails to the EMPTY set rather than throwing — a // dormant audience is a rule that reaches nobody, which is §5.1a rule 4's // behaviour and much better than a rule that 500s the engine. const shardLinks = require('../model/shardLinks/shardLinks.model') const shardState = require('../model/shardState/shardState.model') const core = require('../core') const log = core.logger('shard-audiences') // One wrapper, so every resolver has the same failure behaviour and none of them // has to remember it. A resolver that throws would fail the whole enqueue for // every other audience in the same segment. const safely = (id, fn) => async (params) => { try { return await fn(params || {}) } catch (err) { log.warn('audience resolve failed — treating as empty', { audience: id, message: err.message }) return [] } } const AUDIENCES = [ { // `namespaced()` requires the module's own prefix, so these are declared with // it rather than relying on core to add one. Audiences have their own id // space — an audience names a set of PEOPLE and a trigger names an EVENT — so // `uo.guild.members` here does not collide with any trigger id. id: 'uo.guild.members', label: 'Members of a guild', description: 'Everyone with a linked game account on one guild\'s roster.', params: [{ id: 'guildId', type: 'int', required: true }], ceiling: 'members', resolve: safely('uo.guild.members', async ({ guildId }) => { if (guildId == null) return [] const accounts = await shardState.listGuildMemberAccounts(guildId) return shardLinks.userIdsForAccounts(accounts) }), }, { id: 'uo.governors', label: 'Town governors', description: 'Everyone with a linked game account currently holding a city governorship.', params: [], ceiling: 'members', resolve: safely('uo.governors', async () => { const accounts = await shardState.listGovernorAccounts() return shardLinks.userIdsForAccounts(accounts) }), }, { id: 'uo.linked.accounts', label: 'Players with a linked game account', // The set an operator reaches for first, and — more usefully — the one a // `not` composes against: "everyone who has NOT linked" is the audience for // the message that asks them to. description: 'Every website user who has linked at least one game account.', params: [], ceiling: 'members', resolve: safely('uo.linked.accounts', () => shardLinks.allLinkedUserIds()), }, ] module.exports = { AUDIENCES }