|
|
|
|
@@ -147,9 +147,9 @@ statement of your dependencies, and it makes a test double for it — see
|
|
|
|
|
|
|
|
|
|
## What you register
|
|
|
|
|
|
|
|
|
|
Twenty calls, all synchronous and all listed in [§2.4][api]: fifteen
|
|
|
|
|
registrations, the two lifecycle hooks, the two identity-link hooks, and the demo's
|
|
|
|
|
seed hook. Five of the
|
|
|
|
|
Twenty-three calls, all synchronous and all listed in [§2.4][api]: seventeen
|
|
|
|
|
registrations, the two lifecycle hooks, the two identity-link hooks, the demo's
|
|
|
|
|
seed hook, and the account-deletion hook. Five of the
|
|
|
|
|
registrations are the event system's, and [chapter 5](05-events.md) has those. What
|
|
|
|
|
is worth knowing is not their signatures but the model behind them.
|
|
|
|
|
|
|
|
|
|
@@ -538,6 +538,79 @@ api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {})
|
|
|
|
|
module made from it is yours to keep or remove.
|
|
|
|
|
- Neither hook can mint a session or change who is signed in.
|
|
|
|
|
|
|
|
|
|
### When a member deletes their account
|
|
|
|
|
|
|
|
|
|
A member can delete their own site account, and staff can delete one from the
|
|
|
|
|
admin panel (`MODULE_API_VERSION` 1.20.0). Core removes the row, and every table
|
|
|
|
|
with a foreign key to it follows: `ON DELETE CASCADE` for what was theirs alone,
|
|
|
|
|
`SET NULL` for what they made that others still read. Your module hears about it
|
|
|
|
|
once, first:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
api.onUserDeleting(async ({ userId, deletedBy }) => {}) // deletedBy: 'self' | 'staff'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **It runs before the row goes**, so you can still read what the person holds.
|
|
|
|
|
- **A cascade is not an unlink.** If your game holds anything for the person, such
|
|
|
|
|
as a link it reports back on every login or a grant it enforces, undo it here
|
|
|
|
|
through your module's own unlink, the same call a player's own unlink makes. That
|
|
|
|
|
way the game sees exactly what it would see if they had unlinked by hand. Clean up
|
|
|
|
|
any data you keep without a foreign key here too.
|
|
|
|
|
- **It cannot stop the deletion.** A throw is logged, and so is no answer within
|
|
|
|
|
10 s, and the account is deleted anyway, because a person who asks to be deleted
|
|
|
|
|
must be. So a module that talks to a game needs **a record of what is left and a
|
|
|
|
|
retry**: write down what you are about to undo *before* the first call, keep what
|
|
|
|
|
the game could not take (it was down, or it refused), and try again when the game
|
|
|
|
|
comes back. That record cannot have a foreign key to `users`, because it outlives
|
|
|
|
|
the user it names.
|
|
|
|
|
- **Your hook keeps running past the 10 s.** Core stops waiting, not your code, so
|
|
|
|
|
the user may already be gone when your last call finishes. Anything you write
|
|
|
|
|
afterwards must not need the row: an activity entry, for instance, names the id in
|
|
|
|
|
its `detail`, not its `userId`.
|
|
|
|
|
- **Every module that holds per-person game state should register it.** Declaring it
|
|
|
|
|
needs `coreApi` `^1.20.0`, because an older core has no `api.onUserDeleting`, and
|
|
|
|
|
calling a missing function throws during `register()`.
|
|
|
|
|
|
|
|
|
|
### When members report, hide or block what your game shows
|
|
|
|
|
|
|
|
|
|
If your module shows what players wrote, such as chat from the game, members can
|
|
|
|
|
report it, hide it for themselves, and block whoever wrote it, and staff can hide
|
|
|
|
|
it for everyone (`MODULE_API_VERSION` 1.21.0, [`USER_SAFETY.md`][safety]). Core
|
|
|
|
|
keeps the Reports queue and each member's blocks and hides. You keep the content,
|
|
|
|
|
and you tell core three things about it:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
api.registerReportTargets([{ id: 'mygame.chat', label: 'Chat line', snapshot, hide, unhide }])
|
|
|
|
|
api.registerGameIdentities([{ kind: 'steam', label: 'Steam account', ownersOf }])
|
|
|
|
|
// and, from your own per-reader routes: ctx.safety.forViewer(userId), ctx.safety.staffAmong(userIds)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **`snapshot({ targetId, viewer })` answers for that reader, and `null` covers two
|
|
|
|
|
cases.** An item that is gone and an item they could not have seen get the same
|
|
|
|
|
`null`, so core gives them the same 404. Otherwise a report or a block would tell
|
|
|
|
|
anybody who said line 4410 on a server whose player list is hidden from them. Run
|
|
|
|
|
the same visibility check your page runs.
|
|
|
|
|
- **Name the author by their site account when they have one.** `author.userId` is
|
|
|
|
|
the linked account and `author.identity` is the game id. `ownersOf(ids)` answers
|
|
|
|
|
which ids are linked. A block then lands on the account, and only an unlinked id
|
|
|
|
|
is blocked as itself. Core rewrites that block as an account block once the id
|
|
|
|
|
links, so you never have to.
|
|
|
|
|
- **Filter on the server, in your query.** A route that shows the content to
|
|
|
|
|
signed-in readers calls `ctx.safety.forViewer(userId)` and leaves out what that
|
|
|
|
|
reader blocked or hid. Do it in the `WHERE` clause, so a page of 100 is still 100
|
|
|
|
|
long, and keep the route per-viewer (`Cache-Control: private`), or a shared cache
|
|
|
|
|
hands one reader's filter to the next. If your page offers *Block*, mark each item
|
|
|
|
|
with whether it can be blocked. Use `ctx.safety.staffAmong` for that, because
|
|
|
|
|
staff cannot be blocked.
|
|
|
|
|
- **Hide for everyone is a flag you keep.** `hide` is called from core's queue.
|
|
|
|
|
Leave the item out for everyone but staff, and show it to staff marked, with an
|
|
|
|
|
undo where it lives. Core never calls `unhide` itself.
|
|
|
|
|
- **Each call has 5 s.** A module that does not answer is a 503 to the member,
|
|
|
|
|
never a silent success. Declaring any of it needs `coreApi` `^1.21.0`.
|
|
|
|
|
|
|
|
|
|
Module-Rust's `server/safety.js` is a complete worked example: chat lines, Steam
|
|
|
|
|
ids and the feed's own filter.
|
|
|
|
|
|
|
|
|
|
### On a public demo site
|
|
|
|
|
|
|
|
|
|
A site can run as a public demo (`MODULE_API_VERSION` 1.18.0). Anybody signs in with
|
|
|
|
|
@@ -563,6 +636,11 @@ members are for that. With the flag off none of them changes anything, except th
|
|
|
|
|
syntax with an optional trailing `/*`, and under one of your declared `mounts`.
|
|
|
|
|
**It is validated on every boot, demo or not**, so a wrong entry fails on your
|
|
|
|
|
machine rather than only on the demo.
|
|
|
|
|
- **Two refusals reach your routes without being listed.** On a demo, core refuses
|
|
|
|
|
any request with a multipart body, and any write whose JSON body carries a
|
|
|
|
|
`data:` URI in any string (a pasted image is an upload by another name), with the
|
|
|
|
|
same `403` body. An upload route of yours is locked on a demo whether or not
|
|
|
|
|
`demoLocks` names it.
|
|
|
|
|
- **`ctx.demoMode` is for a lock a route list cannot express**, one that depends on
|
|
|
|
|
what is in the request rather than which URL it went to. The Rust module refuses a
|
|
|
|
|
console command by its first word, and a plugin's config file by the plugin that
|
|
|
|
|
@@ -901,3 +979,4 @@ the next chapter is for.
|
|
|
|
|
|
|
|
|
|
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
|
|
|
|
|
[acceptance]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/kit-acceptance.md
|
|
|
|
|
[safety]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/USER_SAFETY.md
|
|
|
|
|
|