aurival 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 +209 -54
- package/dist/bot.d.ts +25 -4
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +33 -7
- package/dist/bot.js.map +1 -1
- package/dist/caps.d.ts +38 -0
- package/dist/caps.d.ts.map +1 -1
- package/dist/caps.js +38 -0
- package/dist/caps.js.map +1 -1
- package/dist/embeds.d.ts.map +1 -1
- package/dist/embeds.js +52 -9
- package/dist/embeds.js.map +1 -1
- package/dist/errors.d.ts +51 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +59 -1
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +56 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +72 -6
- package/dist/events.js.map +1 -1
- package/dist/http.d.ts +13 -5
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +15 -4
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,8 +75,8 @@ bot.command('say', async (ctx) => {
|
|
|
75
75
|
ctx.command; // "say"
|
|
76
76
|
ctx.arguments; // the raw rest of the line, unparsed, possibly ""
|
|
77
77
|
ctx.chat; // { id, type, name, member_count }
|
|
78
|
-
ctx.sender; // { id, handle, name }
|
|
79
|
-
ctx.message.text; // "/say hi"
|
|
78
|
+
ctx.sender; // { id, handle, name }, always set
|
|
79
|
+
ctx.message.text; // "/say hi", the invoking message, verbatim
|
|
80
80
|
await ctx.reply('…');
|
|
81
81
|
});
|
|
82
82
|
```
|
|
@@ -184,7 +184,8 @@ bot.command('fix', async (ctx) => {
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
bot.command('oops', async (ctx) => {
|
|
187
|
-
await ctx.
|
|
187
|
+
const sent = await ctx.reply('ignore that');
|
|
188
|
+
await ctx.delete(sent);
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
bot.command('upvote', async (ctx) => {
|
|
@@ -224,6 +225,10 @@ straight back to `edit()`, `delete()` and `react()` — no id juggling. The `sen
|
|
|
224
225
|
those, when it is set at all, is known by id alone — `handle` and `name` are empty strings,
|
|
225
226
|
because the REST entity names the sender with a bare `usr_…`.
|
|
226
227
|
|
|
228
|
+
`edit()` and `delete()` take only the bot's own messages; anyone else's answers
|
|
229
|
+
`message_not_yours`, which is why `oops` above deletes the reply it just sent rather than the
|
|
230
|
+
message that invoked it. `react()` and `unreact()` work on any message in a chat the bot is in.
|
|
231
|
+
|
|
227
232
|
The typing indicator is automatic: a command handler still running 300 ms after it started
|
|
228
233
|
shows the chat "is thinking", and the indicator clears when the handler returns, including on a
|
|
229
234
|
throw. A handler that replies inside those 300 ms sends nothing, so a fast bot never flickers.
|
|
@@ -245,6 +250,11 @@ bot.on('member.joined', async (ctx) => {
|
|
|
245
250
|
});
|
|
246
251
|
```
|
|
247
252
|
|
|
253
|
+
Every entry in `mentions` needs its `@handle` token actually present in `text`, outside any
|
|
254
|
+
code block or inline code, or the server rejects the request — write the handle into the
|
|
255
|
+
message yourself, in plain text; the SDK will not edit `text` for you, and a mention inside
|
|
256
|
+
code, or with no token at all, renders as nothing.
|
|
257
|
+
|
|
248
258
|
## Embeds and buttons
|
|
249
259
|
|
|
250
260
|
`Embed` is a builder — every `setAuthor`/`setThumbnail`/`addField`/`setFooter` call returns the
|
|
@@ -258,20 +268,20 @@ already uses:
|
|
|
258
268
|
```ts
|
|
259
269
|
import { Embed, Button } from 'aurival';
|
|
260
270
|
|
|
261
|
-
const embed = new Embed({ title: '
|
|
262
|
-
.setAuthor('
|
|
263
|
-
.
|
|
264
|
-
.addField('
|
|
265
|
-
.
|
|
266
|
-
.setFooter('
|
|
271
|
+
const embed = new Embed({ title: 'Title', description: 'Description. Plain text or markdown, under the title.' })
|
|
272
|
+
.setAuthor('Author line')
|
|
273
|
+
.addField('Field name', 'Field value', true)
|
|
274
|
+
.addField('Second field', 'Second value', true)
|
|
275
|
+
.setImage('https://bots.aurival.com/docs-assets/aurival-welcome-cover.jpg')
|
|
276
|
+
.setFooter('Footer text', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png');
|
|
267
277
|
|
|
268
278
|
const buttons = [
|
|
269
|
-
new Button({ label: '
|
|
270
|
-
new Button({ label: '
|
|
271
|
-
new Button({ label: '
|
|
279
|
+
new Button({ label: 'Primary action', id: 'primary', style: 'primary' }),
|
|
280
|
+
new Button({ label: 'Secondary action', id: 'secondary', style: 'secondary' }),
|
|
281
|
+
new Button({ label: 'Danger action', id: 'danger', style: 'danger' }),
|
|
272
282
|
];
|
|
273
283
|
|
|
274
|
-
await ctx.reply(
|
|
284
|
+
await ctx.reply({ embeds: [embed], buttons });
|
|
275
285
|
```
|
|
276
286
|
|
|
277
287
|
A card earns its place when there is something to put on it: buttons, fields, an image. A reply
|
|
@@ -288,19 +298,20 @@ url throws, because a link button with nowhere to go is a dead pill:
|
|
|
288
298
|
import { Button } from 'aurival';
|
|
289
299
|
|
|
290
300
|
const buttons = [
|
|
291
|
-
Button.link({ label: '
|
|
292
|
-
Button.link({ label: '
|
|
293
|
-
new Button({ label: '
|
|
301
|
+
Button.link({ label: 'Opens aurival.com', url: 'https://aurival.com' }),
|
|
302
|
+
Button.link({ label: 'Opens the docs', url: 'https://bots.aurival.com' }),
|
|
303
|
+
new Button({ label: 'Primary action', id: 'primary', style: 'primary' }),
|
|
294
304
|
];
|
|
295
305
|
|
|
296
|
-
await ctx.reply('
|
|
306
|
+
await ctx.reply('Two link buttons and a regular one, sharing one row.', { buttons });
|
|
297
307
|
```
|
|
298
308
|
|
|
299
309
|
A link button never comes back to you: no `button.pressed` event, ever, and it never flips the
|
|
300
310
|
row to used. It also stays tappable after a sibling is pressed, and while a sibling is still
|
|
301
311
|
waiting on your `ack()` — the row greys out around it, the link does not. `id` still has to be
|
|
302
312
|
there and still has to be unique in the message, and the SDK still slugs it from the label when
|
|
303
|
-
you leave it out, so that `
|
|
313
|
+
you leave it out, so that `Opens the docs` pill gets `id: 'opens-the-docs'` like any other
|
|
314
|
+
button. Link
|
|
304
315
|
buttons count toward the five-button cap, and a message made only of link buttons is fine.
|
|
305
316
|
|
|
306
317
|
A url is `https://` and at most 2048 characters, everywhere a url is a link target. A non-link
|
|
@@ -315,11 +326,11 @@ you cannot see is worse than an error you can.
|
|
|
315
326
|
import { Button } from 'aurival';
|
|
316
327
|
|
|
317
328
|
const buttons = [
|
|
318
|
-
new Button({ label: '
|
|
319
|
-
new Button({ label: '
|
|
329
|
+
new Button({ label: 'Secondary action', id: 'secondary', style: 'secondary', emoji: '🤔' }),
|
|
330
|
+
new Button({ label: 'Primary action', id: 'primary', style: 'primary', emoji: '⏰' }),
|
|
320
331
|
];
|
|
321
332
|
|
|
322
|
-
await ctx.reply('
|
|
333
|
+
await ctx.reply('Two buttons, each with one emoji in front of the label.', { buttons });
|
|
323
334
|
```
|
|
324
335
|
|
|
325
336
|
Exactly one unicode emoji, no custom emoji, no `:shortcode:`, no image url. The emoji does not
|
|
@@ -337,16 +348,15 @@ title tappable, and the third argument to `setAuthor` does the same for the auth
|
|
|
337
348
|
import { Embed } from 'aurival';
|
|
338
349
|
|
|
339
350
|
const embed = new Embed({
|
|
340
|
-
title:
|
|
341
|
-
description: '
|
|
342
|
-
|
|
343
|
-
url: 'https://aurival.com/spaces/deepcuts',
|
|
351
|
+
title: 'A linked title',
|
|
352
|
+
description: 'The title and the author line below both open a url when tapped.',
|
|
353
|
+
url: 'https://aurival.com',
|
|
344
354
|
})
|
|
345
|
-
.setAuthor('
|
|
346
|
-
.setThumbnail('https://
|
|
347
|
-
.setFooter('
|
|
355
|
+
.setAuthor('Author line', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png', 'https://aurival.com')
|
|
356
|
+
.setThumbnail('https://bots.aurival.com/docs-assets/aurival-mascot-hero.png')
|
|
357
|
+
.setFooter('Footer text', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png');
|
|
348
358
|
|
|
349
|
-
await ctx.reply(
|
|
359
|
+
await ctx.reply({ embeds: [embed] });
|
|
350
360
|
```
|
|
351
361
|
|
|
352
362
|
Each of the three needs the thing it attaches to: a footer icon needs footer text, an embed url
|
|
@@ -364,13 +374,13 @@ as four asterisks and two words:
|
|
|
364
374
|
import { Embed } from 'aurival';
|
|
365
375
|
|
|
366
376
|
const embed = new Embed({
|
|
367
|
-
title: '
|
|
377
|
+
title: 'Markdown in the description',
|
|
368
378
|
description:
|
|
369
|
-
'**
|
|
370
|
-
'
|
|
371
|
-
}).addField('
|
|
379
|
+
'**Bold**, *italic*, ~~strikethrough~~ and `inline code` all render.\n' +
|
|
380
|
+
'Headings render as bold body text, not larger type.',
|
|
381
|
+
}).addField('Field name', 'Field value', true);
|
|
372
382
|
|
|
373
|
-
await ctx.reply(
|
|
383
|
+
await ctx.reply({ embeds: [embed] });
|
|
374
384
|
```
|
|
375
385
|
|
|
376
386
|
Supported: bold, italic, bold-italic, strikethrough and inline code. Headings render as bold
|
|
@@ -422,8 +432,8 @@ A press comes back as a `button.pressed` event, handled the same way any other e
|
|
|
422
432
|
```ts
|
|
423
433
|
bot.on('button.pressed', async (ctx) => {
|
|
424
434
|
await ctx.ack();
|
|
425
|
-
if (ctx.button === '
|
|
426
|
-
await ctx.reply('
|
|
435
|
+
if (ctx.button === 'primary') {
|
|
436
|
+
await ctx.reply('You pressed the primary button.');
|
|
427
437
|
}
|
|
428
438
|
});
|
|
429
439
|
```
|
|
@@ -432,9 +442,64 @@ The `ctx` there is a `ButtonContext`: `.chat`, `.user`, `.message`, `.button` (t
|
|
|
432
442
|
when building it) and `.interaction`. Call `await ctx.ack()` exactly once per press — a button
|
|
433
443
|
is single-use, and acking one a second time doesn't throw locally, the server 409s the request.
|
|
434
444
|
|
|
435
|
-
The
|
|
445
|
+
The showcase bot runs every one of these cards live. The full set is in the
|
|
436
446
|
[cookbook](https://bots.aurival.com/docs/cookbook).
|
|
437
447
|
|
|
448
|
+
## Buttons only the caller can press
|
|
449
|
+
|
|
450
|
+
By default nobody is locked: any member in the chat can press a card's buttons, exactly as
|
|
451
|
+
before this existed. Pass `forUser` to `reply`, `send`, `edit` or `ack` to lock the press to one
|
|
452
|
+
member.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
await ctx.reply('Locked to the caller', { buttons, forUser: ctx.sender });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
`forUser` takes a `User` (`ctx.sender` is one) or a bare `usr_…` id string. Either one
|
|
459
|
+
serializes to the same id on the wire.
|
|
460
|
+
|
|
461
|
+
Everyone still sees the card and its text. Only the press is gated: a non-caller's buttons
|
|
462
|
+
render at `.56` opacity with no checkmark and are not tappable. Nothing is drawn under the
|
|
463
|
+
row: the card never says who it is for, so write that into the embed footer yourself if you
|
|
464
|
+
want it on the plate. A link button on a locked card stays pressable by anyone. It never
|
|
465
|
+
round-trips to the server, so there is nothing for the lock to gate.
|
|
466
|
+
|
|
467
|
+
A non-caller who presses anyway is refused by the server with a `403`, before anything is
|
|
468
|
+
spent. `used` stays unset and the card is unchanged. That refusal never reaches your bot: a bot
|
|
469
|
+
never presses a button, so there is no SDK exception for it.
|
|
470
|
+
|
|
471
|
+
Omitting `forUser` on a `reply`/`edit`/`ack` that replaces a card **keeps the existing lock**.
|
|
472
|
+
Passing `forUser: null` **clears** it, and the card opens to everyone. Passing a new id **moves**
|
|
473
|
+
the lock to that member. This matters most on `ack`, where a quiz redraws its own card between
|
|
474
|
+
questions and must not silently unlock itself by leaving `forUser` out.
|
|
475
|
+
|
|
476
|
+
A caller-only quiz, start to finish:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { Button, Embed } from 'aurival';
|
|
480
|
+
|
|
481
|
+
bot.command('locked', async (ctx) => {
|
|
482
|
+
const embed = new Embed({
|
|
483
|
+
title: 'Locked to the caller',
|
|
484
|
+
description: 'Everyone sees this card. Only the person who ran the command can press.',
|
|
485
|
+
});
|
|
486
|
+
await ctx.reply({
|
|
487
|
+
embeds: [embed],
|
|
488
|
+
buttons: [new Button({ label: 'Only the caller presses', id: 'only', style: 'primary' })],
|
|
489
|
+
forUser: ctx.sender,
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
bot.on('button.pressed', async (ctx) => {
|
|
494
|
+
await ctx.ack({ text: 'Pressed.', buttons: [] });
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`ButtonContext` carries no `forUser` field. The presser is always the locked user by
|
|
499
|
+
construction: the server refuses everyone else before your handler ever runs, so there is
|
|
500
|
+
nothing on the press for it to expose. If you need the lock on a card you are acking, you
|
|
501
|
+
already have it: you set it on the send.
|
|
502
|
+
|
|
438
503
|
## Cooldowns
|
|
439
504
|
|
|
440
505
|
A cooldown paces a command or a button. You attach it, the SDK keeps the bucket, and the wire
|
|
@@ -503,7 +568,7 @@ Override it where it belongs, and precedence is button, then card, then the bot
|
|
|
503
568
|
```ts
|
|
504
569
|
const bot = new Bot({ buttonCooldown: { rate: 1, per: 5 } }); // this bot's buttons
|
|
505
570
|
await ctx.reply({ buttons, buttonCooldown: null }); // this card's buttons, off
|
|
506
|
-
new Button({ label: '
|
|
571
|
+
new Button({ label: 'Primary action', cooldown: null }); // this one button, off
|
|
507
572
|
```
|
|
508
573
|
|
|
509
574
|
A per-card or per-button cooldown gets its own buckets, keyed on the message, the button id and
|
|
@@ -517,6 +582,23 @@ legitimate once an hour.
|
|
|
517
582
|
Link buttons are outside all of this — a link opens on the device and never round-trips, so it
|
|
518
583
|
cannot carry a cooldown and `Button.link` has no `cooldown` field.
|
|
519
584
|
|
|
585
|
+
### Keep one Cooldown per button
|
|
586
|
+
|
|
587
|
+
A `Cooldown` owns its own bucket of remaining presses. Build a fresh one inside the ack handler —
|
|
588
|
+
or anywhere a press can re-run it — and it forgets every prior press, so the cooldown never
|
|
589
|
+
actually triggers. Define the `Cooldown` and the `Button` once, at module scope, and reuse those
|
|
590
|
+
same objects on the send and on every ack that follows it:
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
// Once, at module scope — not inside the handler.
|
|
594
|
+
const cooldown = new Cooldown(1, 10);
|
|
595
|
+
const button = new Button({ label: '+1', id: 'inc', style: 'primary', cooldown });
|
|
596
|
+
|
|
597
|
+
bot.on('button.pressed', async (ctx) => {
|
|
598
|
+
await ctx.ack({ buttons: [button] }); // the same button, never rebuilt here
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
520
602
|
### The caveat, said plainly
|
|
521
603
|
|
|
522
604
|
**Buckets are process memory.** They reset on restart, and they are not shared between
|
|
@@ -607,31 +689,30 @@ Three states, one rule, on both doors:
|
|
|
607
689
|
with all three left out throws `an edit needs text, embeds or buttons` before anything is sent,
|
|
608
690
|
and the server answers the same sentence as `NothingToEdit`.
|
|
609
691
|
|
|
610
|
-
|
|
611
|
-
|
|
692
|
+
Pressing a button is the whole feature in one pair of handlers. Both cards earn their plate: the
|
|
693
|
+
first card carries buttons, the replacement carries the result.
|
|
612
694
|
|
|
613
695
|
```ts
|
|
614
696
|
import { Button, Embed } from 'aurival';
|
|
615
697
|
|
|
616
|
-
bot.command('
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
});
|
|
698
|
+
bot.command('update', async (ctx) => {
|
|
699
|
+
const embed = new Embed({ title: 'Press a button', description: 'The card is replaced in place when you press one.' });
|
|
700
|
+
const buttons = [
|
|
701
|
+
new Button({ label: 'Press me, card updates', id: 'u1', style: 'primary' }),
|
|
702
|
+
new Button({ label: 'Or press me', id: 'u2', style: 'secondary' }),
|
|
703
|
+
];
|
|
704
|
+
await ctx.reply({ embeds: [embed], buttons });
|
|
624
705
|
});
|
|
625
706
|
|
|
626
707
|
bot.on('button.pressed', async (ctx) => {
|
|
627
|
-
const
|
|
708
|
+
const which = ctx.button === 'u1' ? 'first' : 'second';
|
|
628
709
|
const result = new Embed({
|
|
629
|
-
title:
|
|
630
|
-
description: '
|
|
631
|
-
})
|
|
710
|
+
title: `You pressed the ${which} button`,
|
|
711
|
+
description: 'ack() replaced the card in place. Nothing else in the chat moved.',
|
|
712
|
+
});
|
|
632
713
|
await ctx.ack({
|
|
633
714
|
embeds: [result],
|
|
634
|
-
buttons: [new Button({
|
|
715
|
+
buttons: [new Button({ label: 'Press again', id: 'again', style: 'primary' })],
|
|
635
716
|
});
|
|
636
717
|
});
|
|
637
718
|
```
|
|
@@ -646,7 +727,7 @@ on the id.
|
|
|
646
727
|
outside a press:
|
|
647
728
|
|
|
648
729
|
```ts
|
|
649
|
-
await ctx.edit(sent, { embeds: [new Embed({ title: '
|
|
730
|
+
await ctx.edit(sent, { embeds: [new Embed({ title: 'Title' })] });
|
|
650
731
|
await ctx.edit(sent, { buttons: [] }); // the row is gone, the plate stays
|
|
651
732
|
```
|
|
652
733
|
|
|
@@ -671,6 +752,44 @@ Two new error classes ship with it, `CooldownWithBody` and `CooldownRetryAfterIn
|
|
|
671
752
|
`InvalidRequestError`. You will not normally see either: the SDK builds the cooldown ack itself
|
|
672
753
|
and refuses an out-of-range button cooldown where you write it.
|
|
673
754
|
|
|
755
|
+
### Upgrading from 0.8.x
|
|
756
|
+
|
|
757
|
+
0.9.0 adds new per-field and per-embed caps, and **one of them can break a card that worked on
|
|
758
|
+
0.8.x**: none of these fields were length-limited before, so a card built past one of the new
|
|
759
|
+
limits used to send successfully and now throws. `Embed.addField`, `setAuthor` and `setFooter`
|
|
760
|
+
check length locally as of this version, before any round trip:
|
|
761
|
+
|
|
762
|
+
- an embed field name is at most 256 characters
|
|
763
|
+
- an embed field value is at most 1024 characters
|
|
764
|
+
- an embed author name is at most 256 characters
|
|
765
|
+
- an embed footer's text is at most 2048 characters
|
|
766
|
+
- `setImage` and `setThumbnail` urls are at most 2048 characters (author and footer icons
|
|
767
|
+
stay uncapped)
|
|
768
|
+
- the embeds on one message carry at most 6000 characters in total, summed across every
|
|
769
|
+
title, description, field name and value, author name and footer text — checked when the
|
|
770
|
+
message is sent, since it spans every embed, not one
|
|
771
|
+
|
|
772
|
+
A card that never approaches these sizes is unaffected. One built past a limit now throws
|
|
773
|
+
locally, with the same sentence the server used to send back as a 400 — the SDK catches it
|
|
774
|
+
before the round trip instead of after. The server enforces the identical caps independently
|
|
775
|
+
of SDK version, so a bot on an older SDK sees the 400 from the wire the moment this ships, not
|
|
776
|
+
only on upgrade. Six matching error classes ship for the wire-error path, all
|
|
777
|
+
`InvalidRequestError`: `EmbedFieldNameTooLong`, `EmbedFieldValueTooLong`,
|
|
778
|
+
`EmbedAuthorNameTooLong`, `EmbedFooterTextTooLong`, `ImageURLTooLong` and `EmbedsTooLong`.
|
|
779
|
+
|
|
780
|
+
`mentions` got stricter too, and this is also a server-side change that applies regardless of
|
|
781
|
+
SDK version: a `@handle` token that appears only inside a code block or inline code no longer
|
|
782
|
+
satisfies the mention, and the server answers `mention_token_missing` the same way it always
|
|
783
|
+
has for a missing token — write the handle into `text` yourself, in plain text, outside any
|
|
784
|
+
fence or backtick.
|
|
785
|
+
|
|
786
|
+
Two fixes land alongside the caps: `ctx.edit(sent, { forUser: null })` with no other part
|
|
787
|
+
named is now a real edit that reaches the wire as `{ "for_user": null }`, rather than being
|
|
788
|
+
refused locally before the round trip — naming only the lock is a real change to the card.
|
|
789
|
+
And a successful button-cooldown ack now logs one `info` line naming the message, the button,
|
|
790
|
+
the presser and the window, so a cooldown firing and a press silently vanishing no longer look
|
|
791
|
+
identical from the outside.
|
|
792
|
+
|
|
674
793
|
## Shadowed commands
|
|
675
794
|
|
|
676
795
|
A bot can declare up to 50 commands, the SDK refuses to connect past that.
|
|
@@ -686,6 +805,42 @@ aurival warn: /ping is shadowed by another bot in chat_01j…, so it will not re
|
|
|
686
805
|
Rename the command, or get the other bot out of that chat. Nothing else in the SDK reacts
|
|
687
806
|
to it — a shadowed command in one chat is still live in every other.
|
|
688
807
|
|
|
808
|
+
## Command aliases
|
|
809
|
+
|
|
810
|
+
`aliases` on `bot.command(...)` gives one command several spellings, all handled by the same
|
|
811
|
+
function. `/roll` and `/r` fire the same handler, share the same cooldown bucket, and are one
|
|
812
|
+
command everywhere the server or the app talks about it.
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
bot.command('roll', { description: 'Roll dice', aliases: ['r'] }, async (ctx) => {
|
|
816
|
+
await ctx.reply(`/${ctx.invokedAs} ran ${ctx.command}, dice rolled`);
|
|
817
|
+
});
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
`ctx.command` is always the canonical name, `'roll'`, whichever spelling fired the handler.
|
|
821
|
+
`ctx.invokedAs` is the token the human actually typed, lowercased: `'roll'` or `'r'`. They are
|
|
822
|
+
equal on a canonical call. Code that checks `ctx.command === 'roll'` keeps working no matter
|
|
823
|
+
which spelling reached it.
|
|
824
|
+
|
|
825
|
+
The options object also takes `cooldown`, same as any other command. It is one bucket, keyed on
|
|
826
|
+
the canonical name, shared across every spelling:
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
bot.command('roll', { description: 'Roll dice', aliases: ['r'], cooldown: { rate: 1, per: 5 } }, handler);
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
At most three aliases per command. A fourth throws locally, before anything is synced. It is the
|
|
833
|
+
same validation path as a bad command name: lowercase letters, digits, hyphens and underscores
|
|
834
|
+
only, and none of the reserved names (`help`, `report`, `block`, `mute`, `kick`, `ban`, `admin`,
|
|
835
|
+
`staff`, `support`, `aurival`).
|
|
836
|
+
|
|
837
|
+
The cooldown notice echoes the typed token: hit the limit through `/r` and you get `Slow down.
|
|
838
|
+
Try /r again in 3 s.`, never `/roll`.
|
|
839
|
+
|
|
840
|
+
In the app, the command picker lists a command's aliases as a muted secondary line under its
|
|
841
|
+
row: `also /r`. Picking a row that matched by alias fills that alias, not the canonical
|
|
842
|
+
spelling: typing `/r` and picking the row inserts `/r `, not `/roll `.
|
|
843
|
+
|
|
689
844
|
## Environment
|
|
690
845
|
|
|
691
846
|
| variable | what it does |
|
package/dist/bot.d.ts
CHANGED
|
@@ -78,6 +78,19 @@ export interface CommandOptions {
|
|
|
78
78
|
description?: string | undefined;
|
|
79
79
|
cooldown?: CooldownLike | undefined;
|
|
80
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;
|
|
81
94
|
}
|
|
82
95
|
/**
|
|
83
96
|
* How long a command handler runs before the chat is told the bot is thinking
|
|
@@ -97,6 +110,17 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
|
|
|
97
110
|
machine: Machine;
|
|
98
111
|
paired: boolean;
|
|
99
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
|
+
}
|
|
100
124
|
/**
|
|
101
125
|
* The one door to command sync, so the lane that syncs is always the lane that
|
|
102
126
|
* reports. Both `Bot` call sites go through here.
|
|
@@ -104,10 +128,7 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
|
|
|
104
128
|
* Exported for the tests, not from `index.ts` — the package's export list
|
|
105
129
|
* mirrors Python's `__all__` (SDK-7) and this is not on it.
|
|
106
130
|
*/
|
|
107
|
-
export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload:
|
|
108
|
-
name: string;
|
|
109
|
-
description: string;
|
|
110
|
-
}>, log: Logger): Promise<void>;
|
|
131
|
+
export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload: SyncPayloadEntry[], log: Logger): Promise<void>;
|
|
111
132
|
/**
|
|
112
133
|
* One warning line per shadowed chat (S11). Silently dropping `conflicts` is
|
|
113
134
|
* the silence the field exists to end: the developer whose command never fires.
|
package/dist/bot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
|
package/dist/bot.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Auth, KeyFile, machineLabel, pair, resolveHost } from './auth.js';
|
|
|
3
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 { commandCooldownNotice } from './caps.js';
|
|
6
|
+
import { CAP_TOO_MANY_ALIASES, MAX_ALIASES_PER_COMMAND, commandCooldownNotice } from './caps.js';
|
|
7
7
|
import { Cooldown, buttonBucketKey, normalizeCooldownOption, resolveButtonCooldown, retryAfterMs, roundSeconds, subjectKey, } from './cooldown.js';
|
|
8
8
|
import { EVENT_BACKLOG_OVERFLOWED, EVENT_COMMAND_INVOKED, Socket } from './socket.js';
|
|
9
9
|
import * as status from './status.js';
|
|
@@ -139,6 +139,7 @@ export class Bot {
|
|
|
139
139
|
let handler;
|
|
140
140
|
let cooldownLike;
|
|
141
141
|
let onCooldownHook;
|
|
142
|
+
let aliasesLike;
|
|
142
143
|
if (typeof second === 'string') {
|
|
143
144
|
description = second;
|
|
144
145
|
handler = third;
|
|
@@ -150,14 +151,21 @@ export class Bot {
|
|
|
150
151
|
description = second.description ?? '';
|
|
151
152
|
cooldownLike = second.cooldown;
|
|
152
153
|
onCooldownHook = second.onCooldown;
|
|
154
|
+
aliasesLike = second.aliases;
|
|
153
155
|
handler = third;
|
|
154
156
|
}
|
|
155
157
|
if (handler === undefined)
|
|
156
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
|
+
}
|
|
157
165
|
const key = lookupKey(name);
|
|
158
166
|
if (this.#registered.has(key))
|
|
159
167
|
status.duplicateCommand(name, this.#quiet);
|
|
160
|
-
const registered = { command: { name, description }, handler };
|
|
168
|
+
const registered = { command: { name, description, aliases }, handler };
|
|
161
169
|
if (cooldownLike !== undefined)
|
|
162
170
|
registered.cooldown = Cooldown.from(cooldownLike);
|
|
163
171
|
if (onCooldownHook !== undefined)
|
|
@@ -285,10 +293,16 @@ export class Bot {
|
|
|
285
293
|
* and land the sync in the background.
|
|
286
294
|
*/
|
|
287
295
|
async #syncCommands(http, bot, signal) {
|
|
288
|
-
const payload = [...this.#registered.values()].map((r) =>
|
|
289
|
-
name: r.command.name,
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
});
|
|
292
306
|
try {
|
|
293
307
|
await syncCommandsAndReport(http, bot, payload, this.#log);
|
|
294
308
|
return null;
|
|
@@ -424,7 +438,11 @@ export class Bot {
|
|
|
424
438
|
}
|
|
425
439
|
const n = roundSeconds(refusedAfterSeconds);
|
|
426
440
|
try {
|
|
427
|
-
|
|
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));
|
|
428
446
|
}
|
|
429
447
|
catch (exc) {
|
|
430
448
|
this.#log.error(`cooldown notice for ${JSON.stringify(ctx.command)} failed: ${exc instanceof Error ? (exc.stack ?? exc.message) : String(exc)}`);
|
|
@@ -468,6 +486,14 @@ export class Bot {
|
|
|
468
486
|
return;
|
|
469
487
|
try {
|
|
470
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}`);
|
|
471
497
|
}
|
|
472
498
|
catch (exc) {
|
|
473
499
|
if (exc instanceof ButtonAlreadyUsed || exc instanceof NotFound) {
|