agent-messenger 2.37.2 → 2.38.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/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/cli.d.ts.map +1 -1
- package/dist/src/platforms/discord/cli.js +3 -1
- package/dist/src/platforms/discord/cli.js.map +1 -1
- package/dist/src/platforms/discord/client.d.ts +17 -1
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +46 -0
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/emoji.d.ts +13 -0
- package/dist/src/platforms/discord/commands/emoji.d.ts.map +1 -0
- package/dist/src/platforms/discord/commands/emoji.js +93 -0
- package/dist/src/platforms/discord/commands/emoji.js.map +1 -0
- package/dist/src/platforms/discord/commands/index.d.ts +2 -0
- package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/index.js +2 -0
- package/dist/src/platforms/discord/commands/index.js.map +1 -1
- package/dist/src/platforms/discord/commands/server.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/server.js +16 -1
- package/dist/src/platforms/discord/commands/server.js.map +1 -1
- package/dist/src/platforms/discord/commands/sticker.d.ts +15 -0
- package/dist/src/platforms/discord/commands/sticker.d.ts.map +1 -0
- package/dist/src/platforms/discord/commands/sticker.js +97 -0
- package/dist/src/platforms/discord/commands/sticker.js.map +1 -0
- package/dist/src/platforms/discord/expression-format.d.ts +41 -0
- package/dist/src/platforms/discord/expression-format.d.ts.map +1 -0
- package/dist/src/platforms/discord/expression-format.js +107 -0
- package/dist/src/platforms/discord/expression-format.js.map +1 -0
- package/dist/src/platforms/discord/expression-names.d.ts +9 -0
- package/dist/src/platforms/discord/expression-names.d.ts.map +1 -0
- package/dist/src/platforms/discord/expression-names.js +33 -0
- package/dist/src/platforms/discord/expression-names.js.map +1 -0
- package/dist/src/platforms/discord/index.d.ts +1 -1
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +29 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/docs/content/docs/cli/discord.mdx +73 -0
- package/docs/content/docs/sdk/discord.mdx +50 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +69 -1
- package/skills/agent-discord/references/common-patterns.md +77 -0
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/cli.ts +4 -0
- package/src/platforms/discord/client.test.ts +133 -0
- package/src/platforms/discord/client.ts +72 -0
- package/src/platforms/discord/commands/emoji.test.ts +122 -0
- package/src/platforms/discord/commands/emoji.ts +124 -0
- package/src/platforms/discord/commands/index.ts +2 -0
- package/src/platforms/discord/commands/server.test.ts +45 -1
- package/src/platforms/discord/commands/server.ts +21 -1
- package/src/platforms/discord/commands/sticker.test.ts +118 -0
- package/src/platforms/discord/commands/sticker.ts +135 -0
- package/src/platforms/discord/expression-format.test.ts +107 -0
- package/src/platforms/discord/expression-format.ts +117 -0
- package/src/platforms/discord/expression-names.test.ts +41 -0
- package/src/platforms/discord/expression-names.ts +36 -0
- package/src/platforms/discord/index.ts +2 -0
- package/src/platforms/discord/types.ts +31 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-channeltalk
|
|
3
3
|
description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.38.0
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-discord
|
|
3
3
|
description: Interact with Discord servers - send messages, read channels, manage reactions
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.38.0
|
|
5
5
|
allowed-tools: Bash(agent-discord:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -241,6 +241,9 @@ agent-discord server switch <server-id>
|
|
|
241
241
|
agent-discord server current
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
+
`server info` also reports the boost tier with the custom emoji and sticker
|
|
245
|
+
counts and the slots still free at that tier.
|
|
246
|
+
|
|
244
247
|
### User Commands
|
|
245
248
|
|
|
246
249
|
```bash
|
|
@@ -383,6 +386,54 @@ agent-discord file list <channel-id>
|
|
|
383
386
|
agent-discord file info <channel-id> <file-id>
|
|
384
387
|
```
|
|
385
388
|
|
|
389
|
+
### Emoji Commands
|
|
390
|
+
|
|
391
|
+
Creating needs `CREATE_GUILD_EXPRESSIONS`; deleting someone else's expression
|
|
392
|
+
needs `MANAGE_GUILD_EXPRESSIONS`; listing needs neither. Name: letters, digits
|
|
393
|
+
and underscores only, 2–32 characters. Image at most 256KB.
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# List custom emoji
|
|
397
|
+
agent-discord emoji list <server-id>
|
|
398
|
+
|
|
399
|
+
# Upload (name defaults to the filename without extension)
|
|
400
|
+
agent-discord emoji create <server-id> <path>
|
|
401
|
+
agent-discord emoji create 1234567890123456789 ./potato.png --name potato
|
|
402
|
+
|
|
403
|
+
# Delete
|
|
404
|
+
agent-discord emoji delete <server-id> <emoji-id>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Sticker Commands
|
|
408
|
+
|
|
409
|
+
Same permissions as emoji. PNG, APNG, GIF or Lottie JSON, at most 512KB. Name
|
|
410
|
+
2–30 characters. Lottie needs a `VERIFIED` or `PARTNERED` guild. Discord
|
|
411
|
+
documents 320x320 but accepted a 408x408 PNG, so the CLI does not enforce a
|
|
412
|
+
size — it checks the format from the file's bytes instead of its extension.
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# List custom stickers
|
|
416
|
+
agent-discord sticker list <server-id>
|
|
417
|
+
|
|
418
|
+
# Upload — --tags is the unicode emoji the sticker relates to
|
|
419
|
+
agent-discord sticker create <server-id> <path> --tags <emoji>
|
|
420
|
+
agent-discord sticker create 1234567890123456789 ./potato.png --tags 🥔 --name potato
|
|
421
|
+
|
|
422
|
+
# Delete
|
|
423
|
+
agent-discord sticker delete <server-id> <sticker-id>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The media type is read from the file's bytes, not its extension, and each
|
|
427
|
+
command takes only its endpoint's formats — emoji accepts PNG, GIF, JPEG and
|
|
428
|
+
WebP; stickers accept PNG, APNG, GIF and a JSON document for Lottie. Anything
|
|
429
|
+
else fails locally with a named error before a request is sent, so a batch says
|
|
430
|
+
which file was wrong instead of returning a bare `Invalid Asset`.
|
|
431
|
+
|
|
432
|
+
Check the remaining slots before a batch upload — `agent-discord server info
|
|
433
|
+
<server-id>` reports the boost tier with the counts and free slots. Discord
|
|
434
|
+
raises both allowances with the boost level: static emoji 50/100/150/250 and
|
|
435
|
+
stickers 5/15/30/60 for tiers 0–3.
|
|
436
|
+
|
|
386
437
|
### Snapshot Command
|
|
387
438
|
|
|
388
439
|
Get server overview for AI agents (brief by default):
|
|
@@ -480,6 +531,23 @@ Common errors:
|
|
|
480
531
|
- `No current server set`: Run `server switch <id>` first
|
|
481
532
|
- `Message not found`: Invalid message ID
|
|
482
533
|
- `Unknown Channel`: Invalid channel ID
|
|
534
|
+
- `File is not one of ...`: The file's bytes match no format this endpoint
|
|
535
|
+
takes. Refused locally — the request was never sent, so retrying is pointless.
|
|
536
|
+
- `File is a <format>; this endpoint takes ...`: The file is a real image, but
|
|
537
|
+
not one this endpoint accepts — a JPEG or WebP sticker, or a JSON emoji.
|
|
538
|
+
Convert it; also refused locally.
|
|
539
|
+
- `Emoji name may only contain ...` / `... name must be at least 2 characters`:
|
|
540
|
+
The name broke Discord's rules and was refused locally. Names come from the
|
|
541
|
+
filename unless `--name` is given, so `potato-13.png` fails on the hyphen.
|
|
542
|
+
- `JSON file is not a Lottie animation (no layers array)` / `... is not valid
|
|
543
|
+
JSON` / `... is not valid UTF-8`: A JSON sticker that cannot be an animation.
|
|
544
|
+
Each names its own fault, because a file can carry a `layers` array and still
|
|
545
|
+
be broken. All three are refused locally; Discord answers such a file with a
|
|
546
|
+
bare `Invalid Asset`.
|
|
547
|
+
- `Invalid Asset` (from Discord): The format was accepted locally but Discord
|
|
548
|
+
refused the content itself — a truncated or corrupt image.
|
|
549
|
+
- `Maximum number of stickers reached (N)` (from Discord): No slots left. Check
|
|
550
|
+
`server info` for the tier's allowance before a batch.
|
|
483
551
|
|
|
484
552
|
## Configuration
|
|
485
553
|
|
|
@@ -333,6 +333,83 @@ agent-discord channel list
|
|
|
333
333
|
|
|
334
334
|
**When to use**: Managing multiple servers, cross-server operations.
|
|
335
335
|
|
|
336
|
+
## Pattern 11: Bulk Upload Custom Emoji
|
|
337
|
+
|
|
338
|
+
**Use case**: Register a folder of images as server emoji without running out of slots halfway through
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
#!/bin/bash
|
|
342
|
+
|
|
343
|
+
SERVER_ID="123456789012345678"
|
|
344
|
+
|
|
345
|
+
# Check the allowance first — Discord raises it with the boost level,
|
|
346
|
+
# so the free slots depend on premium_tier, not a fixed number.
|
|
347
|
+
INFO=$(agent-discord server info "$SERVER_ID")
|
|
348
|
+
FREE=$(echo "$INFO" | jq -r '.static_emoji_slots_remaining')
|
|
349
|
+
echo "Free emoji slots: $FREE"
|
|
350
|
+
|
|
351
|
+
UPLOADED=0
|
|
352
|
+
for FILE in ./emoji/*.png; do
|
|
353
|
+
if [ "$UPLOADED" -ge "$FREE" ]; then
|
|
354
|
+
echo "Out of slots — stopping. Remaining files were not uploaded."
|
|
355
|
+
break
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# The name defaults to the filename without its extension, and Discord only
|
|
359
|
+
# accepts letters, digits and underscores. Rename potato-01.png beforehand,
|
|
360
|
+
# or pass --name.
|
|
361
|
+
RESULT=$(agent-discord emoji create "$SERVER_ID" "$FILE")
|
|
362
|
+
|
|
363
|
+
if echo "$RESULT" | jq -e '.success' > /dev/null 2>&1; then
|
|
364
|
+
UPLOADED=$((UPLOADED + 1))
|
|
365
|
+
else
|
|
366
|
+
echo "Failed on $FILE: $(echo "$RESULT" | jq -r '.error')"
|
|
367
|
+
fi
|
|
368
|
+
done
|
|
369
|
+
|
|
370
|
+
echo "Uploaded $UPLOADED emoji"
|
|
371
|
+
agent-discord emoji list "$SERVER_ID" | jq '{count, static_count}'
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Notes**:
|
|
375
|
+
|
|
376
|
+
- `emoji create` rejects a name outside Discord's rules before the request, so a
|
|
377
|
+
bad filename names itself instead of returning `Invalid Form Body`.
|
|
378
|
+
- Roll back a mistake with `agent-discord emoji delete <server-id> <emoji-id>`.
|
|
379
|
+
|
|
380
|
+
## Pattern 12: Upload a Server Sticker
|
|
381
|
+
|
|
382
|
+
**Use case**: Add a sticker, which has a stricter format than emoji
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
#!/bin/bash
|
|
386
|
+
|
|
387
|
+
SERVER_ID="123456789012345678"
|
|
388
|
+
|
|
389
|
+
# Discord documents 320x320 for stickers (it accepted a larger PNG in
|
|
390
|
+
# practice, but stay on spec). Max 512KB.
|
|
391
|
+
sips -z 320 320 ./art/potato.png --out /tmp/potato_320.png
|
|
392
|
+
|
|
393
|
+
# --tags is the unicode emoji the sticker relates to, and is required.
|
|
394
|
+
RESULT=$(agent-discord sticker create "$SERVER_ID" /tmp/potato_320.png \
|
|
395
|
+
--tags 🥔 --name potato --description "A potato")
|
|
396
|
+
|
|
397
|
+
if echo "$RESULT" | jq -e '.success' > /dev/null 2>&1; then
|
|
398
|
+
echo "Sticker id: $(echo "$RESULT" | jq -r '.id')"
|
|
399
|
+
else
|
|
400
|
+
echo "Failed: $(echo "$RESULT" | jq -r '.error')"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
agent-discord sticker list "$SERVER_ID"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Notes**:
|
|
407
|
+
|
|
408
|
+
- Base sticker slots are 5, rising to 15/30/60 with boost tiers 1/2/3.
|
|
409
|
+
`agent-discord server info` reports `sticker_slots_remaining`.
|
|
410
|
+
- Sticker names are 2–30 characters. A one-character name is refused locally.
|
|
411
|
+
- Lottie JSON uploads only on `VERIFIED` or `PARTNERED` guilds.
|
|
412
|
+
|
|
336
413
|
## Best Practices
|
|
337
414
|
|
|
338
415
|
### 1. Always Get Channel IDs First
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-webexbot
|
|
3
3
|
description: Interact with Cisco Webex using bot tokens - send messages, reply in threads, upload and download files, look up people, read spaces, manage memberships, stream real-time events
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.38.0
|
|
5
5
|
allowed-tools: Bash(agent-webexbot:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
authCommand,
|
|
9
9
|
channelCommand,
|
|
10
10
|
dmCommand,
|
|
11
|
+
emojiCommand,
|
|
11
12
|
fileCommand,
|
|
12
13
|
friendCommand,
|
|
13
14
|
memberCommand,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
reactionCommand,
|
|
19
20
|
serverCommand,
|
|
20
21
|
snapshotCommand,
|
|
22
|
+
stickerCommand,
|
|
21
23
|
threadCommand,
|
|
22
24
|
userCommand,
|
|
23
25
|
whoamiCommand,
|
|
@@ -50,6 +52,7 @@ program.addCommand(authCommand)
|
|
|
50
52
|
program.addCommand(serverCommand)
|
|
51
53
|
program.addCommand(channelCommand)
|
|
52
54
|
program.addCommand(dmCommand)
|
|
55
|
+
program.addCommand(emojiCommand)
|
|
53
56
|
program.addCommand(fileCommand)
|
|
54
57
|
program.addCommand(friendCommand)
|
|
55
58
|
program.addCommand(memberCommand)
|
|
@@ -59,6 +62,7 @@ program.addCommand(noteCommand)
|
|
|
59
62
|
program.addCommand(profileCommand)
|
|
60
63
|
program.addCommand(reactionCommand)
|
|
61
64
|
program.addCommand(snapshotCommand)
|
|
65
|
+
program.addCommand(stickerCommand)
|
|
62
66
|
program.addCommand(threadCommand)
|
|
63
67
|
program.addCommand(userCommand)
|
|
64
68
|
program.addCommand(whoamiCommand)
|
|
@@ -1099,4 +1099,137 @@ describe('DiscordClient', () => {
|
|
|
1099
1099
|
expect(result.totalUnread).toBe(7)
|
|
1100
1100
|
})
|
|
1101
1101
|
})
|
|
1102
|
+
describe('expressions', () => {
|
|
1103
|
+
const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
1104
|
+
|
|
1105
|
+
it('listEmojis requests the guild emoji collection', async () => {
|
|
1106
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1107
|
+
mockResponse([{ id: 'e1', name: 'potato_01', animated: false }])
|
|
1108
|
+
|
|
1109
|
+
const emojis = await client.listEmojis('g1')
|
|
1110
|
+
|
|
1111
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/emojis')
|
|
1112
|
+
expect(fetchCalls[0].options?.method).toBe('GET')
|
|
1113
|
+
expect(emojis[0].name).toBe('potato_01')
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
it('createEmoji posts the image as a base64 data URI', async () => {
|
|
1117
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1118
|
+
mockResponse({ id: 'e1', name: 'potato_01', animated: false })
|
|
1119
|
+
|
|
1120
|
+
const emoji = await client.createEmoji('g1', 'potato_01', png, 'potato_01.png')
|
|
1121
|
+
|
|
1122
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/emojis')
|
|
1123
|
+
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
1124
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
1125
|
+
expect(body.name).toBe('potato_01')
|
|
1126
|
+
expect(body.image).toBe(`data:image/png;base64,${Buffer.from(png).toString('base64')}`)
|
|
1127
|
+
expect(emoji.id).toBe('e1')
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
it('deleteEmoji issues a DELETE for the emoji', async () => {
|
|
1131
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1132
|
+
mockResponse(null, 204)
|
|
1133
|
+
|
|
1134
|
+
await client.deleteEmoji('g1', 'e1')
|
|
1135
|
+
|
|
1136
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/emojis/e1')
|
|
1137
|
+
expect(fetchCalls[0].options?.method).toBe('DELETE')
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
it('listStickers requests the guild sticker collection', async () => {
|
|
1141
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1142
|
+
mockResponse([{ id: 's1', name: 'potato_01', tags: 'potato', type: 2, format_type: 1 }])
|
|
1143
|
+
|
|
1144
|
+
const stickers = await client.listStickers('g1')
|
|
1145
|
+
|
|
1146
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/stickers')
|
|
1147
|
+
expect(fetchCalls[0].options?.method).toBe('GET')
|
|
1148
|
+
expect(stickers[0].name).toBe('potato_01')
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
it('createSticker posts multipart fields alongside the file', async () => {
|
|
1152
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1153
|
+
mockResponse({ id: 's1', name: 'potato_01', tags: 'potato', type: 2, format_type: 1 })
|
|
1154
|
+
|
|
1155
|
+
const sticker = await client.createSticker(
|
|
1156
|
+
'g1',
|
|
1157
|
+
{ name: 'potato_01', description: 'a potato', tags: 'potato' },
|
|
1158
|
+
png,
|
|
1159
|
+
'potato_01.png',
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/stickers')
|
|
1163
|
+
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
1164
|
+
const form = fetchCalls[0].options?.body as FormData
|
|
1165
|
+
expect(form.get('name')).toBe('potato_01')
|
|
1166
|
+
expect(form.get('description')).toBe('a potato')
|
|
1167
|
+
expect(form.get('tags')).toBe('potato')
|
|
1168
|
+
expect((form.get('file') as File).name).toBe('potato_01.png')
|
|
1169
|
+
// Discord rejects a sticker part with no media type as "Invalid Asset".
|
|
1170
|
+
expect((form.get('file') as File).type).toBe('image/png')
|
|
1171
|
+
expect(sticker.id).toBe('s1')
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
it('createSticker types the part from the bytes, not the filename', async () => {
|
|
1175
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1176
|
+
mockResponse({ id: 's2', name: 'potato_02', tags: 'potato', type: 2, format_type: 4 })
|
|
1177
|
+
const gif = new Uint8Array([...Buffer.from('GIF89a'), 0, 0, 0, 0])
|
|
1178
|
+
|
|
1179
|
+
// The filename claims PNG; the bytes are a GIF. Discord answers a part
|
|
1180
|
+
// whose type does not match its content with "Invalid Asset".
|
|
1181
|
+
await client.createSticker('g1', { name: 'potato_02', tags: 'potato' }, gif, 'potato_02.png')
|
|
1182
|
+
|
|
1183
|
+
const form = fetchCalls[0].options?.body as FormData
|
|
1184
|
+
expect((form.get('file') as File).type).toBe('image/gif')
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
it('createSticker refuses bytes that are not a supported format', async () => {
|
|
1188
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1189
|
+
const junk = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
|
|
1190
|
+
|
|
1191
|
+
await expect(
|
|
1192
|
+
client.createSticker('g1', { name: 'potato_02', tags: 'potato' }, junk, 'potato_02.png'),
|
|
1193
|
+
).rejects.toThrow('not one of')
|
|
1194
|
+
expect(fetchCalls).toHaveLength(0)
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
it('createSticker refuses a JPEG, which the sticker endpoint does not take', async () => {
|
|
1198
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1199
|
+
const jpeg = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0])
|
|
1200
|
+
|
|
1201
|
+
await expect(
|
|
1202
|
+
client.createSticker('g1', { name: 'potato_02', tags: 'potato' }, jpeg, 'potato_02.jpg'),
|
|
1203
|
+
).rejects.toThrow('File is a jpeg')
|
|
1204
|
+
expect(fetchCalls).toHaveLength(0)
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
it('createEmoji refuses a JSON document, which the emoji endpoint does not take', async () => {
|
|
1208
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1209
|
+
const json = new Uint8Array(Buffer.from('{"v":"5.5.7"}'))
|
|
1210
|
+
|
|
1211
|
+
await expect(client.createEmoji('g1', 'potato_01', json, 'potato_01.json')).rejects.toThrow('File is a json')
|
|
1212
|
+
expect(fetchCalls).toHaveLength(0)
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
it('createSticker refuses JSON that is not a Lottie animation', async () => {
|
|
1216
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1217
|
+
const empty = new Uint8Array(Buffer.from('{}'))
|
|
1218
|
+
|
|
1219
|
+
await expect(
|
|
1220
|
+
client.createSticker('g1', { name: 'potato_02', tags: 'potato' }, empty, 'potato_02.json'),
|
|
1221
|
+
).rejects.toThrow('not a Lottie animation')
|
|
1222
|
+
expect(fetchCalls).toHaveLength(0)
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
it('deleteSticker issues a DELETE for the sticker', async () => {
|
|
1226
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
1227
|
+
mockResponse(null, 204)
|
|
1228
|
+
|
|
1229
|
+
await client.deleteSticker('g1', 's1')
|
|
1230
|
+
|
|
1231
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/guilds/g1/stickers/s1')
|
|
1232
|
+
expect(fetchCalls[0].options?.method).toBe('DELETE')
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1102
1235
|
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
|
|
3
|
+
import type { ExpressionFormat } from './expression-format'
|
|
4
|
+
import { EMOJI_FORMATS, STICKER_FORMATS, lottieError, mediaTypeOf, sniffFormat } from './expression-format'
|
|
3
5
|
import { getDiscordHeaders } from './super-properties'
|
|
4
6
|
import { DiscordSearchIndexNotReadyResponseSchema, DiscordSearchResponseSchema } from './types'
|
|
5
7
|
import type {
|
|
6
8
|
DiscordChannel,
|
|
7
9
|
DiscordDMChannel,
|
|
10
|
+
DiscordEmoji,
|
|
8
11
|
DiscordFile,
|
|
9
12
|
DiscordGuild,
|
|
10
13
|
DiscordGuildMember,
|
|
@@ -16,6 +19,7 @@ import type {
|
|
|
16
19
|
DiscordRole,
|
|
17
20
|
DiscordSearchOptions,
|
|
18
21
|
DiscordSearchResult,
|
|
22
|
+
DiscordSticker,
|
|
19
23
|
DiscordUnreadDM,
|
|
20
24
|
DiscordUnreadDMsResult,
|
|
21
25
|
DiscordUnreadMention,
|
|
@@ -95,6 +99,7 @@ interface RateLimitBucket {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
const BASE_URL = 'https://discord.com/api/v10'
|
|
102
|
+
|
|
98
103
|
const MAX_RETRIES = 3
|
|
99
104
|
const BASE_BACKOFF_MS = 100
|
|
100
105
|
const MAX_SEARCH_INDEX_RETRY_MS = 30_000
|
|
@@ -384,6 +389,73 @@ export class DiscordClient {
|
|
|
384
389
|
return files
|
|
385
390
|
}
|
|
386
391
|
|
|
392
|
+
/**
|
|
393
|
+
* The media type the part declares comes from the bytes, never the filename,
|
|
394
|
+
* and only from the formats the target endpoint actually takes — a JPEG
|
|
395
|
+
* declared to the sticker endpoint is refused by Discord as "Invalid Asset".
|
|
396
|
+
*/
|
|
397
|
+
private mediaTypeOrThrow(image: Uint8Array, allowed: ReadonlySet<ExpressionFormat>): string {
|
|
398
|
+
const format = sniffFormat(image)
|
|
399
|
+
if (!format || !allowed.has(format)) {
|
|
400
|
+
const names = [...allowed].join(', ')
|
|
401
|
+
throw new DiscordError(
|
|
402
|
+
format ? `File is a ${format}; this endpoint takes ${names}` : `File is not one of ${names}`,
|
|
403
|
+
'unsupported_asset',
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
if (format === 'json') {
|
|
407
|
+
const problem = lottieError(image)
|
|
408
|
+
if (problem) throw new DiscordError(problem, 'unsupported_asset')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return mediaTypeOf(format)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async listEmojis(guildId: string): Promise<DiscordEmoji[]> {
|
|
415
|
+
return this.request<DiscordEmoji[]>('GET', `/guilds/${guildId}/emojis`)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async createEmoji(
|
|
419
|
+
guildId: string,
|
|
420
|
+
name: string,
|
|
421
|
+
image: Uint8Array,
|
|
422
|
+
filename: string,
|
|
423
|
+
roles: string[] = [],
|
|
424
|
+
): Promise<DiscordEmoji> {
|
|
425
|
+
return this.request<DiscordEmoji>('POST', `/guilds/${guildId}/emojis`, {
|
|
426
|
+
name,
|
|
427
|
+
image: `data:${this.mediaTypeOrThrow(image, EMOJI_FORMATS)};base64,${Buffer.from(image).toString('base64')}`,
|
|
428
|
+
roles,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async deleteEmoji(guildId: string, emojiId: string): Promise<void> {
|
|
433
|
+
await this.request<void>('DELETE', `/guilds/${guildId}/emojis/${emojiId}`)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async listStickers(guildId: string): Promise<DiscordSticker[]> {
|
|
437
|
+
return this.request<DiscordSticker[]>('GET', `/guilds/${guildId}/stickers`)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async createSticker(
|
|
441
|
+
guildId: string,
|
|
442
|
+
fields: { name: string; description?: string; tags: string },
|
|
443
|
+
image: Uint8Array,
|
|
444
|
+
filename: string,
|
|
445
|
+
): Promise<DiscordSticker> {
|
|
446
|
+
const formData = new FormData()
|
|
447
|
+
formData.append('name', fields.name)
|
|
448
|
+
formData.append('description', fields.description ?? '')
|
|
449
|
+
formData.append('tags', fields.tags)
|
|
450
|
+
formData.append('file', new Blob([image], { type: this.mediaTypeOrThrow(image, STICKER_FORMATS) }), filename)
|
|
451
|
+
|
|
452
|
+
return this.requestFormData<DiscordSticker>(`/guilds/${guildId}/stickers`, formData)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async deleteSticker(guildId: string, stickerId: string): Promise<void> {
|
|
456
|
+
await this.request<void>('DELETE', `/guilds/${guildId}/stickers/${stickerId}`)
|
|
457
|
+
}
|
|
458
|
+
|
|
387
459
|
async listDMChannels(): Promise<DiscordDMChannel[]> {
|
|
388
460
|
return this.request<DiscordDMChannel[]>('GET', '/users/@me/channels')
|
|
389
461
|
}
|