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
|
@@ -1,15 +1,95 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { basename, join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import type { DiscordCreateMessageOptions, DiscordFile, DiscordMessage } from '../types'
|
|
8
|
+
|
|
9
|
+
const mockCreateMessage = mock(
|
|
10
|
+
async (channelId: string, options: DiscordCreateMessageOptions): Promise<DiscordMessage> => {
|
|
11
|
+
const files = options.files ?? []
|
|
12
|
+
const sizes = await Promise.all(files.map(async (file) => (await readFile(file.path)).length))
|
|
13
|
+
return {
|
|
14
|
+
id: 'msg-upload',
|
|
15
|
+
channel_id: channelId,
|
|
16
|
+
author: { id: 'bot-123', username: 'bot' },
|
|
17
|
+
content: options.content ?? '',
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
attachments: files.map((file, index) => ({
|
|
20
|
+
id: `att${index + 1}`,
|
|
21
|
+
filename: file.filename ?? basename(file.path),
|
|
22
|
+
size: sizes[index],
|
|
23
|
+
url: `https://cdn.discord.com/attachments/${channelId}/${file.filename ?? basename(file.path)}`,
|
|
24
|
+
})),
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const mockListFiles = mock(
|
|
30
|
+
(_channelId: string): Promise<DiscordFile[]> =>
|
|
31
|
+
Promise.resolve([
|
|
32
|
+
{ id: 'att1', filename: 'test.txt', size: 12, url: 'https://cdn.discord.com/test.txt' },
|
|
33
|
+
{
|
|
34
|
+
id: 'att2',
|
|
35
|
+
filename: 'report.pdf',
|
|
36
|
+
size: 2048,
|
|
37
|
+
url: 'https://cdn.discord.com/report.pdf',
|
|
38
|
+
content_type: 'application/pdf',
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const mockFindFile = mock((_channelId: string, fileId: string): Promise<DiscordFile | undefined> => {
|
|
44
|
+
if (fileId === 'att1') {
|
|
45
|
+
return Promise.resolve({ id: 'att1', filename: 'test.txt', size: 12, url: 'https://cdn.discord.com/test.txt' })
|
|
46
|
+
}
|
|
47
|
+
return Promise.resolve(undefined)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const mockResolveChannel = mock((_guildId: string, channel: string): Promise<string> => {
|
|
51
|
+
if (channel === 'general') return Promise.resolve('ch-general')
|
|
52
|
+
if (/^\d+$/.test(channel)) return Promise.resolve(channel)
|
|
53
|
+
return Promise.reject(new Error(`Channel not found: "${channel}". Use channel ID or exact channel name.`))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const mockResolveThread = mock((_guildId: string, _parentChannelId: string, thread: string): Promise<string> => {
|
|
57
|
+
if (thread === 'release-discussion') return Promise.resolve('thread-1')
|
|
58
|
+
return Promise.reject(new Error(`Thread not found: "${thread}".`))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
mock.module('../client', () => ({
|
|
62
|
+
DiscordBotClient: class MockDiscordBotClient {
|
|
63
|
+
async login(_credentials?: unknown) {
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
createMessage = mockCreateMessage
|
|
67
|
+
listFiles = mockListFiles
|
|
68
|
+
findFile = mockFindFile
|
|
69
|
+
resolveChannel = mockResolveChannel
|
|
70
|
+
resolveThread = mockResolveThread
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
2
73
|
|
|
3
74
|
import { DiscordBotCredentialManager } from '../credential-manager'
|
|
4
|
-
import { listAction, uploadAction } from './file'
|
|
75
|
+
import { infoAction, listAction, uploadAction } from './file'
|
|
5
76
|
import type { BotOption } from './shared'
|
|
6
77
|
|
|
7
78
|
describe('file commands', () => {
|
|
8
79
|
let mockCredManager: DiscordBotCredentialManager
|
|
9
80
|
let options: BotOption
|
|
10
|
-
|
|
81
|
+
let tempDir: string
|
|
82
|
+
let fileA: string
|
|
83
|
+
let fileB: string
|
|
84
|
+
|
|
85
|
+
beforeEach(async () => {
|
|
86
|
+
tempDir = join(tmpdir(), `discordbot-file-test-${Date.now()}`)
|
|
87
|
+
await mkdir(tempDir, { recursive: true })
|
|
88
|
+
fileA = join(tempDir, 'a.txt')
|
|
89
|
+
fileB = join(tempDir, 'b.txt')
|
|
90
|
+
await Bun.write(fileA, 'file-a')
|
|
91
|
+
await Bun.write(fileB, 'file-b')
|
|
11
92
|
|
|
12
|
-
beforeEach(() => {
|
|
13
93
|
mockCredManager = {
|
|
14
94
|
getCurrentServer: mock(async () => 'server-123'),
|
|
15
95
|
getCredentials: mock(async () => ({
|
|
@@ -19,47 +99,11 @@ describe('file commands', () => {
|
|
|
19
99
|
})),
|
|
20
100
|
} as unknown as DiscordBotCredentialManager
|
|
21
101
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
status: 200,
|
|
28
|
-
headers: {
|
|
29
|
-
'Content-Type': 'application/json',
|
|
30
|
-
'X-RateLimit-Remaining': '10',
|
|
31
|
-
'X-RateLimit-Reset': String(Date.now() / 1000 + 60),
|
|
32
|
-
'X-RateLimit-Bucket': 'test-bucket',
|
|
33
|
-
},
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (urlStr.includes('/channels/') && urlStr.includes('/messages')) {
|
|
38
|
-
return new Response(
|
|
39
|
-
JSON.stringify([
|
|
40
|
-
{
|
|
41
|
-
id: 'msg1',
|
|
42
|
-
channel_id: 'ch-general',
|
|
43
|
-
author: { id: 'bot-123', username: 'bot' },
|
|
44
|
-
content: '',
|
|
45
|
-
timestamp: new Date().toISOString(),
|
|
46
|
-
attachments: [{ id: 'att1', filename: 'test.txt', size: 12, url: 'https://cdn.discord.com/test.txt' }],
|
|
47
|
-
},
|
|
48
|
-
]),
|
|
49
|
-
{
|
|
50
|
-
status: 200,
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
'X-RateLimit-Remaining': '10',
|
|
54
|
-
'X-RateLimit-Reset': String(Date.now() / 1000 + 60),
|
|
55
|
-
'X-RateLimit-Bucket': 'test-bucket',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Response(JSON.stringify({ message: 'Not Found' }), { status: 404 })
|
|
62
|
-
}
|
|
102
|
+
mockCreateMessage.mockClear()
|
|
103
|
+
mockListFiles.mockClear()
|
|
104
|
+
mockFindFile.mockClear()
|
|
105
|
+
mockResolveChannel.mockClear()
|
|
106
|
+
mockResolveThread.mockClear()
|
|
63
107
|
|
|
64
108
|
options = {
|
|
65
109
|
_credManager: mockCredManager,
|
|
@@ -67,78 +111,186 @@ describe('file commands', () => {
|
|
|
67
111
|
})
|
|
68
112
|
|
|
69
113
|
afterEach(() => {
|
|
70
|
-
|
|
114
|
+
if (existsSync(tempDir)) {
|
|
115
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
116
|
+
}
|
|
71
117
|
})
|
|
72
118
|
|
|
73
119
|
describe('uploadAction', () => {
|
|
74
|
-
it('uploads file
|
|
75
|
-
const result = await uploadAction('general',
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
it('uploads a file and returns file, files, message_id, and channel_id', async () => {
|
|
121
|
+
const result = await uploadAction('general', [fileA], options)
|
|
122
|
+
|
|
123
|
+
expect(result.error).toBeUndefined()
|
|
124
|
+
expect(result.success).toBe(true)
|
|
125
|
+
expect(result.message_id).toBe('msg-upload')
|
|
126
|
+
expect(result.channel_id).toBe('ch-general')
|
|
127
|
+
expect(result.files).toHaveLength(1)
|
|
128
|
+
expect(result.file).toEqual(result.files?.[0])
|
|
129
|
+
expect(result.file?.filename).toBe('a.txt')
|
|
130
|
+
expect(result.file?.size).toBe(6)
|
|
131
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('ch-general', {
|
|
132
|
+
content: undefined,
|
|
133
|
+
files: [{ path: fileA, filename: undefined }],
|
|
134
|
+
reply_to: undefined,
|
|
78
135
|
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('uploads multiple files in one message', async () => {
|
|
139
|
+
const result = await uploadAction('general', [fileA, fileB], options)
|
|
79
140
|
|
|
80
|
-
expect(result).
|
|
141
|
+
expect(result.error).toBeUndefined()
|
|
142
|
+
expect(result.files).toHaveLength(2)
|
|
143
|
+
expect(result.files?.map((file) => file.filename)).toEqual(['a.txt', 'b.txt'])
|
|
144
|
+
expect(result.file).toEqual(result.files?.[0])
|
|
145
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('ch-general', {
|
|
146
|
+
content: undefined,
|
|
147
|
+
files: [
|
|
148
|
+
{ path: fileA, filename: undefined },
|
|
149
|
+
{ path: fileB, filename: undefined },
|
|
150
|
+
],
|
|
151
|
+
reply_to: undefined,
|
|
152
|
+
})
|
|
81
153
|
})
|
|
82
154
|
|
|
83
|
-
it('
|
|
84
|
-
const result = await uploadAction('
|
|
85
|
-
|
|
155
|
+
it('sends text with the files', async () => {
|
|
156
|
+
const result = await uploadAction('general', [fileA], { ...options, text: 'see attached' })
|
|
157
|
+
|
|
158
|
+
expect(result.error).toBeUndefined()
|
|
159
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('ch-general', {
|
|
160
|
+
content: 'see attached',
|
|
161
|
+
files: [{ path: fileA, filename: undefined }],
|
|
162
|
+
reply_to: undefined,
|
|
163
|
+
})
|
|
86
164
|
})
|
|
87
165
|
|
|
88
|
-
it('
|
|
89
|
-
const result = await uploadAction('general', '
|
|
90
|
-
|
|
91
|
-
|
|
166
|
+
it('uploads into a thread by name', async () => {
|
|
167
|
+
const result = await uploadAction('general', [fileA], { ...options, thread: 'release-discussion' })
|
|
168
|
+
|
|
169
|
+
expect(result.error).toBeUndefined()
|
|
170
|
+
expect(mockResolveThread).toHaveBeenCalledWith('server-123', 'ch-general', 'release-discussion')
|
|
171
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('thread-1', {
|
|
172
|
+
content: undefined,
|
|
173
|
+
files: [{ path: fileA, filename: undefined }],
|
|
174
|
+
reply_to: undefined,
|
|
92
175
|
})
|
|
176
|
+
expect(result.channel_id).toBe('thread-1')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('replies to a message when --reply is given', async () => {
|
|
180
|
+
const result = await uploadAction('general', [fileA], { ...options, reply: 'msg1' })
|
|
181
|
+
|
|
182
|
+
expect(result.error).toBeUndefined()
|
|
183
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('ch-general', {
|
|
184
|
+
content: undefined,
|
|
185
|
+
files: [{ path: fileA, filename: undefined }],
|
|
186
|
+
reply_to: 'msg1',
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('rejects --filename with multiple files before any client call', async () => {
|
|
191
|
+
const result = await uploadAction('general', [fileA, fileB], { ...options, filename: 'x.txt' })
|
|
192
|
+
|
|
193
|
+
expect(result.error).toBe('--filename requires exactly one file')
|
|
194
|
+
expect(mockResolveChannel).not.toHaveBeenCalled()
|
|
195
|
+
expect(mockCreateMessage).not.toHaveBeenCalled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('renames the uploaded part with --filename', async () => {
|
|
199
|
+
const result = await uploadAction('general', [fileA], { ...options, filename: 'renamed.txt' })
|
|
200
|
+
|
|
201
|
+
expect(result.error).toBeUndefined()
|
|
202
|
+
expect(result.file?.filename).toBe('renamed.txt')
|
|
203
|
+
expect(mockCreateMessage).toHaveBeenCalledWith('ch-general', {
|
|
204
|
+
content: undefined,
|
|
205
|
+
files: [{ path: fileA, filename: 'renamed.txt' }],
|
|
206
|
+
reply_to: undefined,
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('returns an error when a file does not exist', async () => {
|
|
211
|
+
const result = await uploadAction('general', [join(tempDir, 'missing.txt')], options)
|
|
212
|
+
|
|
213
|
+
expect(result.error).toContain('ENOENT')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('returns an error when more than 10 files are given', async () => {
|
|
217
|
+
mockCreateMessage.mockImplementationOnce(() =>
|
|
218
|
+
Promise.reject(new Error('Discord allows at most 10 files per message')),
|
|
219
|
+
)
|
|
220
|
+
const paths = Array.from({ length: 11 }, (_, i) => join(tempDir, `f${i}.txt`))
|
|
221
|
+
const result = await uploadAction('general', paths, options)
|
|
222
|
+
|
|
223
|
+
expect(result.error).toContain('10')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('returns error when channel resolution fails', async () => {
|
|
227
|
+
const result = await uploadAction('nonexistent', [fileA], options)
|
|
228
|
+
|
|
93
229
|
expect(result.error).toBeDefined()
|
|
230
|
+
expect(mockCreateMessage).not.toHaveBeenCalled()
|
|
94
231
|
})
|
|
95
232
|
})
|
|
96
233
|
|
|
97
|
-
describe('
|
|
98
|
-
it('
|
|
99
|
-
const result = await
|
|
100
|
-
...options,
|
|
101
|
-
_credManager: mockCredManager,
|
|
102
|
-
})
|
|
234
|
+
describe('infoAction', () => {
|
|
235
|
+
it('returns file info for an existing file without listing the channel', async () => {
|
|
236
|
+
const result = await infoAction('general', 'att1', options)
|
|
103
237
|
|
|
104
|
-
expect(result).
|
|
238
|
+
expect(result.error).toBeUndefined()
|
|
239
|
+
expect(result.id).toBe('att1')
|
|
240
|
+
expect(result.filename).toBe('test.txt')
|
|
241
|
+
expect(result.size).toBe(12)
|
|
242
|
+
expect(result.url).toBe('https://cdn.discord.com/test.txt')
|
|
243
|
+
expect(result.content_type).toBeNull()
|
|
244
|
+
expect(mockFindFile).toHaveBeenCalledWith('ch-general', 'att1')
|
|
245
|
+
expect(mockListFiles).not.toHaveBeenCalled()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('returns error when file is not found', async () => {
|
|
249
|
+
const result = await infoAction('general', 'nope', options)
|
|
250
|
+
|
|
251
|
+
expect(result.error).toBe('File not found: nope')
|
|
252
|
+
expect(mockFindFile).toHaveBeenCalledWith('ch-general', 'nope')
|
|
105
253
|
})
|
|
106
254
|
|
|
107
255
|
it('returns error when channel resolution fails', async () => {
|
|
108
|
-
const result = await
|
|
256
|
+
const result = await infoAction('nonexistent', 'att1', options)
|
|
257
|
+
|
|
109
258
|
expect(result.error).toBeDefined()
|
|
259
|
+
expect(mockFindFile).not.toHaveBeenCalled()
|
|
260
|
+
expect(mockListFiles).not.toHaveBeenCalled()
|
|
110
261
|
})
|
|
111
262
|
})
|
|
112
263
|
|
|
113
|
-
describe('
|
|
114
|
-
it('
|
|
115
|
-
const result = await
|
|
116
|
-
...options,
|
|
117
|
-
_credManager: mockCredManager,
|
|
118
|
-
})
|
|
264
|
+
describe('listAction', () => {
|
|
265
|
+
it('lists files in channel', async () => {
|
|
266
|
+
const result = await listAction('general', options)
|
|
119
267
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
268
|
+
expect(result.error).toBeUndefined()
|
|
269
|
+
expect(result.success).toBe(true)
|
|
270
|
+
expect(mockListFiles).toHaveBeenCalledWith('ch-general')
|
|
271
|
+
expect(mockFindFile).not.toHaveBeenCalled()
|
|
272
|
+
expect(result.files).toEqual([
|
|
273
|
+
{
|
|
274
|
+
id: 'att1',
|
|
275
|
+
filename: 'test.txt',
|
|
276
|
+
size: 12,
|
|
277
|
+
url: 'https://cdn.discord.com/test.txt',
|
|
278
|
+
content_type: null,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'att2',
|
|
282
|
+
filename: 'report.pdf',
|
|
283
|
+
size: 2048,
|
|
284
|
+
url: 'https://cdn.discord.com/report.pdf',
|
|
285
|
+
content_type: 'application/pdf',
|
|
286
|
+
},
|
|
287
|
+
])
|
|
130
288
|
})
|
|
131
289
|
|
|
132
|
-
it('returns
|
|
133
|
-
const result = await listAction('
|
|
134
|
-
...options,
|
|
135
|
-
_credManager: mockCredManager,
|
|
136
|
-
})
|
|
290
|
+
it('returns error when channel resolution fails', async () => {
|
|
291
|
+
const result = await listAction('nonexistent', options)
|
|
137
292
|
|
|
138
|
-
|
|
139
|
-
expect(result.success).toBe(true)
|
|
140
|
-
expect(Array.isArray(result.files)).toBe(true)
|
|
141
|
-
}
|
|
293
|
+
expect(result.error).toBeDefined()
|
|
142
294
|
})
|
|
143
295
|
})
|
|
144
296
|
})
|
|
@@ -5,48 +5,60 @@ import { Command } from 'commander'
|
|
|
5
5
|
import { handleError } from '@/shared/utils/error-handler'
|
|
6
6
|
import { formatOutput } from '@/shared/utils/output'
|
|
7
7
|
|
|
8
|
-
import type {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
filename: string
|
|
15
|
-
size: number
|
|
16
|
-
url: string
|
|
17
|
-
content_type?: string | null
|
|
8
|
+
import type { AttachmentOutput, BotOption, SendTargetOption } from './shared'
|
|
9
|
+
import { getClient, getCurrentServer, resolveSendTarget, toAttachmentOutput } from './shared'
|
|
10
|
+
|
|
11
|
+
export interface UploadOption extends SendTargetOption {
|
|
12
|
+
text?: string
|
|
13
|
+
filename?: string
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
interface UploadActionResult {
|
|
21
17
|
success?: boolean
|
|
22
18
|
error?: string
|
|
23
|
-
file?:
|
|
19
|
+
file?: AttachmentOutput
|
|
20
|
+
files?: AttachmentOutput[]
|
|
21
|
+
message_id?: string
|
|
22
|
+
channel_id?: string
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
interface ListActionResult {
|
|
27
26
|
success?: boolean
|
|
28
27
|
error?: string
|
|
29
|
-
files?:
|
|
28
|
+
files?: AttachmentOutput[]
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
type InfoActionResult = AttachmentOutput | { error: string }
|
|
32
|
+
|
|
33
|
+
export async function uploadAction(
|
|
34
|
+
channel: string,
|
|
35
|
+
filePaths: string[],
|
|
36
|
+
options: UploadOption,
|
|
37
|
+
): Promise<UploadActionResult> {
|
|
33
38
|
try {
|
|
39
|
+
if (options.filename !== undefined && filePaths.length > 1) {
|
|
40
|
+
return { error: '--filename requires exactly one file' }
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
const serverId = await getCurrentServer(options)
|
|
35
44
|
const client = await getClient(options)
|
|
36
|
-
const channelId = await client
|
|
45
|
+
const { channelId, threadId } = await resolveSendTarget(client, serverId, channel, options.thread)
|
|
37
46
|
|
|
38
|
-
const
|
|
39
|
-
|
|
47
|
+
const message = await client.createMessage(threadId ?? channelId, {
|
|
48
|
+
content: options.text,
|
|
49
|
+
files: filePaths.map((filePath) => ({ path: resolve(filePath), filename: options.filename })),
|
|
50
|
+
reply_to: options.reply,
|
|
51
|
+
})
|
|
40
52
|
|
|
41
|
-
const
|
|
42
|
-
id: file.id,
|
|
43
|
-
filename: file.filename,
|
|
44
|
-
size: file.size,
|
|
45
|
-
url: file.url,
|
|
46
|
-
content_type: file.content_type || null,
|
|
47
|
-
}
|
|
53
|
+
const files = toAttachmentOutput(message.attachments)
|
|
48
54
|
|
|
49
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
file: files[0],
|
|
58
|
+
files,
|
|
59
|
+
message_id: message.id,
|
|
60
|
+
channel_id: message.channel_id,
|
|
61
|
+
}
|
|
50
62
|
} catch (error) {
|
|
51
63
|
return { error: (error as Error).message }
|
|
52
64
|
}
|
|
@@ -60,15 +72,24 @@ export async function listAction(channel: string, options: BotOption): Promise<L
|
|
|
60
72
|
|
|
61
73
|
const files = await client.listFiles(channelId)
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
content_type: file.content_type || null,
|
|
69
|
-
}))
|
|
75
|
+
return { success: true, files: toAttachmentOutput(files) }
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { error: (error as Error).message }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
export async function infoAction(channel: string, fileId: string, options: BotOption): Promise<InfoActionResult> {
|
|
82
|
+
try {
|
|
83
|
+
const serverId = await getCurrentServer(options)
|
|
84
|
+
const client = await getClient(options)
|
|
85
|
+
const channelId = await client.resolveChannel(serverId, channel)
|
|
86
|
+
|
|
87
|
+
const file = await client.findFile(channelId, fileId)
|
|
88
|
+
if (!file) {
|
|
89
|
+
return { error: `File not found: ${fileId}` }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return toAttachmentOutput([file])[0]
|
|
72
93
|
} catch (error) {
|
|
73
94
|
return { error: (error as Error).message }
|
|
74
95
|
}
|
|
@@ -78,15 +99,36 @@ export const fileCommand = new Command('file')
|
|
|
78
99
|
.description('File commands')
|
|
79
100
|
.addCommand(
|
|
80
101
|
new Command('upload')
|
|
81
|
-
.description('Upload
|
|
102
|
+
.description('Upload one or more files to channel')
|
|
103
|
+
.argument('<channel>', 'Channel ID or name')
|
|
104
|
+
.argument('<file-path...>', 'One or more file paths (max 10)')
|
|
105
|
+
.option('--text <text>', 'Message text sent with the file(s)')
|
|
106
|
+
.option('--thread <id|name>', 'Post into this thread')
|
|
107
|
+
.option('--reply <message-id>', 'Reply to a message')
|
|
108
|
+
.option('--filename <name>', 'Override the filename (single file only)')
|
|
109
|
+
.option('--server <id>', 'Use specific server')
|
|
110
|
+
.option('--bot <id>', 'Use specific bot')
|
|
111
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
112
|
+
.action(async (channelArg: string, filePathArgs: string[], opts: UploadOption & BotOption) => {
|
|
113
|
+
try {
|
|
114
|
+
const result = await uploadAction(channelArg, filePathArgs, opts)
|
|
115
|
+
console.log(formatOutput(result, opts.pretty))
|
|
116
|
+
} catch (error) {
|
|
117
|
+
handleError(error as Error)
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
.addCommand(
|
|
122
|
+
new Command('info')
|
|
123
|
+
.description('Get file info')
|
|
82
124
|
.argument('<channel>', 'Channel ID or name')
|
|
83
|
-
.argument('<file-
|
|
125
|
+
.argument('<file-id>', 'File ID')
|
|
84
126
|
.option('--server <id>', 'Use specific server')
|
|
85
127
|
.option('--bot <id>', 'Use specific bot')
|
|
86
128
|
.option('--pretty', 'Pretty print JSON output')
|
|
87
|
-
.action(async (channelArg: string,
|
|
129
|
+
.action(async (channelArg: string, fileIdArg: string, options: BotOption) => {
|
|
88
130
|
try {
|
|
89
|
-
const result = await
|
|
131
|
+
const result = await infoAction(channelArg, fileIdArg, options)
|
|
90
132
|
console.log(formatOutput(result, options.pretty))
|
|
91
133
|
} catch (error) {
|
|
92
134
|
handleError(error as Error)
|