aurival 0.6.0 → 0.8.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 +202 -0
- package/dist/bot.d.ts +76 -4
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +183 -9
- package/dist/bot.js.map +1 -1
- package/dist/caps.d.ts +43 -0
- package/dist/caps.d.ts.map +1 -1
- package/dist/caps.js +45 -0
- package/dist/caps.js.map +1 -1
- package/dist/cooldown.d.ts +164 -0
- package/dist/cooldown.d.ts.map +1 -0
- package/dist/cooldown.js +221 -0
- package/dist/cooldown.js.map +1 -0
- package/dist/embeds.d.ts +38 -0
- package/dist/embeds.d.ts.map +1 -1
- package/dist/embeds.js +59 -5
- package/dist/embeds.js.map +1 -1
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +69 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +153 -20
- package/dist/events.js.map +1 -1
- package/dist/http.d.ts +34 -5
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +41 -4
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -435,6 +435,151 @@ is single-use, and acking one a second time doesn't throw locally, the server 40
|
|
|
435
435
|
The full set of showcase examples (trivia, giveaways, DJ bots, moderation reports…) is in the
|
|
436
436
|
[cookbook](https://bots.aurival.com/docs/cookbook).
|
|
437
437
|
|
|
438
|
+
## Buttons only the caller can press
|
|
439
|
+
|
|
440
|
+
By default nobody is locked: any member in the chat can press a card's buttons, exactly as
|
|
441
|
+
before this existed. Pass `forUser` to `reply`, `send`, `edit` or `ack` to lock the press to one
|
|
442
|
+
member.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
await ctx.reply('Question 1 of 5', { buttons, forUser: ctx.sender });
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`forUser` takes a `User` (`ctx.sender` is one) or a bare `usr_…` id string. Either one
|
|
449
|
+
serializes to the same id on the wire.
|
|
450
|
+
|
|
451
|
+
Everyone still sees the card and its text. Only the press is gated: a non-caller's buttons
|
|
452
|
+
render at `.56` opacity with no checkmark, are not tappable, and the app shows a `For {name}`
|
|
453
|
+
hint under the row. A link button on a locked card stays pressable by anyone. It never
|
|
454
|
+
round-trips to the server, so there is nothing for the lock to gate.
|
|
455
|
+
|
|
456
|
+
A non-caller who presses anyway is refused by the server with a `403`, before anything is
|
|
457
|
+
spent. `used` stays unset and the card is unchanged. That refusal never reaches your bot: a bot
|
|
458
|
+
never presses a button, so there is no SDK exception for it.
|
|
459
|
+
|
|
460
|
+
Omitting `forUser` on a `reply`/`edit`/`ack` that replaces a card **keeps the existing lock**.
|
|
461
|
+
Passing `forUser: null` **clears** it, and the card opens to everyone. Passing a new id **moves**
|
|
462
|
+
the lock to that member. This matters most on `ack`, where a quiz redraws its own card between
|
|
463
|
+
questions and must not silently unlock itself by leaving `forUser` out.
|
|
464
|
+
|
|
465
|
+
A caller-only quiz, start to finish:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
import { Button } from 'aurival';
|
|
469
|
+
|
|
470
|
+
bot.command('quiz', async (ctx) => {
|
|
471
|
+
await ctx.reply('Which planet is largest?', {
|
|
472
|
+
buttons: [
|
|
473
|
+
new Button({ label: 'Mars', style: 'secondary' }),
|
|
474
|
+
new Button({ label: 'Jupiter', style: 'secondary' }),
|
|
475
|
+
],
|
|
476
|
+
forUser: ctx.sender,
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
bot.on('button.pressed', async (ctx) => {
|
|
481
|
+
const correct = ctx.button === 'jupiter';
|
|
482
|
+
const text = correct
|
|
483
|
+
? 'Correct. Jupiter is about eleven Earths across.'
|
|
484
|
+
: 'Not quite. Jupiter is about eleven Earths across.';
|
|
485
|
+
await ctx.ack({ text, buttons: [] });
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`ButtonContext` carries no `forUser` field. The presser is always the locked user by
|
|
490
|
+
construction: the server refuses everyone else before your handler ever runs, so there is
|
|
491
|
+
nothing on the press for it to expose. If you need the lock on a card you are acking, you
|
|
492
|
+
already have it: you set it on the send.
|
|
493
|
+
|
|
494
|
+
## Cooldowns
|
|
495
|
+
|
|
496
|
+
A cooldown paces a command or a button. You attach it, the SDK keeps the bucket, and the wire
|
|
497
|
+
never carries it — the server does not know one exists.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { Bot, Cooldown } from 'aurival';
|
|
501
|
+
|
|
502
|
+
const bot = new Bot(); // buttonCooldown: Cooldown(1, 2) already
|
|
503
|
+
|
|
504
|
+
bot.command('roll', { cooldown: { rate: 1, per: 5 } }, async (ctx) => { /* … */ });
|
|
505
|
+
bot.command('leaderboard', { cooldown: new Cooldown(3, 60, 'chat') }, async (ctx) => { /* … */ });
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**`per` is seconds, not milliseconds.** That is deliberate and it is the one place this SDK
|
|
509
|
+
departs from JS habit: `AurivalAPIError.retryAfter` is already seconds, and a developer who
|
|
510
|
+
catches one and a developer who writes a cooldown should not hold two meanings of one number.
|
|
511
|
+
|
|
512
|
+
The options object is a third form of `command()`. The two you already use —
|
|
513
|
+
`command(name, handler)` and `command(name, description, handler)` — are untouched, and
|
|
514
|
+
`description` is a field on the options object when you want all three.
|
|
515
|
+
|
|
516
|
+
Three buckets, and each one says who shares the limit:
|
|
517
|
+
|
|
518
|
+
| bucket | key | reads as |
|
|
519
|
+
| ------------------ | ------------------------------ | ----------------------------------------------- |
|
|
520
|
+
| `user` (default) | the invoking or pressing user | each person gets one every N seconds |
|
|
521
|
+
| `chat` | the conversation | this chat gets one every N seconds, whoever asks |
|
|
522
|
+
| `global` | nothing | this bot answers one every N seconds, everywhere |
|
|
523
|
+
|
|
524
|
+
A command that is refused never reaches your handler, and a refusal spends nothing — the token
|
|
525
|
+
is taken only when the call passes.
|
|
526
|
+
|
|
527
|
+
### What the member sees
|
|
528
|
+
|
|
529
|
+
One plain reply per bucket per window, then silence for the rest of it. Somebody who types
|
|
530
|
+
`/roll` eight times in five seconds gets one sentence, not eight:
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
Slow down. Try /roll again in 3 s.
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Replace it, or suppress it, with a hook. A registered hook owns the whole response: the SDK
|
|
537
|
+
sends nothing and the hook either replies or stays quiet. There is no return value to get right.
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
bot.onCooldown(async (ctx, retryAfter) => { // bot-level
|
|
541
|
+
await ctx.reply(`easy, ${Math.ceil(retryAfter)}s`);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
bot.command('roll', { cooldown: { rate: 1, per: 5 }, onCooldown }, handler); // this one wins
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`retryAfter` is seconds remaining, unrounded. The hook is called once per bucket per window too,
|
|
548
|
+
so replacing the sentence does not re-introduce the spam it existed to stop.
|
|
549
|
+
|
|
550
|
+
### Buttons already have one
|
|
551
|
+
|
|
552
|
+
Every bot ships with `Cooldown(1, 2, 'user')` on its buttons, without asking: one press every two
|
|
553
|
+
seconds per person, one bucket per person per bot. A press inside the window is answered rather
|
|
554
|
+
than handled — the presser's pending ink clears and their device shows a toast — and the button
|
|
555
|
+
stays live, because a cooldown is a wait, not a spend.
|
|
556
|
+
|
|
557
|
+
Override it where it belongs, and precedence is button, then card, then the bot default:
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
const bot = new Bot({ buttonCooldown: { rate: 1, per: 5 } }); // this bot's buttons
|
|
561
|
+
await ctx.reply({ buttons, buttonCooldown: null }); // this card's buttons, off
|
|
562
|
+
new Button({ label: 'Paint', cooldown: null }); // this one button, off
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
A per-card or per-button cooldown gets its own buckets, keyed on the message, the button id and
|
|
566
|
+
the bucket subject — button ids are unique within a message and nowhere else, so two cards that
|
|
567
|
+
both call a button `roll` never share a limit. `null` disables at any level.
|
|
568
|
+
|
|
569
|
+
A button cooldown is at most 60 seconds and is refused where you write it, not later inside an
|
|
570
|
+
ack you cannot see. Command cooldowns have no cap: `{ rate: 1, per: 3600 }` on a command is a
|
|
571
|
+
legitimate once an hour.
|
|
572
|
+
|
|
573
|
+
Link buttons are outside all of this — a link opens on the device and never round-trips, so it
|
|
574
|
+
cannot carry a cooldown and `Button.link` has no `cooldown` field.
|
|
575
|
+
|
|
576
|
+
### The caveat, said plainly
|
|
577
|
+
|
|
578
|
+
**Buckets are process memory.** They reset on restart, and they are not shared between
|
|
579
|
+
instances: a bot running two processes has two independent buckets, and a deploy clears every
|
|
580
|
+
bucket it had. Cooldowns pace a conversation. They are not a quota, and they are not the abuse
|
|
581
|
+
bound — the server keeps its own floor underneath them.
|
|
582
|
+
|
|
438
583
|
## Upgrading from 0.2.x
|
|
439
584
|
|
|
440
585
|
There is one `Context` per event family now, instead of one class for every event with most of
|
|
@@ -561,6 +706,27 @@ await ctx.edit(sent, { embeds: [new Embed({ title: 'Starting in 3…' })] });
|
|
|
561
706
|
await ctx.edit(sent, { buttons: [] }); // the row is gone, the plate stays
|
|
562
707
|
```
|
|
563
708
|
|
|
709
|
+
### Upgrading from 0.6.x
|
|
710
|
+
|
|
711
|
+
0.7.0 adds cooldowns and takes nothing away. `Cooldown` is a new export, `command()` gained a
|
|
712
|
+
third options form, `Bot` gained `buttonCooldown`, `reply()`/`send()` gained `buttonCooldown`,
|
|
713
|
+
and `Button` gained `cooldown`. Every one of them has a default that keeps 0.6.0 behaviour, with
|
|
714
|
+
**one exception you should know about**: buttons now carry `Cooldown(1, 2, 'user')` by default,
|
|
715
|
+
so a member cannot press the same bot's buttons faster than once every two seconds. That is
|
|
716
|
+
deliberate and it is on by default. If your bot's buttons are something a member is meant to
|
|
717
|
+
mash, turn it off explicitly:
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
const bot = new Bot({ buttonCooldown: null });
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Both existing `command()` overloads keep their exact signatures, so no call you have written
|
|
724
|
+
changes shape.
|
|
725
|
+
|
|
726
|
+
Two new error classes ship with it, `CooldownWithBody` and `CooldownRetryAfterInvalid`, both
|
|
727
|
+
`InvalidRequestError`. You will not normally see either: the SDK builds the cooldown ack itself
|
|
728
|
+
and refuses an out-of-range button cooldown where you write it.
|
|
729
|
+
|
|
564
730
|
## Shadowed commands
|
|
565
731
|
|
|
566
732
|
A bot can declare up to 50 commands, the SDK refuses to connect past that.
|
|
@@ -576,6 +742,42 @@ aurival warn: /ping is shadowed by another bot in chat_01j…, so it will not re
|
|
|
576
742
|
Rename the command, or get the other bot out of that chat. Nothing else in the SDK reacts
|
|
577
743
|
to it — a shadowed command in one chat is still live in every other.
|
|
578
744
|
|
|
745
|
+
## Command aliases
|
|
746
|
+
|
|
747
|
+
`aliases` on `bot.command(...)` gives one command several spellings, all handled by the same
|
|
748
|
+
function. `/roll` and `/r` fire the same handler, share the same cooldown bucket, and are one
|
|
749
|
+
command everywhere the server or the app talks about it.
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
bot.command('roll', { description: 'Roll dice', aliases: ['r'] }, async (ctx) => {
|
|
753
|
+
await ctx.reply(`/${ctx.invokedAs} ran ${ctx.command}, dice rolled`);
|
|
754
|
+
});
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
`ctx.command` is always the canonical name, `'roll'`, whichever spelling fired the handler.
|
|
758
|
+
`ctx.invokedAs` is the token the human actually typed, lowercased: `'roll'` or `'r'`. They are
|
|
759
|
+
equal on a canonical call. Code that checks `ctx.command === 'roll'` keeps working no matter
|
|
760
|
+
which spelling reached it.
|
|
761
|
+
|
|
762
|
+
The options object also takes `cooldown`, same as any other command. It is one bucket, keyed on
|
|
763
|
+
the canonical name, shared across every spelling:
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
bot.command('roll', { description: 'Roll dice', aliases: ['r'], cooldown: { rate: 1, per: 5 } }, handler);
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
At most three aliases per command. A fourth throws locally, before anything is synced. It is the
|
|
770
|
+
same validation path as a bad command name: lowercase letters, digits, hyphens and underscores
|
|
771
|
+
only, and none of the reserved names (`help`, `report`, `block`, `mute`, `kick`, `ban`, `admin`,
|
|
772
|
+
`staff`, `support`, `aurival`).
|
|
773
|
+
|
|
774
|
+
The cooldown notice echoes the typed token: hit the limit through `/r` and you get `Slow down.
|
|
775
|
+
Try /r again in 3 s.`, never `/roll`.
|
|
776
|
+
|
|
777
|
+
In the app, the command picker lists a command's aliases as a muted secondary line under its
|
|
778
|
+
row: `also /r`. Picking a row that matched by alias fills that alias, not the canonical
|
|
779
|
+
spelling: typing `/r` and picking the row inserts `/r `, not `/roll `.
|
|
780
|
+
|
|
579
781
|
## Environment
|
|
580
782
|
|
|
581
783
|
| variable | what it does |
|
package/dist/bot.d.ts
CHANGED
|
@@ -5,8 +5,20 @@ import { Context, Event } from './events.js';
|
|
|
5
5
|
import { HttpClient } from './http.js';
|
|
6
6
|
import type { AnyContext, BotContext, BotEventType, ButtonContext, ButtonEventType, EventContext, MemberContext, MemberEventType, ReactionContext, ReactionEventType } from './events.js';
|
|
7
7
|
import type { Logger } from './http.js';
|
|
8
|
+
import type { CooldownLike, CooldownOption } from './cooldown.js';
|
|
8
9
|
/** A `bot.command()` handler. */
|
|
9
10
|
export type Handler = (ctx: Context) => Promise<void> | void;
|
|
11
|
+
/**
|
|
12
|
+
* A per-command or bot-level cooldown hook (AMENDMENT-08 §4). `retryAfter`
|
|
13
|
+
* is the raw, unrounded seconds `Cooldown.check()` returned — round it
|
|
14
|
+
* yourself (`Math.max(1, Math.ceil(retryAfter))`, §5's rule) to reproduce
|
|
15
|
+
* the built-in sentence's `{n}`. Replaces the built-in "Slow down…" notice
|
|
16
|
+
* entirely: a hook that does nothing suppresses the notice, a hook that
|
|
17
|
+
* replies sends whatever it wants instead. Gated by the same once-per-window
|
|
18
|
+
* ledger as the built-in notice — a hook runs once per bucket per window,
|
|
19
|
+
* not on every refused call.
|
|
20
|
+
*/
|
|
21
|
+
export type OnCooldownHook = (ctx: Context, retryAfter: number) => Promise<void> | void;
|
|
10
22
|
/** A `member.joined` / `member.left` handler. */
|
|
11
23
|
export type MemberHandler = (ctx: MemberContext) => Promise<void> | void;
|
|
12
24
|
/** A `bot.added` / `bot.removed` handler. */
|
|
@@ -40,6 +52,45 @@ export interface BotOptions {
|
|
|
40
52
|
* keeps typing entirely in your hands (`ctx.withTyping()`). Default `true`.
|
|
41
53
|
*/
|
|
42
54
|
autoTyping?: boolean | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The default cooldown a button press checks when neither its card nor
|
|
57
|
+
* the button itself carries one (AMENDMENT-08 §3). Defaults to `new
|
|
58
|
+
* Cooldown(1, 2.0, 'user')` — one press per user per two seconds — when
|
|
59
|
+
* omitted; pass `null` to disable it bot-wide, leaving only card/button
|
|
60
|
+
* cooldowns (if any) in effect. Bound by the same 60s cap as every other
|
|
61
|
+
* button-family cooldown, checked at construction. **Its bucket lives in
|
|
62
|
+
* process memory: it resets on every restart, and it is never shared
|
|
63
|
+
* across instances or processes** — a bot running two workers has two
|
|
64
|
+
* independent buckets (see {@link Cooldown}'s own doc comment for why).
|
|
65
|
+
*/
|
|
66
|
+
buttonCooldown?: CooldownOption | undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A command's third form: `bot.command(name, { cooldown, onCooldown,
|
|
70
|
+
* description }, handler)`. `description` here is equivalent to the
|
|
71
|
+
* two-argument string overload's — the options object is a third spelling
|
|
72
|
+
* of the same call, not a different feature (R-9's two string overloads are
|
|
73
|
+
* unchanged; this is additive). `cooldown` accepts a `Cooldown` instance or
|
|
74
|
+
* a plain `{ rate, per, bucket? }` literal, normalized at registration.
|
|
75
|
+
* Command cooldowns have no 60s cap (D13) — they never reach the wire.
|
|
76
|
+
*/
|
|
77
|
+
export interface CommandOptions {
|
|
78
|
+
description?: string | undefined;
|
|
79
|
+
cooldown?: CooldownLike | undefined;
|
|
80
|
+
onCooldown?: OnCooldownHook | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* AMENDMENT-09 §2.1: alternate spellings that fire this same handler —
|
|
83
|
+
* `/r` and `/dice` for a command registered as `roll`, say. At most
|
|
84
|
+
* {@link MAX_ALIASES_PER_COMMAND}, checked locally at registration; every
|
|
85
|
+
* other rule an alias obeys (the name pattern, the reserved list,
|
|
86
|
+
* uniqueness) is the server's alone (§2.1) — this SDK reuses
|
|
87
|
+
* `invalid_command_name` for those exactly as the server does, by simply
|
|
88
|
+
* not re-validating them here and letting the sync refusal surface.
|
|
89
|
+
* Dispatch, the cooldown bucket and `onCooldown` all stay keyed on the
|
|
90
|
+
* canonical name; an alias only ever changes which typed token reaches
|
|
91
|
+
* that same handler (`ctx.invokedAs`, `Context`).
|
|
92
|
+
*/
|
|
93
|
+
aliases?: readonly string[] | undefined;
|
|
43
94
|
}
|
|
44
95
|
/**
|
|
45
96
|
* How long a command handler runs before the chat is told the bot is thinking
|
|
@@ -59,6 +110,17 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
|
|
|
59
110
|
machine: Machine;
|
|
60
111
|
paired: boolean;
|
|
61
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* One `PUT /v1/bots/{bot}/commands` row. `aliases` is optional and, when
|
|
115
|
+
* present, always non-empty — the caller omits the key entirely for a
|
|
116
|
+
* command with no aliases (AMENDMENT-09 §2.1: absent and `[]` mean the same
|
|
117
|
+
* thing, so there is no reason to send the empty spelling).
|
|
118
|
+
*/
|
|
119
|
+
export interface SyncPayloadEntry {
|
|
120
|
+
name: string;
|
|
121
|
+
description: string;
|
|
122
|
+
aliases?: string[];
|
|
123
|
+
}
|
|
62
124
|
/**
|
|
63
125
|
* The one door to command sync, so the lane that syncs is always the lane that
|
|
64
126
|
* reports. Both `Bot` call sites go through here.
|
|
@@ -66,10 +128,7 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
|
|
|
66
128
|
* Exported for the tests, not from `index.ts` — the package's export list
|
|
67
129
|
* mirrors Python's `__all__` (SDK-7) and this is not on it.
|
|
68
130
|
*/
|
|
69
|
-
export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload:
|
|
70
|
-
name: string;
|
|
71
|
-
description: string;
|
|
72
|
-
}>, log: Logger): Promise<void>;
|
|
131
|
+
export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload: SyncPayloadEntry[], log: Logger): Promise<void>;
|
|
73
132
|
/**
|
|
74
133
|
* One warning line per shadowed chat (S11). Silently dropping `conflicts` is
|
|
75
134
|
* the silence the field exists to end: the developer whose command never fires.
|
|
@@ -93,6 +152,13 @@ export declare class Bot {
|
|
|
93
152
|
*/
|
|
94
153
|
command(name: string, handler: Handler): void;
|
|
95
154
|
command(name: string, description: string, handler: Handler): void;
|
|
155
|
+
/**
|
|
156
|
+
* The options form (AMENDMENT-08 §4/seat ruling): a per-command cooldown
|
|
157
|
+
* is an option here, not a decorator. `{ cooldown, onCooldown }` beats
|
|
158
|
+
* `bot.onCooldown()` for this command alone — only one hook ever runs for
|
|
159
|
+
* a given refusal.
|
|
160
|
+
*/
|
|
161
|
+
command(name: string, options: CommandOptions, handler: Handler): void;
|
|
96
162
|
/**
|
|
97
163
|
* Register a handler for an event. Which context the handler gets follows
|
|
98
164
|
* from the type, and your editor knows it (BA-R68):
|
|
@@ -127,6 +193,12 @@ export declare class Bot {
|
|
|
127
193
|
* handler that threw, a `problem` frame, a backlog overflow.
|
|
128
194
|
*/
|
|
129
195
|
onError(fn: ErrorHook): void;
|
|
196
|
+
/**
|
|
197
|
+
* The bot-level cooldown hook (AMENDMENT-08 §4) — runs for any refused
|
|
198
|
+
* command that does not carry its own `onCooldown` option. A per-command
|
|
199
|
+
* hook always wins; only one hook ever runs for a given refusal.
|
|
200
|
+
*/
|
|
201
|
+
onCooldown(fn: OnCooldownHook): void;
|
|
130
202
|
/** Installs signal handlers and runs until SIGINT/SIGTERM or something fatal. */
|
|
131
203
|
run(): Promise<void>;
|
|
132
204
|
start(signal?: AbortSignal): Promise<void>;
|
package/dist/bot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,OAAO,EAAQ,OAAO,EAAmC,MAAM,WAAW,CAAC;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAc,MAAM,aAAa,CAAC;AACzD,OAAO,EAAgB,UAAU,EAAiB,MAAM,WAAW,CAAC;AACpE,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,eAAe,EAEf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
|
package/dist/bot.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** The developer-facing surface: declare commands, call run(), we hold the socket. */
|
|
2
2
|
import { Auth, KeyFile, machineLabel, pair, resolveHost } from './auth.js';
|
|
3
|
-
import { AurivalError, BotSuspended, RateLimitError, SessionSuperseded } from './errors.js';
|
|
3
|
+
import { AurivalError, BotSuspended, ButtonAlreadyUsed, NotFound, RateLimitError, SessionSuperseded } from './errors.js';
|
|
4
4
|
import { Context, Event, contextFor } from './events.js';
|
|
5
5
|
import { DEFAULT_HOST, HttpClient, defaultLogger } from './http.js';
|
|
6
|
+
import { CAP_TOO_MANY_ALIASES, MAX_ALIASES_PER_COMMAND, commandCooldownNotice } from './caps.js';
|
|
7
|
+
import { Cooldown, buttonBucketKey, normalizeCooldownOption, resolveButtonCooldown, retryAfterMs, roundSeconds, subjectKey, } from './cooldown.js';
|
|
6
8
|
import { EVENT_BACKLOG_OVERFLOWED, EVENT_COMMAND_INVOKED, Socket } from './socket.js';
|
|
7
9
|
import * as status from './status.js';
|
|
8
10
|
/** Types `on()` refuses, and why — each is a STRUCTURAL case where a
|
|
@@ -118,22 +120,57 @@ export class Bot {
|
|
|
118
120
|
#http = null;
|
|
119
121
|
#quiet;
|
|
120
122
|
#autoTyping;
|
|
123
|
+
#buttonCooldown;
|
|
124
|
+
#cooldownHook = null;
|
|
121
125
|
constructor(options = {}) {
|
|
122
126
|
this.#log = options.logger ?? defaultLogger();
|
|
123
127
|
this.#host = options.host;
|
|
124
128
|
this.#keyPath = options.keyPath;
|
|
125
129
|
this.#quiet = status.isQuiet(options.quiet);
|
|
126
130
|
this.#autoTyping = options.autoTyping ?? true;
|
|
131
|
+
const resolvedButtonCooldown = normalizeCooldownOption(options.buttonCooldown, {
|
|
132
|
+
boundToButton: true,
|
|
133
|
+
});
|
|
134
|
+
this.#buttonCooldown =
|
|
135
|
+
resolvedButtonCooldown === undefined ? new Cooldown(1, 2.0, 'user') : resolvedButtonCooldown;
|
|
127
136
|
}
|
|
128
137
|
command(name, second, third) {
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
let description = '';
|
|
139
|
+
let handler;
|
|
140
|
+
let cooldownLike;
|
|
141
|
+
let onCooldownHook;
|
|
142
|
+
let aliasesLike;
|
|
143
|
+
if (typeof second === 'string') {
|
|
144
|
+
description = second;
|
|
145
|
+
handler = third;
|
|
146
|
+
}
|
|
147
|
+
else if (typeof second === 'function') {
|
|
148
|
+
handler = second;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
description = second.description ?? '';
|
|
152
|
+
cooldownLike = second.cooldown;
|
|
153
|
+
onCooldownHook = second.onCooldown;
|
|
154
|
+
aliasesLike = second.aliases;
|
|
155
|
+
handler = third;
|
|
156
|
+
}
|
|
131
157
|
if (handler === undefined)
|
|
132
158
|
throw new AurivalError(`command(${name}) needs a handler`);
|
|
159
|
+
// AMENDMENT-09 §2.1: `[]` in every state but never `undefined` — a
|
|
160
|
+
// `Command` this SDK constructs always carries the list, even empty.
|
|
161
|
+
const aliases = aliasesLike !== undefined ? [...aliasesLike] : [];
|
|
162
|
+
if (aliases.length > MAX_ALIASES_PER_COMMAND) {
|
|
163
|
+
throw new AurivalError(CAP_TOO_MANY_ALIASES);
|
|
164
|
+
}
|
|
133
165
|
const key = lookupKey(name);
|
|
134
166
|
if (this.#registered.has(key))
|
|
135
167
|
status.duplicateCommand(name, this.#quiet);
|
|
136
|
-
|
|
168
|
+
const registered = { command: { name, description, aliases }, handler };
|
|
169
|
+
if (cooldownLike !== undefined)
|
|
170
|
+
registered.cooldown = Cooldown.from(cooldownLike);
|
|
171
|
+
if (onCooldownHook !== undefined)
|
|
172
|
+
registered.onCooldown = onCooldownHook;
|
|
173
|
+
this.#registered.set(key, registered);
|
|
137
174
|
}
|
|
138
175
|
on(type, handler) {
|
|
139
176
|
const reason = NEVER_DISPATCHED_TO_ON.get(type);
|
|
@@ -153,6 +190,14 @@ export class Bot {
|
|
|
153
190
|
onError(fn) {
|
|
154
191
|
this.#errorHook = fn;
|
|
155
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The bot-level cooldown hook (AMENDMENT-08 §4) — runs for any refused
|
|
195
|
+
* command that does not carry its own `onCooldown` option. A per-command
|
|
196
|
+
* hook always wins; only one hook ever runs for a given refusal.
|
|
197
|
+
*/
|
|
198
|
+
onCooldown(fn) {
|
|
199
|
+
this.#cooldownHook = fn;
|
|
200
|
+
}
|
|
156
201
|
// -- running -----------------------------------------------------------
|
|
157
202
|
/** Installs signal handlers and runs until SIGINT/SIGTERM or something fatal. */
|
|
158
203
|
async run() {
|
|
@@ -209,7 +254,16 @@ export class Bot {
|
|
|
209
254
|
onProblem: (problem) => {
|
|
210
255
|
void this.#callErrorHook(problem, null);
|
|
211
256
|
},
|
|
212
|
-
|
|
257
|
+
// `button.pressed` always claims a handler exists, whether or not a
|
|
258
|
+
// developer ever called `on('button.pressed', ...)` — a press must
|
|
259
|
+
// reach `#handleButtonPress` so the cooldown check (and its ack on
|
|
260
|
+
// refusal) always runs, even for a bot that only sends cards and
|
|
261
|
+
// handles presses nowhere, or not yet (AMENDMENT-08 §1/§5: the button
|
|
262
|
+
// default applies to every bot with no line of code, and the presser
|
|
263
|
+
// must never be left staring at pending ink). A press that passes
|
|
264
|
+
// with no registered handler simply falls through `#handleGeneric`
|
|
265
|
+
// with nothing to call; the socket-level ack still fires either way.
|
|
266
|
+
hasHandler: (type) => type === 'button.pressed' || this.#onHandlers.has(type),
|
|
213
267
|
logger: this.#log,
|
|
214
268
|
botName: machine.bot,
|
|
215
269
|
commandCount: this.#registered.size,
|
|
@@ -239,10 +293,16 @@ export class Bot {
|
|
|
239
293
|
* and land the sync in the background.
|
|
240
294
|
*/
|
|
241
295
|
async #syncCommands(http, bot, signal) {
|
|
242
|
-
const payload = [...this.#registered.values()].map((r) =>
|
|
243
|
-
name: r.command.name,
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
const payload = [...this.#registered.values()].map((r) => {
|
|
297
|
+
const entry = { name: r.command.name, description: r.command.description };
|
|
298
|
+
// AMENDMENT-09 §2.1: the key is present only when the list is
|
|
299
|
+
// non-empty — absent and `[]` mean the same thing on this wire, so
|
|
300
|
+
// there is nothing to gain from sending the empty spelling.
|
|
301
|
+
if (r.command.aliases !== undefined && r.command.aliases.length > 0) {
|
|
302
|
+
entry.aliases = [...r.command.aliases];
|
|
303
|
+
}
|
|
304
|
+
return entry;
|
|
305
|
+
});
|
|
246
306
|
try {
|
|
247
307
|
await syncCommandsAndReport(http, bot, payload, this.#log);
|
|
248
308
|
return null;
|
|
@@ -303,6 +363,10 @@ export class Bot {
|
|
|
303
363
|
await this.#handleCommand(event);
|
|
304
364
|
return;
|
|
305
365
|
}
|
|
366
|
+
if (event.type === 'button.pressed') {
|
|
367
|
+
await this.#handleButtonPress(event);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
306
370
|
await this.#handleGeneric(event);
|
|
307
371
|
}
|
|
308
372
|
async #handleCommand(event) {
|
|
@@ -317,6 +381,13 @@ export class Bot {
|
|
|
317
381
|
if (this.#http === null)
|
|
318
382
|
return;
|
|
319
383
|
const ctx = Context.fromEvent(event, this.#http);
|
|
384
|
+
// AMENDMENT-08: the cooldown check runs before auto-typing and before the
|
|
385
|
+
// handler — a refused invocation never reaches either.
|
|
386
|
+
if (registered.cooldown !== undefined) {
|
|
387
|
+
const refused = await this.#handleCommandCooldown(ctx, registered);
|
|
388
|
+
if (refused)
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
320
391
|
const typing = this.#startAutoTyping(ctx);
|
|
321
392
|
try {
|
|
322
393
|
await registered.handler(ctx);
|
|
@@ -330,6 +401,109 @@ export class Bot {
|
|
|
330
401
|
await typing.stop();
|
|
331
402
|
}
|
|
332
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* A registered command's own cooldown (AMENDMENT-08 §4). Returns `true`
|
|
406
|
+
* when the invocation was refused (dispatch stops here). Its bucket key
|
|
407
|
+
* carries no message/button scope (`attachmentScope = ()`) — each
|
|
408
|
+
* command's `Cooldown` is its own instance, so nothing else could collide
|
|
409
|
+
* with it anyway.
|
|
410
|
+
*
|
|
411
|
+
* Once per bucket per window: either the per-command hook, the bot-level
|
|
412
|
+
* one, or — with neither registered — the fixed reply. A hook does not
|
|
413
|
+
* run on every refused call; it shares the same `noticeOnce` gate the
|
|
414
|
+
* built-in notice uses, so mashing a refused command inside one window
|
|
415
|
+
* produces one hook call (or one reply), not a flood of them.
|
|
416
|
+
*/
|
|
417
|
+
async #handleCommandCooldown(ctx, registered) {
|
|
418
|
+
const cooldown = registered.cooldown;
|
|
419
|
+
if (cooldown === undefined)
|
|
420
|
+
return false;
|
|
421
|
+
const key = subjectKey(cooldown.bucket, ctx.sender.id, ctx.chat.id);
|
|
422
|
+
const refusedAfterSeconds = cooldown.check(key);
|
|
423
|
+
if (refusedAfterSeconds === null)
|
|
424
|
+
return false;
|
|
425
|
+
if (!cooldown.noticeOnce(key))
|
|
426
|
+
return true;
|
|
427
|
+
// Per-command beats bot-level; only one hook ever runs.
|
|
428
|
+
const hook = registered.onCooldown ?? this.#cooldownHook;
|
|
429
|
+
if (hook !== null && hook !== undefined) {
|
|
430
|
+
try {
|
|
431
|
+
await hook(ctx, refusedAfterSeconds);
|
|
432
|
+
}
|
|
433
|
+
catch (exc) {
|
|
434
|
+
this.#log.error(`cooldown hook for ${JSON.stringify(ctx.command)} threw: ${exc instanceof Error ? (exc.stack ?? exc.message) : String(exc)}`);
|
|
435
|
+
await this.#callErrorHook(exc, ctx);
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
const n = roundSeconds(refusedAfterSeconds);
|
|
440
|
+
try {
|
|
441
|
+
// AMENDMENT-09 §13.1: names the token the human actually typed, not
|
|
442
|
+
// the canonical spelling — the bucket is still the canonical
|
|
443
|
+
// command's (one bucket, all spellings); only the rendered sentence
|
|
444
|
+
// changes.
|
|
445
|
+
await ctx.reply(commandCooldownNotice(ctx.invokedAs, n));
|
|
446
|
+
}
|
|
447
|
+
catch (exc) {
|
|
448
|
+
this.#log.error(`cooldown notice for ${JSON.stringify(ctx.command)} failed: ${exc instanceof Error ? (exc.stack ?? exc.message) : String(exc)}`);
|
|
449
|
+
await this.#callErrorHook(exc, ctx);
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* AMENDMENT-08 §3: resolve the pressed button's cooldown (button > card >
|
|
455
|
+
* bot default), check it, and — on refusal — send the cooldown ack
|
|
456
|
+
* instead of dispatching to any `on('button.pressed', ...)` handler. The
|
|
457
|
+
* socket still acks the underlying event exactly as it does for any other
|
|
458
|
+
* dispatch; only the handler is skipped.
|
|
459
|
+
*/
|
|
460
|
+
async #handleButtonPress(event) {
|
|
461
|
+
if (this.#http === null)
|
|
462
|
+
return;
|
|
463
|
+
const ctx = contextFor(event, this.#http);
|
|
464
|
+
const card = this.#http.cardCooldowns.lookup(ctx.message.id);
|
|
465
|
+
const resolved = resolveButtonCooldown(this.#buttonCooldown, card, ctx.button);
|
|
466
|
+
if (resolved.cooldown !== null) {
|
|
467
|
+
const scope = resolved.scoped ? { messageId: ctx.message.id, buttonId: ctx.button } : null;
|
|
468
|
+
const key = buttonBucketKey(resolved.cooldown, scope, ctx.user.id, ctx.chat.id);
|
|
469
|
+
const refusedAfterSeconds = resolved.cooldown.check(key);
|
|
470
|
+
if (refusedAfterSeconds !== null) {
|
|
471
|
+
await this.#ackButtonCooldown(ctx, retryAfterMs(refusedAfterSeconds));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
await this.#handleGeneric(event);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The SDK's own automatic answer to a refused press (§5.1). Swallows
|
|
479
|
+
* `ButtonAlreadyUsed`/`NotFound` silently — a debug line, never `onError`,
|
|
480
|
+
* never thrown — because the press already resolved some other way (a
|
|
481
|
+
* double-tap, an expired row) and the cooldown refusal is moot. Every
|
|
482
|
+
* other status still goes through the normal error path.
|
|
483
|
+
*/
|
|
484
|
+
async #ackButtonCooldown(ctx, ms) {
|
|
485
|
+
if (this.#http === null)
|
|
486
|
+
return;
|
|
487
|
+
try {
|
|
488
|
+
await this.#http.ackCooldown(ctx.interaction, ms);
|
|
489
|
+
// The ack landed. Without this line a button cooldown is invisible to
|
|
490
|
+
// the bot author: the SDK answers the press itself, the handler never
|
|
491
|
+
// runs, and nothing is written anywhere — so a cooldown firing and a
|
|
492
|
+
// press vanishing look identical from the outside. `info`, not
|
|
493
|
+
// `debug`, because the swallowed-refusal lines below are the ones a
|
|
494
|
+
// reader can ignore; this one is the cooldown working. Byte-mirrors
|
|
495
|
+
// `sdk/python/aurival/bot.py`'s sentence (SDK-7).
|
|
496
|
+
this.#log.info(`cooldown: acked press on ${ctx.message.id}/${ctx.button} for ${ctx.user.id}, retry_after_ms=${ms}`);
|
|
497
|
+
}
|
|
498
|
+
catch (exc) {
|
|
499
|
+
if (exc instanceof ButtonAlreadyUsed || exc instanceof NotFound) {
|
|
500
|
+
this.#log.debug(`cooldown ack for ${JSON.stringify(ctx.interaction)} found the press already resolved: ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.#log.error(`cooldown ack for ${JSON.stringify(ctx.interaction)} failed: ${exc instanceof Error ? (exc.stack ?? exc.message) : String(exc)}`);
|
|
504
|
+
await this.#callErrorHook(exc, ctx);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
333
507
|
// -- auto-typing (SDK-41) ------------------------------------------------
|
|
334
508
|
#startAutoTyping(ctx) {
|
|
335
509
|
if (!this.#autoTyping || ctx.chat.id === '')
|