@rackbops/plugin-tracker 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -24
- package/dist/plugin.js +1443 -592
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ as in a server (plan 5.5, item 39).
|
|
|
33
33
|
| `/settings hour hour` | registered | The preferred hour; recurring reminders with no time of their own move to it. |
|
|
34
34
|
| `/web` | registered | A one-time link to sign in to the web area (below), good for 10 minutes and one use. Says so when `TRACKER_WEB_URL` is unset. |
|
|
35
35
|
|
|
36
|
-
**Gates.** Every command and button checks, in order: membership of
|
|
37
|
-
server (when set), admission (in the tracker's store), then
|
|
38
|
-
`/allow`, admission for `/register`, registration for the
|
|
39
|
-
to ask an admin to `/allow` them. Decline and opt-out skip the membership and registration gates,
|
|
36
|
+
**Gates.** Every command and button checks, in order: membership of a `TRACKER_GUILD_ID`
|
|
37
|
+
server (when set; a member of any listed server passes), admission (in the tracker's store), then
|
|
38
|
+
what the action needs (an admin for `/allow`, admission for `/register`, registration for the
|
|
39
|
+
rest). A person nobody admitted is told to ask an admin to `/allow` them. Decline and opt-out skip the membership and registration gates,
|
|
40
40
|
so anyone can always stop the messages; the Reply button checks membership when its modal is sent.
|
|
41
41
|
|
|
42
42
|
**Buttons.** The owner's DM carries Done, Snooze 1h and Reply; a recipient's copy carries the
|
|
@@ -46,11 +46,27 @@ A pressed DM is edited to say what happened.
|
|
|
46
46
|
|
|
47
47
|
**Pause after failed DMs** (plan 5.5). Three DMs in a row that the host says cannot be delivered
|
|
48
48
|
pause the person's delivery and every active task that would DM them, theirs and the ones they
|
|
49
|
-
receive (a `paused` event in each task's history);
|
|
50
|
-
|
|
49
|
+
receive (a `paused` event in each task's history); a DM to them still owed is deferred, not failed.
|
|
50
|
+
The owner of a task paused for a recipient is DMed once and sees why in `/tasks`; the task
|
|
51
51
|
resumes when the recipient next uses the tracker, or `/task resume` goes on without them. A person
|
|
52
52
|
paused for their own DMs is told, and resumed, the next time they use a command or button. A DM
|
|
53
|
-
that goes through clears the count.
|
|
53
|
+
that goes through clears the count. Only a DM the host says cannot reach that person (Discord's
|
|
54
|
+
50007, or 10013 for an account that is gone) counts, once per run: docket fails it for good at once. A DM refused for
|
|
55
|
+
its content is retried by docket (three tries, a minute apart and doubling) and does not count; nor
|
|
56
|
+
does one whose outcome is unknown (see Delivery below).
|
|
57
|
+
|
|
58
|
+
**Delivery** (docket 0.4.0). docket records each DM in the store's `deliveries` table before it is
|
|
59
|
+
sent (a claim) and settles it after: sent, failed (for good, or owed and retried later), or
|
|
60
|
+
unconfirmed -- the send may or may not have reached Discord, so it is never resent, and a warning is
|
|
61
|
+
logged. A run has fired once its record is written; the other recipients of a run still get their
|
|
62
|
+
copies when one of them cannot be reached, and the run is `done`, not `failed`, whatever a DM did.
|
|
63
|
+
At start the plugin runs docket's `recover()`: a run left running is requeued, and a claim left open
|
|
64
|
+
by a stop mid-send is settled unconfirmed and logged.
|
|
65
|
+
|
|
66
|
+
**One task at a time.** A task's runs, the answers to them (buttons, `/task done`, Reply) and its
|
|
67
|
+
edits (the editor, pause, resume, delete, a zone or hour change) take that task's lock
|
|
68
|
+
(`src/locks.ts`), so none of them overlap. The notify and poll ticks run one pass per task that has
|
|
69
|
+
work; an answer that lands while its own task runs waits for that pass.
|
|
54
70
|
|
|
55
71
|
## Web area
|
|
56
72
|
|
|
@@ -131,10 +147,10 @@ other than GET or POST (or the wrong one of the two for a path) is 405.
|
|
|
131
147
|
|
|
132
148
|
**Membership on the web.** With `TRACKER_GUILD_ID` set, `/web` issues a link only to a member,
|
|
133
149
|
and the session remembers when that was confirmed. A web request more than 15 minutes after the
|
|
134
|
-
last confirmation re-checks
|
|
135
|
-
queue), through the discord.js Client of an interaction the
|
|
136
|
-
(the host API has no member lookup of its own; the Client is
|
|
137
|
-
Not a member: every session of theirs ends, on a page that says why. A member: the time is
|
|
150
|
+
last confirmation re-checks membership (one lookup per listed server, all at once, the first yes enough; about 3
|
|
151
|
+
seconds at most, outside the write queue), through the discord.js Client of an interaction the
|
|
152
|
+
plugin has handled since it started (the host API has no member lookup of its own; the Client is
|
|
153
|
+
held in memory, never stored). Not a member of any listed server: every session of theirs ends, on a page that says why. A member: the time is
|
|
138
154
|
refreshed. A failed or slow lookup, or no interaction yet since a restart: they stay signed in
|
|
139
155
|
while the last confirmation is under 24 hours old, and are then signed out and told to run `/web`
|
|
140
156
|
again. Concurrent requests share one lookup per person, and after a failed one that person is not
|
|
@@ -179,10 +195,11 @@ area). Two posts: the first asks, the second must carry `confirm=yes` and the wo
|
|
|
179
195
|
in one SQLite transaction in the write queue, **deleted, not archived**:
|
|
180
196
|
|
|
181
197
|
- every task they own, archived ones too, with everything under it: runs, run events, replies
|
|
182
|
-
(anyone's), history, series, recipients,
|
|
198
|
+
(anyone's), history, series, recipients, deliveries (anyone's), charges and pauses;
|
|
183
199
|
- on everyone else's tasks: their recipient rows, their replies and answers, the history rows they
|
|
184
|
-
made or that name them (an invitation of them, a pause for them), the run events and
|
|
185
|
-
|
|
200
|
+
made or that name them (an invitation of them, a pause for them), the run events and deliveries
|
|
201
|
+
of DMs to them (docket's `deleteDeliveries`), charges made for them, and their pause rows;
|
|
202
|
+
- the budget notices kept for them (`budget:person:<id>:...`);
|
|
186
203
|
- every decline block they are either side of;
|
|
187
204
|
- their admission, delivery health, web sessions, unused sign-in links and API tokens, and their
|
|
188
205
|
person row.
|
|
@@ -193,7 +210,8 @@ id -- `admitted_by` of someone they admitted, and `lifted_by` of a block they li
|
|
|
193
210
|
`forgotten` ("an admin since forgotten"; null already means "the configuration"). Text is matched
|
|
194
211
|
only in the forms the code writes a person's id into -- docket's consent rows (`u5`, `u5 24h`), the
|
|
195
212
|
tracker's `u5: ...` removals and `delivery to {u5} paused` pauses, docket's `u5 <message>` delivery
|
|
196
|
-
events, and the notifier's error phrases (`recipient u5 cannot be messaged`,
|
|
213
|
+
events, and the notifier's error phrases (`recipient u5 cannot be messaged`, ...; both written only
|
|
214
|
+
before 0.9.0, when a run's own row recorded its DMs) -- never as any
|
|
197
215
|
id-like word, so an address with `/u5/` in someone else's text is left alone. A history row or a
|
|
198
216
|
delivery event of theirs is deleted; the error of someone else's run keeps its row and has the id
|
|
199
217
|
replaced with `(forgotten)`. A pause for a recipient now names them by id and shows their current
|
|
@@ -256,9 +274,9 @@ API could be called by any script on it. A token acts as its owner with the owne
|
|
|
256
274
|
and only on the owner's own tasks -- an admin's token included; an admin's wider reads stay on the
|
|
257
275
|
signed-in web pages. Every request re-reads the owner, as the web does a session: a person no
|
|
258
276
|
longer on the tracker, or no longer registered, is refused and their tokens deleted; with `TRACKER_GUILD_ID` set, membership
|
|
259
|
-
is re-checked on the web's schedule (after 15 minutes, one
|
|
260
|
-
has left
|
|
261
|
-
for 24 hours from the last confirmation, then answers 503 until one succeeds (the token is kept).
|
|
277
|
+
is re-checked on the web's schedule (after 15 minutes, one check, shared with the web), and one who
|
|
278
|
+
has left every listed server loses every token and every session; a lookup that keeps failing
|
|
279
|
+
lets them on for 24 hours from the last confirmation, then answers 503 until one succeeds (the token is kept).
|
|
262
280
|
A token is looked up by the SHA-256 of what was sent, so the comparison is over a hash the sender
|
|
263
281
|
cannot steer.
|
|
264
282
|
|
|
@@ -321,7 +339,7 @@ and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
|
321
339
|
| 401 | `unauthorized` | No `Authorization` header. With `WWW-Authenticate: Bearer realm="tracker"`. |
|
|
322
340
|
| 401 | `invalid_token` | A token unknown, revoked or expired, or whose owner is no longer registered (their tokens are then deleted): one answer for all. |
|
|
323
341
|
| 403 | `origin_refused` | The request carried an `Origin` header. |
|
|
324
|
-
| 403 | `not_member` | The owner has left
|
|
342
|
+
| 403 | `not_member` | The owner has left every `TRACKER_GUILD_ID` server; their tokens are revoked. |
|
|
325
343
|
| 404 | `not_found` | No such endpoint, or no such task of yours. Anyone else's task -- one shared with you, or any task to an admin's token -- answers exactly as an unknown id. |
|
|
326
344
|
| 405 | `method_not_allowed` | With `Allow`. |
|
|
327
345
|
| 409 | `conflict` | A finished task edited, or a pause of a task not active (a resume of one not paused). |
|
|
@@ -359,11 +377,12 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
359
377
|
|
|
360
378
|
| Piece | File | Notes |
|
|
361
379
|
|---|---|---|
|
|
362
|
-
| Store | `src/store.ts`, `src/schema.ts` | docket's `Store` port on `bun:sqlite`, in `<dataDir>/tracker/tracker.sqlite` (WAL). A Discord id belongs to at most one user. Schema versioned by `PRAGMA user_version`; a shipped migration is never edited. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
363
|
-
| Notify lane | `src/notify-lane.ts` | docket's `Lanes.tickNotify` on two host ticks, every 60 s,
|
|
380
|
+
| Store | `src/store.ts`, `src/schema.ts` | docket's `Store` port on `bun:sqlite`, in `<dataDir>/tracker/tracker.sqlite` (WAL). A Discord id belongs to at most one user. Schema versioned by `PRAGMA user_version`; a shipped migration is never edited. Schema 5 (0.9.0) is docket 0.4.0's: a run's `record`, a series point's `key`, `deliveries` (from `delivery_claims`), `usage` and `notices`. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
381
|
+
| Notify lane | `src/notify-lane.ts` | docket's `Lanes.tickNotify` on two host ticks, every 60 s, one pass per task with work (a due run, a run in flight, a DM owed or claimed), each under that task's lock through a view of the store limited to that task: `notify` runs every type but the page readers, `poll` runs only them (`price`) with the Fetch port, so a slow page never holds up a reminder. Registers `reminder`, `renewal` and `price`. The execute lane is not ticked. |
|
|
364
382
|
| Page reads | `src/fetch.ts` | docket's `Fetch` port for `price`: http or https on the default port, no credentials, every resolved address public (no loopback, private, link-local, CGNAT, multicast or reserved range, IPv4 or IPv6), redirects followed by hand and re-checked (at most 5), 15 s including the name lookup, at most 3 MB kept. The body is then rebuilt in linear time (`src/page.ts`) to just what extraction reads -- JSON-LD, meta tags, and the page with every `<` blanked, so `near` still reads text, attributes and script data -- because docket's extraction patterns take quadratic time on a page of unclosed tags. A read the tick's abort cuts short requeues its run instead of counting a miss. A DNS answer that changes between the check and the read is not caught here. |
|
|
365
383
|
| Renewals, prices | `src/tracked.ts`, `src/price.ts`, `src/series.ts`, `src/page.ts` | `/renewal`, `/price` and `/task decide`; the series lines of `/task history`. The series (docket's `series` table, schema 1) holds a renewal's paid amounts and a price's readings. |
|
|
366
|
-
| Delivery | `src/notifier.ts`, `src/
|
|
384
|
+
| Delivery | `src/notifier.ts`, `src/buttons.ts` | docket's `Notifier` over `host.dm`, with the buttons. docket claims each (run, person) in the store's `deliveries` before the DM and settles it after. The notifier maps the host's answers to docket's errors: cannot be messaged (50007) or an unknown user is `DeliveryFailedError(msg, true)` (failed for good; 50007 and 10013 count toward the pause); a message refused for its content is a plain `DeliveryFailedError` (retried); a paused person is `ExecutorUnavailableError` (deferred); anything else is rethrown, so docket settles it unconfirmed and never resends it. |
|
|
385
|
+
| Task lock | `src/locks.ts` | One task at a time: its runs, answers and edits never overlap (docket 0.4.0 asks the host to serialize per task). |
|
|
367
386
|
| Pause | `src/delivery-health.ts` | The per-person failure count, the pause and the resume. |
|
|
368
387
|
| People | `src/people.ts`, `src/admissions.ts` | Discord id, time zone, preferred hour, admin flag -- in the tracker's store, never usr; who admitted each person and when they registered. `TRACKER_ADMIN_DISCORD_IDS` only ever grants admin. |
|
|
369
388
|
| Commands | `src/discord.ts`, `src/interactions.ts`, `src/discord-common.ts` (discord.js); `src/actions.ts`, `src/press.ts`, `src/history.ts`, `src/access.ts` | The discord.js files read options and render; the rest is Discord-free over the injected store, clock and notifier. Store writes are handled one at a time; Discord lookups and DMs run outside that queue. |
|
|
@@ -378,7 +397,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
378
397
|
| Env key | Secret | Meaning |
|
|
379
398
|
|---|---|---|
|
|
380
399
|
| `TRACKER_ADMIN_DISCORD_IDS` | no | Comma-separated Discord user ids (spaces around commas allowed; an empty entry, as from a trailing comma, is refused and the plugin does not load) made admin at start. Unset = none. Removing an id does not revoke it. |
|
|
381
|
-
| `TRACKER_GUILD_ID` | no | The Discord server whose members may use the tracker. Checked through the interaction's client with a single-member lookup (no privileged intent). Unset = no membership gate, and a warning is logged each time the plugin activates; a malformed
|
|
400
|
+
| `TRACKER_GUILD_ID` | no | The Discord server whose members may use the tracker, or a comma-separated list of them (spaces around commas allowed; a repeated id counts once): a member of any listed server passes. Checked through the interaction's client with a single-member lookup per server (no privileged intent), all asked at once; a yes from any server is a member, a no from every server is not, and otherwise the answer is unknown (refused, never revoked). Inside a listed server, the person running a command needs no lookup. Unset = no membership gate, and a warning is logged each time the plugin activates; a malformed or empty entry anywhere in the list refuses to load, naming it. One store and one admission list serve every listed server; there are no per-server admins. |
|
|
382
401
|
| `TRACKER_WEB_URL` | no | The https origin the bot's HTTP is reached at through its tunnel, e.g. `https://clerk.example.com` (no path). `/web` links and the allowed `Origin` come from it, never from a request's `Host`. Unset = no web area (`/web` says so, the pages answer 404); anything but a bare https origin refuses to load. |
|
|
383
402
|
|
|
384
403
|
`/tracker/healthz` and the web area need the bot's `HTTP_PORT` set; without it there is no HTTP at all.
|
|
@@ -392,7 +411,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
392
411
|
method, if chosen (plan item 41).
|
|
393
412
|
- The city-hall Executor adapter (the execute lane).
|
|
394
413
|
- Editing in Discord (the editor is on the web only), and changing a price tracker's page or `near` after it is made (make a new one); a free-form pattern for `price` (`near` is the safe subset: a user's regular expression run on a large page could hang the bot).
|
|
395
|
-
- An optional Discord-role gate (plan 5.5), and showing unconfirmed deliveries to admins anywhere but the log.
|
|
414
|
+
- An optional Discord-role gate (plan 5.5), and showing unconfirmed deliveries to admins anywhere but the log, or a run's deliveries in its history.
|
|
396
415
|
|
|
397
416
|
docket-core, docket-types and `@rackbops/styles` are `devDependencies`: `bun build` bundles them
|
|
398
417
|
into `dist/plugin.js` (the theme's CSS as text), and the bot loads that file without installing
|