agent-messenger 2.38.0 → 2.39.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/dist/package.json +1 -1
- package/dist/src/platforms/discordbot/client-message.d.ts +15 -0
- package/dist/src/platforms/discordbot/client-message.d.ts.map +1 -0
- package/dist/src/platforms/discordbot/client-message.js +70 -0
- package/dist/src/platforms/discordbot/client-message.js.map +1 -0
- package/dist/src/platforms/discordbot/client-threads.d.ts +10 -0
- package/dist/src/platforms/discordbot/client-threads.d.ts.map +1 -0
- package/dist/src/platforms/discordbot/client-threads.js +31 -0
- package/dist/src/platforms/discordbot/client-threads.js.map +1 -0
- package/dist/src/platforms/discordbot/client.d.ts +14 -2
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +21 -28
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/file.d.ts +14 -10
- package/dist/src/platforms/discordbot/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/file.js +58 -24
- package/dist/src/platforms/discordbot/commands/file.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts +3 -9
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +9 -51
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/reaction.d.ts +9 -0
- package/dist/src/platforms/discordbot/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/reaction.js +39 -0
- package/dist/src/platforms/discordbot/commands/reaction.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/shared.d.ts +36 -0
- package/dist/src/platforms/discordbot/commands/shared.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/shared.js +36 -0
- package/dist/src/platforms/discordbot/commands/shared.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/thread.d.ts +14 -0
- package/dist/src/platforms/discordbot/commands/thread.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/thread.js +40 -0
- package/dist/src/platforms/discordbot/commands/thread.js.map +1 -1
- package/dist/src/platforms/discordbot/index.d.ts +1 -1
- package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/index.js.map +1 -1
- package/dist/src/platforms/discordbot/types.d.ts +68 -17
- package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/types.js +20 -16
- package/dist/src/platforms/discordbot/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +4 -32
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/sync-state-store.d.ts +11 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.js +89 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.js.map +1 -0
- package/dist/src/platforms/slack/cli.d.ts.map +1 -1
- package/dist/src/platforms/slack/cli.js +5 -0
- package/dist/src/platforms/slack/cli.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +9 -3
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/credential-manager.d.ts +2 -0
- package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/slack/credential-manager.js +17 -3
- package/dist/src/platforms/slack/credential-manager.js.map +1 -1
- package/docs/content/docs/cli/discordbot.mdx +60 -27
- package/docs/content/docs/cli/slack.mdx +8 -1
- package/docs/content/docs/sdk/discordbot.mdx +101 -3
- package/e2e/README.md +3 -1
- package/e2e/config.ts +1 -1
- package/e2e/discordbot.e2e.test.ts +206 -11
- 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 +1 -1
- package/skills/agent-discordbot/SKILL.md +57 -11
- package/skills/agent-discordbot/references/authentication.md +13 -11
- package/skills/agent-discordbot/references/common-patterns.md +36 -8
- 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 +9 -2
- package/skills/agent-slack/references/authentication.md +12 -0
- package/skills/agent-slack/references/common-patterns.md +18 -0
- 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/discordbot/client-message.ts +91 -0
- package/src/platforms/discordbot/client-threads.test.ts +177 -0
- package/src/platforms/discordbot/client-threads.ts +69 -0
- package/src/platforms/discordbot/client.test.ts +277 -1
- package/src/platforms/discordbot/client.ts +60 -40
- package/src/platforms/discordbot/commands/file.test.ts +242 -90
- package/src/platforms/discordbot/commands/file.ts +78 -36
- package/src/platforms/discordbot/commands/message.test.ts +183 -26
- package/src/platforms/discordbot/commands/message.ts +13 -61
- package/src/platforms/discordbot/commands/reaction.test.ts +73 -1
- package/src/platforms/discordbot/commands/reaction.ts +46 -0
- package/src/platforms/discordbot/commands/shared.test.ts +98 -0
- package/src/platforms/discordbot/commands/shared.ts +67 -0
- package/src/platforms/discordbot/commands/thread.test.ts +58 -1
- package/src/platforms/discordbot/commands/thread.ts +58 -0
- package/src/platforms/discordbot/index.ts +3 -0
- package/src/platforms/discordbot/types.test.ts +46 -0
- package/src/platforms/discordbot/types.ts +44 -19
- package/src/platforms/kakaotalk/client-listener-integration.test.ts +21 -3
- package/src/platforms/kakaotalk/client.test.ts +42 -4
- package/src/platforms/kakaotalk/client.ts +4 -39
- package/src/platforms/kakaotalk/sync-state-store.test.ts +197 -0
- package/src/platforms/kakaotalk/sync-state-store.ts +99 -0
- package/src/platforms/slack/cli.test.ts +111 -1
- package/src/platforms/slack/cli.ts +5 -0
- package/src/platforms/slack/commands/auth.ts +14 -3
- package/src/platforms/slack/credential-manager.ts +21 -3
- package/src/platforms/slack/credential-manager.workspace-select.test.ts +70 -0
|
@@ -47,6 +47,21 @@ describe('DiscordBotClient', () => {
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const messagePage = (startIndex: number, count: number) =>
|
|
51
|
+
Array.from({ length: count }, (_, offset) => {
|
|
52
|
+
const index = startIndex + offset
|
|
53
|
+
return {
|
|
54
|
+
id: `msg${index}`,
|
|
55
|
+
channel_id: 'ch1',
|
|
56
|
+
author: { id: '123', username: 'user1' },
|
|
57
|
+
content: '',
|
|
58
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
59
|
+
attachments: [
|
|
60
|
+
{ id: `att${index}`, filename: `file${index}.txt`, size: 10, url: `https://example.com/file${index}.txt` },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
50
65
|
describe('constructor', () => {
|
|
51
66
|
it('requires token', async () => {
|
|
52
67
|
await expect(new DiscordBotClient().login({ token: '' })).rejects.toThrow(DiscordBotError)
|
|
@@ -89,6 +104,35 @@ describe('DiscordBotClient', () => {
|
|
|
89
104
|
})
|
|
90
105
|
})
|
|
91
106
|
|
|
107
|
+
describe('oversized upload errors', () => {
|
|
108
|
+
it('maps HTTP 413 with an empty response body', async () => {
|
|
109
|
+
fetchResponses.push(new Response(null, { status: 413 }))
|
|
110
|
+
|
|
111
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
112
|
+
await expect(client.testAuth()).rejects.toMatchObject({
|
|
113
|
+
code: 'http_413',
|
|
114
|
+
message: 'File(s) too large for this server upload limit (Discord error 40005)',
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('maps Discord error 40005 to the actionable upload message', async () => {
|
|
119
|
+
mockResponse({ code: 40005, message: 'Request entity too large' }, 400)
|
|
120
|
+
|
|
121
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
122
|
+
await expect(client.testAuth()).rejects.toMatchObject({
|
|
123
|
+
code: '40005',
|
|
124
|
+
message: 'File(s) too large for this server upload limit (Discord error 40005)',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('preserves unrelated permission errors', async () => {
|
|
129
|
+
mockResponse({ code: 50013, message: 'Missing Permissions' }, 403)
|
|
130
|
+
|
|
131
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
132
|
+
await expect(client.testAuth()).rejects.toMatchObject({ code: '50013', message: 'Missing Permissions' })
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
92
136
|
describe('listGuilds', () => {
|
|
93
137
|
it('returns list of guilds', async () => {
|
|
94
138
|
mockResponse([
|
|
@@ -165,7 +209,8 @@ describe('DiscordBotClient', () => {
|
|
|
165
209
|
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
166
210
|
await client.sendMessage('ch1', 'Thread reply', { thread_id: 'thread123' })
|
|
167
211
|
|
|
168
|
-
expect(fetchCalls[0].
|
|
212
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/thread123/messages')
|
|
213
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Thread reply' }))
|
|
169
214
|
})
|
|
170
215
|
|
|
171
216
|
it('includes message_reference when reply_to is provided', async () => {
|
|
@@ -186,6 +231,179 @@ describe('DiscordBotClient', () => {
|
|
|
186
231
|
})
|
|
187
232
|
})
|
|
188
233
|
|
|
234
|
+
describe('createMessage', () => {
|
|
235
|
+
it('sends files with text as multipart form data', async () => {
|
|
236
|
+
const { tmpdir } = await import('node:os')
|
|
237
|
+
const { join } = await import('node:path')
|
|
238
|
+
const tempFile = join(tmpdir(), 'report.pdf')
|
|
239
|
+
await Bun.write(tempFile, 'report contents')
|
|
240
|
+
|
|
241
|
+
mockResponse({
|
|
242
|
+
id: 'msg1',
|
|
243
|
+
channel_id: 'ch1',
|
|
244
|
+
author: { id: '123', username: 'bot' },
|
|
245
|
+
content: 'see attached',
|
|
246
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
247
|
+
attachments: [{ id: 'att1', filename: 'report.pdf', size: 15, url: 'https://example.com/report.pdf' }],
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
251
|
+
await client.createMessage('ch1', { content: 'see attached', files: [{ path: tempFile }] })
|
|
252
|
+
|
|
253
|
+
const body = fetchCalls[0].options?.body
|
|
254
|
+
expect(body).toBeInstanceOf(FormData)
|
|
255
|
+
const formData = body as FormData
|
|
256
|
+
expect(JSON.parse(formData.get('payload_json') as string)).toEqual({
|
|
257
|
+
content: 'see attached',
|
|
258
|
+
attachments: [{ id: 0, filename: 'report.pdf' }],
|
|
259
|
+
})
|
|
260
|
+
expect((formData.get('files[0]') as File).name).toBe('report.pdf')
|
|
261
|
+
const headers = fetchCalls[0].options?.headers as Record<string, string> | undefined
|
|
262
|
+
expect(headers?.['Content-Type']).toBeUndefined()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('preserves indexed multipart wire format for multiple files', async () => {
|
|
266
|
+
const { tmpdir } = await import('node:os')
|
|
267
|
+
const { join } = await import('node:path')
|
|
268
|
+
const firstPath = join(tmpdir(), 'a.txt')
|
|
269
|
+
const secondPath = join(tmpdir(), 'b.txt')
|
|
270
|
+
await Bun.write(firstPath, 'alpha')
|
|
271
|
+
await Bun.write(secondPath, 'bravo!')
|
|
272
|
+
mockResponse({ attachments: [] })
|
|
273
|
+
|
|
274
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
275
|
+
await client.createMessage('ch1', {
|
|
276
|
+
content: 'two files',
|
|
277
|
+
files: [{ path: firstPath }, { path: secondPath }],
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const body = fetchCalls[0].options?.body
|
|
281
|
+
expect(body).toBeInstanceOf(FormData)
|
|
282
|
+
const formData = body as FormData
|
|
283
|
+
expect(JSON.parse(formData.get('payload_json') as string)).toEqual({
|
|
284
|
+
content: 'two files',
|
|
285
|
+
attachments: [
|
|
286
|
+
{ id: 0, filename: 'a.txt' },
|
|
287
|
+
{ id: 1, filename: 'b.txt' },
|
|
288
|
+
],
|
|
289
|
+
})
|
|
290
|
+
expect((formData.get('files[0]') as File).name).toBe('a.txt')
|
|
291
|
+
expect((formData.get('files[0]') as File).size).toBe(5)
|
|
292
|
+
expect((formData.get('files[1]') as File).name).toBe('b.txt')
|
|
293
|
+
expect((formData.get('files[1]') as File).size).toBe(6)
|
|
294
|
+
const headers = fetchCalls[0].options?.headers as Record<string, string> | undefined
|
|
295
|
+
expect(headers?.['Content-Type']).toBeUndefined()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('normalizes Windows-style implicit filenames', async () => {
|
|
299
|
+
const { tmpdir } = await import('node:os')
|
|
300
|
+
const { join } = await import('node:path')
|
|
301
|
+
const tempFile = join(tmpdir(), 'C:\\dir\\x.txt')
|
|
302
|
+
await Bun.write(tempFile, 'content')
|
|
303
|
+
mockResponse({ attachments: [{ id: 'att1', filename: 'x.txt', size: 7, url: 'https://example.com' }] })
|
|
304
|
+
|
|
305
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
306
|
+
await client.createMessage('ch1', { files: [{ path: tempFile }] })
|
|
307
|
+
|
|
308
|
+
const formData = fetchCalls[0].options?.body as FormData
|
|
309
|
+
expect(JSON.parse(formData.get('payload_json') as string).attachments[0].filename).toBe('x.txt')
|
|
310
|
+
expect((formData.get('files[0]') as File).name).toBe('x.txt')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('uses a filename override for multipart parts', async () => {
|
|
314
|
+
const { tmpdir } = await import('node:os')
|
|
315
|
+
const { join } = await import('node:path')
|
|
316
|
+
const tempFile = join(tmpdir(), 'test-discordbot-create-message-original.txt')
|
|
317
|
+
await Bun.write(tempFile, 'content')
|
|
318
|
+
mockResponse({ attachments: [{ id: 'att1', filename: 'renamed.txt', size: 7, url: 'https://example.com' }] })
|
|
319
|
+
|
|
320
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
321
|
+
await client.createMessage('ch1', { files: [{ path: tempFile, filename: 'renamed.txt' }] })
|
|
322
|
+
|
|
323
|
+
const formData = fetchCalls[0].options?.body as FormData
|
|
324
|
+
expect(JSON.parse(formData.get('payload_json') as string).attachments[0].filename).toBe('renamed.txt')
|
|
325
|
+
expect((formData.get('files[0]') as File).name).toBe('renamed.txt')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('rejects invalid filenames before fetching', async () => {
|
|
329
|
+
const invalidFilenames = ['../evil', 'a/b', 'a\\b', '.', '..', '']
|
|
330
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
331
|
+
|
|
332
|
+
for (const filename of invalidFilenames) {
|
|
333
|
+
await expect(
|
|
334
|
+
client.createMessage('ch1', { content: 'x', files: [{ path: '/tmp/file', filename }] }),
|
|
335
|
+
).rejects.toMatchObject({ code: 'invalid_filename', message: 'Invalid filename' })
|
|
336
|
+
}
|
|
337
|
+
expect(fetchCalls).toHaveLength(0)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('rejects more than 10 files before fetching', async () => {
|
|
341
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
342
|
+
const files = Array.from({ length: 11 }, (_, index) => ({ path: `/tmp/file-${index}` }))
|
|
343
|
+
|
|
344
|
+
await expect(client.createMessage('ch1', { files })).rejects.toMatchObject({
|
|
345
|
+
code: 'too_many_files',
|
|
346
|
+
message: 'Discord allows at most 10 files per message',
|
|
347
|
+
})
|
|
348
|
+
expect(fetchCalls).toHaveLength(0)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('rejects an empty message before fetching', async () => {
|
|
352
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
353
|
+
|
|
354
|
+
await expect(client.createMessage('ch1', { content: ' ' })).rejects.toMatchObject({
|
|
355
|
+
code: 'empty_message',
|
|
356
|
+
message: 'Message must have content or at least one file',
|
|
357
|
+
})
|
|
358
|
+
expect(fetchCalls).toHaveLength(0)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('propagates missing file errors', async () => {
|
|
362
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
363
|
+
|
|
364
|
+
await expect(
|
|
365
|
+
client.createMessage('ch1', { content: 'x', files: [{ path: '/tmp/does-not-exist-ulw' }] }),
|
|
366
|
+
).rejects.toThrow(/ENOENT/)
|
|
367
|
+
expect(fetchCalls).toHaveLength(0)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('posts file messages into the requested thread', async () => {
|
|
371
|
+
const { tmpdir } = await import('node:os')
|
|
372
|
+
const { join } = await import('node:path')
|
|
373
|
+
const tempFile = join(tmpdir(), 'test-discordbot-thread-file.txt')
|
|
374
|
+
await Bun.write(tempFile, 'content')
|
|
375
|
+
mockResponse({
|
|
376
|
+
attachments: [{ id: 'att1', filename: 'test-discordbot-thread-file.txt', size: 7, url: 'https://example.com' }],
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
380
|
+
await client.createMessage('parent', { content: 'x', thread_id: 'thread123', files: [{ path: tempFile }] })
|
|
381
|
+
|
|
382
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/thread123/messages')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('sends replies and files together', async () => {
|
|
386
|
+
const { tmpdir } = await import('node:os')
|
|
387
|
+
const { join } = await import('node:path')
|
|
388
|
+
const tempFile = join(tmpdir(), 'test-discordbot-reply-file.txt')
|
|
389
|
+
await Bun.write(tempFile, 'content')
|
|
390
|
+
mockResponse({
|
|
391
|
+
attachments: [{ id: 'att1', filename: 'test-discordbot-reply-file.txt', size: 7, url: 'https://example.com' }],
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
395
|
+
await client.createMessage('ch1', { content: 'reply', reply_to: 'parent123', files: [{ path: tempFile }] })
|
|
396
|
+
|
|
397
|
+
const formData = fetchCalls[0].options?.body as FormData
|
|
398
|
+
expect(JSON.parse(formData.get('payload_json') as string)).toEqual({
|
|
399
|
+
content: 'reply',
|
|
400
|
+
message_reference: { message_id: 'parent123' },
|
|
401
|
+
attachments: [{ id: 0, filename: 'test-discordbot-reply-file.txt' }],
|
|
402
|
+
})
|
|
403
|
+
expect(formData.get('files[0]')).toBeTruthy()
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
189
407
|
describe('editMessage', () => {
|
|
190
408
|
it('edits message with PATCH', async () => {
|
|
191
409
|
mockResponse({
|
|
@@ -355,6 +573,64 @@ describe('DiscordBotClient', () => {
|
|
|
355
573
|
expect(files).toHaveLength(1)
|
|
356
574
|
expect(files[0].filename).toBe('file1.txt')
|
|
357
575
|
})
|
|
576
|
+
|
|
577
|
+
it('requests a single page even when the page is full', async () => {
|
|
578
|
+
mockResponse(messagePage(0, 100))
|
|
579
|
+
|
|
580
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
581
|
+
const files = await client.listFiles('ch1')
|
|
582
|
+
|
|
583
|
+
expect(files).toHaveLength(100)
|
|
584
|
+
expect(fetchCalls).toHaveLength(1)
|
|
585
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/ch1/messages?limit=100')
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe('findFile', () => {
|
|
590
|
+
it('stops after the first page when the file is in the newest messages', async () => {
|
|
591
|
+
mockResponse(messagePage(0, 100))
|
|
592
|
+
|
|
593
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
594
|
+
const file = await client.findFile('ch1', 'att0')
|
|
595
|
+
|
|
596
|
+
expect(file?.filename).toBe('file0.txt')
|
|
597
|
+
expect(fetchCalls).toHaveLength(1)
|
|
598
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/ch1/messages?limit=100')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('follows the before cursor to reach an older page', async () => {
|
|
602
|
+
mockResponse(messagePage(0, 100))
|
|
603
|
+
mockResponse(messagePage(100, 40))
|
|
604
|
+
|
|
605
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
606
|
+
const file = await client.findFile('ch1', 'att120')
|
|
607
|
+
|
|
608
|
+
expect(file?.filename).toBe('file120.txt')
|
|
609
|
+
expect(fetchCalls).toHaveLength(2)
|
|
610
|
+
expect(fetchCalls[1].url).toBe('https://discord.com/api/v10/channels/ch1/messages?limit=100&before=msg99')
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('stops at the end of the channel history when the file is missing', async () => {
|
|
614
|
+
mockResponse(messagePage(0, 100))
|
|
615
|
+
mockResponse(messagePage(100, 3))
|
|
616
|
+
|
|
617
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
618
|
+
const file = await client.findFile('ch1', 'missing')
|
|
619
|
+
|
|
620
|
+
expect(file).toBeUndefined()
|
|
621
|
+
expect(fetchCalls).toHaveLength(2)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('gives up after the bounded page budget', async () => {
|
|
625
|
+
for (let page = 0; page < 12; page += 1) mockResponse(messagePage(page * 100, 100))
|
|
626
|
+
|
|
627
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
628
|
+
const file = await client.findFile('ch1', 'missing')
|
|
629
|
+
|
|
630
|
+
expect(file).toBeUndefined()
|
|
631
|
+
expect(fetchCalls).toHaveLength(10)
|
|
632
|
+
expect(fetchCalls[9].url).toBe('https://discord.com/api/v10/channels/ch1/messages?limit=100&before=msg899')
|
|
633
|
+
})
|
|
358
634
|
})
|
|
359
635
|
|
|
360
636
|
describe('createThread', () => {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
createMessage as createMessageHelper,
|
|
3
|
+
findFile as findFileHelper,
|
|
4
|
+
listFiles as listFilesHelper,
|
|
5
|
+
uploadFile as uploadFileHelper,
|
|
6
|
+
} from './client-message'
|
|
7
|
+
import { listThreads as listThreadsHelper, resolveThread as resolveThreadHelper } from './client-threads'
|
|
8
|
+
import type {
|
|
9
|
+
DiscordChannel,
|
|
10
|
+
DiscordCreateMessageOptions,
|
|
11
|
+
DiscordFile,
|
|
12
|
+
DiscordGuild,
|
|
13
|
+
DiscordMessage,
|
|
14
|
+
DiscordUser,
|
|
15
|
+
} from './types'
|
|
4
16
|
import { DiscordBotError } from './types'
|
|
5
17
|
|
|
6
18
|
const BASE_URL = 'https://discord.com/api/v10'
|
|
@@ -105,6 +117,13 @@ export class DiscordBotClient {
|
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
private handleErrorResponse(response: Response, errorBody: Record<string, string | number>): never {
|
|
120
|
+
if (errorBody.code === 40005 || response.status === 413) {
|
|
121
|
+
throw new DiscordBotError(
|
|
122
|
+
'File(s) too large for this server upload limit (Discord error 40005)',
|
|
123
|
+
errorBody.code?.toString() ?? 'http_413',
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
throw new DiscordBotError(
|
|
109
128
|
(errorBody.message as string) || `HTTP ${response.status}`,
|
|
110
129
|
errorBody.code?.toString() || `http_${response.status}`,
|
|
@@ -249,19 +268,21 @@ export class DiscordBotClient {
|
|
|
249
268
|
return this.request<DiscordChannel>('GET', `/channels/${channelId}`)
|
|
250
269
|
}
|
|
251
270
|
|
|
271
|
+
async createMessage(channelId: string, options: DiscordCreateMessageOptions): Promise<DiscordMessage> {
|
|
272
|
+
return createMessageHelper(
|
|
273
|
+
(method, path, body) => this.request(method, path, body),
|
|
274
|
+
(path, formData) => this.requestFormData(path, formData),
|
|
275
|
+
channelId,
|
|
276
|
+
options,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
252
280
|
async sendMessage(
|
|
253
281
|
channelId: string,
|
|
254
282
|
content: string,
|
|
255
283
|
options?: { thread_id?: string; reply_to?: string },
|
|
256
284
|
): Promise<DiscordMessage> {
|
|
257
|
-
|
|
258
|
-
if (options?.thread_id) {
|
|
259
|
-
body.thread_id = options.thread_id
|
|
260
|
-
}
|
|
261
|
-
if (options?.reply_to) {
|
|
262
|
-
body.message_reference = { message_id: options.reply_to }
|
|
263
|
-
}
|
|
264
|
-
return this.request<DiscordMessage>('POST', `/channels/${channelId}/messages`, body)
|
|
285
|
+
return this.createMessage(channelId, { content, thread_id: options?.thread_id, reply_to: options?.reply_to })
|
|
265
286
|
}
|
|
266
287
|
|
|
267
288
|
async getMessages(channelId: string, limit: number = 50): Promise<DiscordMessage[]> {
|
|
@@ -302,39 +323,25 @@ export class DiscordBotClient {
|
|
|
302
323
|
return this.request<DiscordUser>('GET', `/users/${userId}`)
|
|
303
324
|
}
|
|
304
325
|
|
|
305
|
-
async uploadFile(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const first = message.attachments?.[0]
|
|
318
|
-
if (!first) {
|
|
319
|
-
throw new DiscordBotError('Upload succeeded but no attachments returned', 'no_attachments')
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return first
|
|
326
|
+
async uploadFile(
|
|
327
|
+
channelId: string,
|
|
328
|
+
filePath: string,
|
|
329
|
+
options?: { content?: string; filename?: string; reply_to?: string; thread_id?: string },
|
|
330
|
+
): Promise<DiscordFile> {
|
|
331
|
+
return uploadFileHelper(
|
|
332
|
+
(id, messageOptions) => this.createMessage(id, messageOptions),
|
|
333
|
+
channelId,
|
|
334
|
+
filePath,
|
|
335
|
+
options,
|
|
336
|
+
)
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
async listFiles(channelId: string): Promise<DiscordFile[]> {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
const messages = await this.request<MessageWithAttachments[]>('GET', `/channels/${channelId}/messages?limit=100`)
|
|
340
|
+
return listFilesHelper((method, path, body) => this.request(method, path, body), channelId)
|
|
341
|
+
}
|
|
330
342
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (msg.attachments && msg.attachments.length > 0) {
|
|
334
|
-
files.push(...msg.attachments)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return files
|
|
343
|
+
async findFile(channelId: string, fileId: string): Promise<DiscordFile | undefined> {
|
|
344
|
+
return findFileHelper((method, path, body) => this.request(method, path, body), channelId, fileId)
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
async createThread(
|
|
@@ -372,4 +379,17 @@ export class DiscordBotClient {
|
|
|
372
379
|
}
|
|
373
380
|
return found.id
|
|
374
381
|
}
|
|
382
|
+
|
|
383
|
+
async listThreads(guildId: string, options?: { parentId?: string; archived?: boolean }) {
|
|
384
|
+
return listThreadsHelper({ request: (method, path, body) => this.request(method, path, body) }, guildId, options)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async resolveThread(guildId: string, parentChannelId: string, thread: string) {
|
|
388
|
+
return resolveThreadHelper(
|
|
389
|
+
{ request: (method, path, body) => this.request(method, path, body) },
|
|
390
|
+
guildId,
|
|
391
|
+
parentChannelId,
|
|
392
|
+
thread,
|
|
393
|
+
)
|
|
394
|
+
}
|
|
375
395
|
}
|