agent-messenger 2.29.1 → 2.30.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 +15 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +1 -0
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +3 -0
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/message.d.ts +3 -0
- package/dist/src/platforms/discord/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/message.js +30 -0
- package/dist/src/platforms/discord/commands/message.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +4 -4
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/slack/commands/message.js +4 -4
- package/dist/src/platforms/slack/commands/message.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/message.js +4 -4
- package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/client.d.ts +1 -0
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +19 -0
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/chat.d.ts +3 -0
- package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/chat.js +34 -1
- package/dist/src/platforms/teams/commands/chat.js.map +1 -1
- package/dist/src/platforms/telegram/client.d.ts +1 -0
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js +21 -0
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/message.js +25 -1
- package/dist/src/platforms/telegram/commands/message.js.map +1 -1
- package/dist/src/platforms/telegrambot/commands/message.d.ts +1 -1
- package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/commands/message.js +3 -3
- package/dist/src/platforms/telegrambot/commands/message.js.map +1 -1
- package/dist/src/platforms/whatsapp/client.d.ts +3 -1
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +19 -3
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/message.js +18 -0
- package/dist/src/platforms/whatsapp/commands/message.js.map +1 -1
- package/docs/content/docs/cli/discordbot.mdx +2 -2
- package/docs/content/docs/cli/slack.mdx +2 -2
- package/docs/content/docs/cli/slackbot.mdx +2 -2
- package/docs/content/docs/cli/telegrambot.mdx +1 -1
- package/e2e/README.md +3 -3
- package/e2e/discordbot.e2e.test.ts +2 -2
- package/e2e/slack.e2e.test.ts +2 -2
- package/e2e/slackbot.e2e.test.ts +2 -2
- package/e2e/telegrambot.e2e.test.ts +2 -2
- 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 +5 -1
- package/skills/agent-discordbot/SKILL.md +3 -3
- package/skills/agent-discordbot/references/common-patterns.md +2 -2
- 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 +3 -3
- package/skills/agent-slack/references/common-patterns.md +2 -2
- package/skills/agent-slackbot/SKILL.md +3 -3
- package/skills/agent-slackbot/references/common-patterns.md +4 -4
- package/skills/agent-teams/SKILL.md +6 -1
- package/skills/agent-telegram/SKILL.md +4 -1
- package/skills/agent-telegrambot/SKILL.md +2 -2
- 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 +25 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +22 -0
- package/src/platforms/discord/client.ts +4 -0
- package/src/platforms/discord/commands/message.test.ts +26 -1
- package/src/platforms/discord/commands/message.ts +41 -0
- package/src/platforms/discordbot/commands/message.test.ts +6 -6
- package/src/platforms/discordbot/commands/message.ts +4 -4
- package/src/platforms/slack/commands/message.test.ts +4 -4
- package/src/platforms/slack/commands/message.ts +4 -4
- package/src/platforms/slackbot/commands/message.ts +4 -4
- package/src/platforms/teams/client.test.ts +24 -0
- package/src/platforms/teams/client.ts +28 -0
- package/src/platforms/teams/commands/chat.test.ts +23 -1
- package/src/platforms/teams/commands/chat.ts +44 -0
- package/src/platforms/telegram/client.test.ts +60 -0
- package/src/platforms/telegram/client.ts +24 -0
- package/src/platforms/telegram/commands/message.test.ts +38 -0
- package/src/platforms/telegram/commands/message.ts +39 -0
- package/src/platforms/telegrambot/commands/message.ts +3 -3
- package/src/platforms/whatsapp/client.test.ts +58 -0
- package/src/platforms/whatsapp/client.ts +23 -3
- package/src/platforms/whatsapp/commands/message.test.ts +43 -0
- package/src/platforms/whatsapp/commands/message.ts +25 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-whatsapp
|
|
3
3
|
description: Interact with WhatsApp - send messages, read chats, manage conversations
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.30.0
|
|
5
5
|
allowed-tools: Bash(agent-whatsapp:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -251,6 +251,10 @@ agent-whatsapp message send +1234567890 "Hello!"
|
|
|
251
251
|
agent-whatsapp message send 123456789-123345@g.us "Hello team!"
|
|
252
252
|
agent-whatsapp message send +1234567890 "Hello!" --account <id>
|
|
253
253
|
|
|
254
|
+
# Edit one of your own messages (within ~15 minutes)
|
|
255
|
+
agent-whatsapp message edit <chat> <message-id> <text>
|
|
256
|
+
agent-whatsapp message edit +1234567890 ABC123DEF456 "Updated text"
|
|
257
|
+
|
|
254
258
|
# React to a message
|
|
255
259
|
agent-whatsapp message react <chat> <message-id> <emoji>
|
|
256
260
|
agent-whatsapp message react +1234567890 ABC123DEF456 "👍"
|
|
@@ -342,6 +346,26 @@ agent-whatsapp message list +1234567890 --limit 1
|
|
|
342
346
|
agent-whatsapp message react +1234567890 <message-id> "👍"
|
|
343
347
|
```
|
|
344
348
|
|
|
349
|
+
### Edit one of your own messages
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Get the message ID of a message you sent
|
|
353
|
+
agent-whatsapp message list +1234567890 --limit 5
|
|
354
|
+
|
|
355
|
+
# Edit it (only your own messages, within ~15 minutes)
|
|
356
|
+
agent-whatsapp message edit +1234567890 <message-id> "Updated text"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The SDK exposes the same capability via `WhatsAppClient.editMessage`:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { WhatsAppClient } from 'agent-messenger/whatsapp'
|
|
363
|
+
|
|
364
|
+
const client = await new WhatsAppClient().login()
|
|
365
|
+
const sent = await client.sendMessage('+1234567890', 'Deploying…')
|
|
366
|
+
await client.editMessage('+1234567890', sent.id, 'Deployed ✅')
|
|
367
|
+
```
|
|
368
|
+
|
|
345
369
|
## Error Handling
|
|
346
370
|
|
|
347
371
|
All commands return consistent error format:
|
|
@@ -246,6 +246,28 @@ describe('DiscordClient', () => {
|
|
|
246
246
|
})
|
|
247
247
|
})
|
|
248
248
|
|
|
249
|
+
describe('editMessage', () => {
|
|
250
|
+
it('edits message with PATCH', async () => {
|
|
251
|
+
mockResponse({
|
|
252
|
+
id: 'msg1',
|
|
253
|
+
channel_id: 'ch1',
|
|
254
|
+
author: { id: '123', username: 'bot' },
|
|
255
|
+
content: 'Updated content',
|
|
256
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
257
|
+
edited_timestamp: '2024-01-01T00:05:00.000Z',
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const client = await new DiscordClient().login({ token: 'test-token' })
|
|
261
|
+
const message = await client.editMessage('ch1', 'msg1', 'Updated content')
|
|
262
|
+
|
|
263
|
+
expect(message.content).toBe('Updated content')
|
|
264
|
+
expect(message.edited_timestamp).toBe('2024-01-01T00:05:00.000Z')
|
|
265
|
+
expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/ch1/messages/msg1')
|
|
266
|
+
expect(fetchCalls[0].options?.method).toBe('PATCH')
|
|
267
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Updated content' }))
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
249
271
|
describe('deleteMessage', () => {
|
|
250
272
|
it('deletes message', async () => {
|
|
251
273
|
mockResponse(null, 204)
|
|
@@ -242,6 +242,10 @@ export class DiscordClient {
|
|
|
242
242
|
return this.request<DiscordMessage>('GET', `/channels/${channelId}/messages/${messageId}`)
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
async editMessage(channelId: string, messageId: string, content: string): Promise<DiscordMessage> {
|
|
246
|
+
return this.request<DiscordMessage>('PATCH', `/channels/${channelId}/messages/${messageId}`, { content })
|
|
247
|
+
}
|
|
248
|
+
|
|
245
249
|
async deleteMessage(channelId: string, messageId: string): Promise<void> {
|
|
246
250
|
return this.request<void>('DELETE', `/channels/${channelId}/messages/${messageId}`)
|
|
247
251
|
}
|
|
@@ -2,11 +2,12 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import { DiscordClient } from '../client'
|
|
4
4
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
5
|
-
import { ackAction, deleteAction, getAction, listAction, searchAction, sendAction } from './message'
|
|
5
|
+
import { ackAction, deleteAction, editAction, getAction, listAction, searchAction, sendAction } from './message'
|
|
6
6
|
|
|
7
7
|
let clientSendMessageSpy: ReturnType<typeof spyOn>
|
|
8
8
|
let clientGetMessagesSpy: ReturnType<typeof spyOn>
|
|
9
9
|
let clientGetMessageSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let clientEditMessageSpy: ReturnType<typeof spyOn>
|
|
10
11
|
let clientDeleteMessageSpy: ReturnType<typeof spyOn>
|
|
11
12
|
let clientAckMessageSpy: ReturnType<typeof spyOn>
|
|
12
13
|
let clientSearchMessagesSpy: ReturnType<typeof spyOn>
|
|
@@ -47,6 +48,15 @@ beforeEach(() => {
|
|
|
47
48
|
timestamp: '2025-01-29T10:00:00Z',
|
|
48
49
|
})
|
|
49
50
|
|
|
51
|
+
clientEditMessageSpy = spyOn(DiscordClient.prototype, 'editMessage').mockResolvedValue({
|
|
52
|
+
id: 'msg_123',
|
|
53
|
+
channel_id: 'ch_456',
|
|
54
|
+
author: { id: 'user_789', username: 'testuser' },
|
|
55
|
+
content: 'Updated content',
|
|
56
|
+
timestamp: '2025-01-29T10:00:00Z',
|
|
57
|
+
edited_timestamp: '2025-01-29T10:05:00Z',
|
|
58
|
+
})
|
|
59
|
+
|
|
50
60
|
clientDeleteMessageSpy = spyOn(DiscordClient.prototype, 'deleteMessage').mockResolvedValue(undefined)
|
|
51
61
|
|
|
52
62
|
clientAckMessageSpy = spyOn(DiscordClient.prototype, 'ackMessage').mockResolvedValue(undefined)
|
|
@@ -87,6 +97,7 @@ afterEach(() => {
|
|
|
87
97
|
clientSendMessageSpy?.mockRestore()
|
|
88
98
|
clientGetMessagesSpy?.mockRestore()
|
|
89
99
|
clientGetMessageSpy?.mockRestore()
|
|
100
|
+
clientEditMessageSpy?.mockRestore()
|
|
90
101
|
clientDeleteMessageSpy?.mockRestore()
|
|
91
102
|
clientAckMessageSpy?.mockRestore()
|
|
92
103
|
clientSearchMessagesSpy?.mockRestore()
|
|
@@ -127,6 +138,20 @@ it('get: returns single message', async () => {
|
|
|
127
138
|
expect(output).toContain('msg_123')
|
|
128
139
|
})
|
|
129
140
|
|
|
141
|
+
it('edit: returns updated message', async () => {
|
|
142
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
143
|
+
console.log = consoleSpy
|
|
144
|
+
|
|
145
|
+
await editAction('ch_456', 'msg_123', 'Updated content', { pretty: false })
|
|
146
|
+
|
|
147
|
+
expect(clientEditMessageSpy).toHaveBeenCalledWith('ch_456', 'msg_123', 'Updated content')
|
|
148
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
149
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
150
|
+
expect(output).toContain('msg_123')
|
|
151
|
+
expect(output).toContain('Updated content')
|
|
152
|
+
expect(output).toContain('edited_timestamp')
|
|
153
|
+
})
|
|
154
|
+
|
|
130
155
|
it('delete: returns success', async () => {
|
|
131
156
|
const consoleSpy = mock((_msg: string) => {})
|
|
132
157
|
console.log = consoleSpy
|
|
@@ -121,6 +121,38 @@ export async function deleteAction(
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export async function editAction(
|
|
125
|
+
channelId: string,
|
|
126
|
+
messageId: string,
|
|
127
|
+
content: string,
|
|
128
|
+
options: { pretty?: boolean },
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
const credManager = new DiscordCredentialManager()
|
|
132
|
+
const config = await credManager.load()
|
|
133
|
+
|
|
134
|
+
if (!config.token) {
|
|
135
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
140
|
+
const message = await client.editMessage(channelId, messageId, content)
|
|
141
|
+
|
|
142
|
+
const output = {
|
|
143
|
+
id: message.id,
|
|
144
|
+
content: message.content,
|
|
145
|
+
author: message.author.username,
|
|
146
|
+
timestamp: message.timestamp,
|
|
147
|
+
edited_timestamp: message.edited_timestamp,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(formatOutput(output, options.pretty))
|
|
151
|
+
} catch (error) {
|
|
152
|
+
handleError(error as Error)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
124
156
|
export async function ackAction(channelId: string, messageId: string, options: { pretty?: boolean }): Promise<void> {
|
|
125
157
|
try {
|
|
126
158
|
const credManager = new DiscordCredentialManager()
|
|
@@ -244,6 +276,15 @@ export const messageCommand = new Command('message')
|
|
|
244
276
|
.option('--pretty', 'Pretty print JSON output')
|
|
245
277
|
.action(deleteAction),
|
|
246
278
|
)
|
|
279
|
+
.addCommand(
|
|
280
|
+
new Command('edit')
|
|
281
|
+
.description('Edit a message')
|
|
282
|
+
.argument('<channel-id>', 'Channel ID')
|
|
283
|
+
.argument('<message-id>', 'Message ID')
|
|
284
|
+
.argument('<content>', 'New message content')
|
|
285
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
286
|
+
.action(editAction),
|
|
287
|
+
)
|
|
247
288
|
.addCommand(
|
|
248
289
|
new Command('ack')
|
|
249
290
|
.description('Mark message as read (acknowledge)')
|
|
@@ -82,7 +82,7 @@ mock.module('../client', () => ({
|
|
|
82
82
|
}))
|
|
83
83
|
|
|
84
84
|
import { DiscordBotCredentialManager } from '../credential-manager'
|
|
85
|
-
import { deleteAction, getAction, listAction, repliesAction, sendAction
|
|
85
|
+
import { deleteAction, editAction, getAction, listAction, repliesAction, sendAction } from './message'
|
|
86
86
|
|
|
87
87
|
describe('message commands', () => {
|
|
88
88
|
let tempDir: string
|
|
@@ -243,9 +243,9 @@ describe('message commands', () => {
|
|
|
243
243
|
})
|
|
244
244
|
})
|
|
245
245
|
|
|
246
|
-
describe('
|
|
247
|
-
it('
|
|
248
|
-
const result = await
|
|
246
|
+
describe('editAction', () => {
|
|
247
|
+
it('edits a message', async () => {
|
|
248
|
+
const result = await editAction('general', 'msg1', 'updated text', { _credManager: manager })
|
|
249
249
|
|
|
250
250
|
expect(result.id).toBe('msg1')
|
|
251
251
|
expect(result.content).toBe('updated text')
|
|
@@ -254,7 +254,7 @@ describe('message commands', () => {
|
|
|
254
254
|
})
|
|
255
255
|
|
|
256
256
|
it('resolves channel name', async () => {
|
|
257
|
-
await
|
|
257
|
+
await editAction('general', 'msg1', 'new', { _credManager: manager })
|
|
258
258
|
|
|
259
259
|
expect(mockResolveChannel).toHaveBeenCalledWith('guild1', 'general')
|
|
260
260
|
})
|
|
@@ -262,7 +262,7 @@ describe('message commands', () => {
|
|
|
262
262
|
it('returns error on failure', async () => {
|
|
263
263
|
mockEditMessage.mockImplementationOnce(() => Promise.reject(new Error('Cannot edit')))
|
|
264
264
|
|
|
265
|
-
const result = await
|
|
265
|
+
const result = await editAction('general', 'msg1', 'new', { _credManager: manager })
|
|
266
266
|
|
|
267
267
|
expect(result.error).toContain('Cannot edit')
|
|
268
268
|
})
|
|
@@ -95,7 +95,7 @@ export async function getAction(channel: string, messageId: string, options: Bot
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export async function
|
|
98
|
+
export async function editAction(
|
|
99
99
|
channel: string,
|
|
100
100
|
messageId: string,
|
|
101
101
|
text: string,
|
|
@@ -207,8 +207,8 @@ export const messageCommand = new Command('message')
|
|
|
207
207
|
}),
|
|
208
208
|
)
|
|
209
209
|
.addCommand(
|
|
210
|
-
new Command('
|
|
211
|
-
.description('
|
|
210
|
+
new Command('edit')
|
|
211
|
+
.description('Edit a message')
|
|
212
212
|
.argument('<channel>', 'Channel ID or name')
|
|
213
213
|
.argument('<message-id>', 'Message ID')
|
|
214
214
|
.argument('<text>', 'New message text')
|
|
@@ -216,7 +216,7 @@ export const messageCommand = new Command('message')
|
|
|
216
216
|
.option('--server <id>', 'Server ID')
|
|
217
217
|
.option('--pretty', 'Pretty print JSON output')
|
|
218
218
|
.action(async (channel: string, messageId: string, text: string, opts: BotOption) => {
|
|
219
|
-
cliOutput(await
|
|
219
|
+
cliOutput(await editAction(channel, messageId, text, opts), opts.pretty)
|
|
220
220
|
}),
|
|
221
221
|
)
|
|
222
222
|
.addCommand(
|
|
@@ -165,17 +165,17 @@ describe('Message Commands', () => {
|
|
|
165
165
|
})
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
-
describe('message
|
|
169
|
-
it('
|
|
168
|
+
describe('message edit', () => {
|
|
169
|
+
it('edits message text', async () => {
|
|
170
170
|
// Given: A channel, message ts, and new text
|
|
171
171
|
const channel = 'C123'
|
|
172
172
|
const ts = '1234567890.123456'
|
|
173
173
|
const newText = 'Updated message'
|
|
174
174
|
|
|
175
|
-
// When:
|
|
175
|
+
// When: Editing message
|
|
176
176
|
const result = await mockClient.updateMessage(channel, ts, newText)
|
|
177
177
|
|
|
178
|
-
// Then: Should return
|
|
178
|
+
// Then: Should return edited message
|
|
179
179
|
expect(result.ts).toBe(ts)
|
|
180
180
|
expect(result.text).toBe(newText)
|
|
181
181
|
})
|
|
@@ -114,7 +114,7 @@ async function getAction(channelInput: string, ts: string, options: { pretty?: b
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async function
|
|
117
|
+
async function editAction(
|
|
118
118
|
channelInput: string,
|
|
119
119
|
ts: string,
|
|
120
120
|
text: string,
|
|
@@ -413,13 +413,13 @@ export const messageCommand = new Command('message')
|
|
|
413
413
|
.action(getAction),
|
|
414
414
|
)
|
|
415
415
|
.addCommand(
|
|
416
|
-
new Command('
|
|
417
|
-
.description('
|
|
416
|
+
new Command('edit')
|
|
417
|
+
.description('Edit message text')
|
|
418
418
|
.argument('<channel>', 'Channel ID or name')
|
|
419
419
|
.argument('<ts>', 'Message timestamp')
|
|
420
420
|
.argument('<text>', 'New message text')
|
|
421
421
|
.option('--pretty', 'Pretty print JSON output')
|
|
422
|
-
.action(
|
|
422
|
+
.action(editAction),
|
|
423
423
|
)
|
|
424
424
|
.addCommand(
|
|
425
425
|
new Command('delete')
|
|
@@ -59,7 +59,7 @@ async function getAction(channelInput: string, ts: string, options: BotOption):
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
async function
|
|
62
|
+
async function editAction(channelInput: string, ts: string, text: string, options: BotOption): Promise<void> {
|
|
63
63
|
try {
|
|
64
64
|
const client = await getClient(options)
|
|
65
65
|
const channel = await client.resolveChannel(channelInput)
|
|
@@ -158,14 +158,14 @@ export const messageCommand = new Command('message')
|
|
|
158
158
|
.action(getAction),
|
|
159
159
|
)
|
|
160
160
|
.addCommand(
|
|
161
|
-
new Command('
|
|
162
|
-
.description('
|
|
161
|
+
new Command('edit')
|
|
162
|
+
.description('Edit a message')
|
|
163
163
|
.argument('<channel>', 'Channel ID or name')
|
|
164
164
|
.argument('<ts>', 'Message timestamp')
|
|
165
165
|
.argument('<text>', 'New message text')
|
|
166
166
|
.option('--bot <id>', 'Use specific bot')
|
|
167
167
|
.option('--pretty', 'Pretty print JSON output')
|
|
168
|
-
.action(
|
|
168
|
+
.action(editAction),
|
|
169
169
|
)
|
|
170
170
|
.addCommand(
|
|
171
171
|
new Command('delete')
|
|
@@ -330,6 +330,30 @@ describe('TeamsClient', () => {
|
|
|
330
330
|
})
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
+
describe('editChatMessage', () => {
|
|
334
|
+
it('PUTs an HTML-escaped edit to a chat message', async () => {
|
|
335
|
+
mockResponse({ edittime: 1704067200000 })
|
|
336
|
+
|
|
337
|
+
const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
|
|
338
|
+
const message = await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', 'a <b> & c')
|
|
339
|
+
|
|
340
|
+
expect(message.id).toBe('msg1')
|
|
341
|
+
expect(message.content).toBe('a <b> & c')
|
|
342
|
+
expect(fetchCalls[0].url).toBe(
|
|
343
|
+
'https://msgapi.teams.live.com/v1/users/ME/conversations/19%3A1on1%40unq.gbl.spaces/messages/msg1',
|
|
344
|
+
)
|
|
345
|
+
expect(fetchCalls[0].options?.method).toBe('PUT')
|
|
346
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
347
|
+
JSON.stringify({
|
|
348
|
+
content: 'a <b> & c',
|
|
349
|
+
messagetype: 'RichText/Html',
|
|
350
|
+
contenttype: 'text',
|
|
351
|
+
skypeeditedid: 'msg1',
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
333
357
|
describe('getTeam', () => {
|
|
334
358
|
it('returns team info', async () => {
|
|
335
359
|
mockResponse({ id: '111', name: 'Test Team', description: 'A test team' })
|
|
@@ -713,6 +713,34 @@ export class TeamsClient {
|
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
async editChatMessage(chatId: string, messageId: string, content: string): Promise<TeamsMessage> {
|
|
717
|
+
interface EditResponse {
|
|
718
|
+
edittime?: string | number
|
|
719
|
+
}
|
|
720
|
+
const encodedChatId = encodeURIComponent(chatId)
|
|
721
|
+
const encodedMessageId = encodeURIComponent(messageId)
|
|
722
|
+
// Skype messaging backend requires skypeeditedid to duplicate the URL message id.
|
|
723
|
+
const response = await this.request<EditResponse>(
|
|
724
|
+
'PUT',
|
|
725
|
+
`/users/ME/conversations/${encodedChatId}/messages/${encodedMessageId}`,
|
|
726
|
+
{
|
|
727
|
+
content: escapeHtml(content),
|
|
728
|
+
messagetype: 'RichText/Html',
|
|
729
|
+
contenttype: 'text',
|
|
730
|
+
skypeeditedid: messageId,
|
|
731
|
+
},
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
const editTime = response?.edittime
|
|
735
|
+
return {
|
|
736
|
+
id: messageId,
|
|
737
|
+
channel_id: chatId,
|
|
738
|
+
author: { id: 'ME', displayName: 'Me' },
|
|
739
|
+
content,
|
|
740
|
+
timestamp: editTime ? new Date(Number(editTime) || editTime).toISOString() : new Date().toISOString(),
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
716
744
|
async getTeam(teamId: string): Promise<TeamsTeam> {
|
|
717
745
|
return this.request<TeamsTeam>('GET', `/csa/api/v1/teams/${teamId}`, undefined, CSA_API_BASE)
|
|
718
746
|
}
|
|
@@ -2,11 +2,12 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import { TeamsClient } from '../client'
|
|
4
4
|
import { TeamsCredentialManager } from '../credential-manager'
|
|
5
|
-
import { historyAction, listAction, sendAction } from './chat'
|
|
5
|
+
import { editAction, historyAction, listAction, sendAction } from './chat'
|
|
6
6
|
|
|
7
7
|
let clientListChatsSpy: ReturnType<typeof spyOn>
|
|
8
8
|
let clientGetChatMessagesSpy: ReturnType<typeof spyOn>
|
|
9
9
|
let clientSendChatMessageSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let clientEditChatMessageSpy: ReturnType<typeof spyOn>
|
|
10
11
|
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
11
12
|
const originalConsoleLog = console.log
|
|
12
13
|
|
|
@@ -34,6 +35,14 @@ beforeEach(() => {
|
|
|
34
35
|
timestamp: '2025-01-29T10:00:00Z',
|
|
35
36
|
})
|
|
36
37
|
|
|
38
|
+
clientEditChatMessageSpy = spyOn(TeamsClient.prototype, 'editChatMessage').mockResolvedValue({
|
|
39
|
+
id: 'msg_123',
|
|
40
|
+
channel_id: '19:1on1@unq.gbl.spaces',
|
|
41
|
+
author: { id: 'ME', displayName: 'Me' },
|
|
42
|
+
content: 'Edited content',
|
|
43
|
+
timestamp: '2025-01-29T10:05:00Z',
|
|
44
|
+
})
|
|
45
|
+
|
|
37
46
|
credManagerLoadSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
|
|
38
47
|
current_account: 'personal',
|
|
39
48
|
accounts: {
|
|
@@ -51,6 +60,7 @@ afterEach(() => {
|
|
|
51
60
|
clientListChatsSpy?.mockRestore()
|
|
52
61
|
clientGetChatMessagesSpy?.mockRestore()
|
|
53
62
|
clientSendChatMessageSpy?.mockRestore()
|
|
63
|
+
clientEditChatMessageSpy?.mockRestore()
|
|
54
64
|
credManagerLoadSpy?.mockRestore()
|
|
55
65
|
console.log = originalConsoleLog
|
|
56
66
|
})
|
|
@@ -98,3 +108,15 @@ it('send: returns sent message', async () => {
|
|
|
98
108
|
const output = consoleSpy.mock.calls[0][0]
|
|
99
109
|
expect(output).toContain('Hello world')
|
|
100
110
|
})
|
|
111
|
+
|
|
112
|
+
it('edit: edits a chat message and returns updated content', async () => {
|
|
113
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
114
|
+
console.log = consoleSpy
|
|
115
|
+
|
|
116
|
+
await editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', { pretty: false })
|
|
117
|
+
|
|
118
|
+
expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content')
|
|
119
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
120
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
121
|
+
expect(output).toContain('Edited content')
|
|
122
|
+
})
|
|
@@ -100,6 +100,41 @@ export async function sendAction(chatId: string, content: string, options: { pre
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
export async function editAction(
|
|
104
|
+
chatId: string,
|
|
105
|
+
messageId: string,
|
|
106
|
+
content: string,
|
|
107
|
+
options: { pretty?: boolean },
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
const credManager = new TeamsCredentialManager()
|
|
111
|
+
const cred = await credManager.getTokenWithExpiry()
|
|
112
|
+
|
|
113
|
+
if (!cred) {
|
|
114
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const client = await new TeamsClient().login({
|
|
119
|
+
token: cred.token,
|
|
120
|
+
tokenExpiresAt: cred.tokenExpiresAt,
|
|
121
|
+
accountType: cred.accountType,
|
|
122
|
+
region: cred.region,
|
|
123
|
+
})
|
|
124
|
+
const message = await client.editChatMessage(chatId, messageId, content)
|
|
125
|
+
|
|
126
|
+
const output = {
|
|
127
|
+
id: message.id,
|
|
128
|
+
content: message.content,
|
|
129
|
+
timestamp: message.timestamp,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(formatOutput(output, options.pretty))
|
|
133
|
+
} catch (error) {
|
|
134
|
+
handleError(error as Error)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
103
138
|
export const chatCommand = new Command('chat')
|
|
104
139
|
.description('Chat commands (1:1, group, and self chats)')
|
|
105
140
|
.addCommand(
|
|
@@ -129,3 +164,12 @@ export const chatCommand = new Command('chat')
|
|
|
129
164
|
.option('--pretty', 'Pretty print JSON output')
|
|
130
165
|
.action(sendAction),
|
|
131
166
|
)
|
|
167
|
+
.addCommand(
|
|
168
|
+
new Command('edit')
|
|
169
|
+
.description('Edit a message in a chat (your own messages only)')
|
|
170
|
+
.argument('<chat-id>', 'Chat ID')
|
|
171
|
+
.argument('<message-id>', 'Message ID')
|
|
172
|
+
.argument('<content>', 'New message content')
|
|
173
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
174
|
+
.action(editAction),
|
|
175
|
+
)
|
|
@@ -235,3 +235,63 @@ describe('sendMessage confirmation', () => {
|
|
|
235
235
|
expect(result.id).toBe(serverId)
|
|
236
236
|
})
|
|
237
237
|
})
|
|
238
|
+
|
|
239
|
+
describe('editMessage', () => {
|
|
240
|
+
it('returns the edited message from the editMessageText response', async () => {
|
|
241
|
+
const events: any[] = []
|
|
242
|
+
let editRequest: any = null
|
|
243
|
+
|
|
244
|
+
const createClientId = mock(() => 1)
|
|
245
|
+
const send = mock((_clientId: number, request: any) => {
|
|
246
|
+
if (request['@type'] === 'getAuthorizationState') {
|
|
247
|
+
events.push({
|
|
248
|
+
'@type': 'updateAuthorizationState',
|
|
249
|
+
authorization_state: { '@type': 'authorizationStateReady' },
|
|
250
|
+
'@extra': request['@extra'],
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (request['@type'] === 'getChat') {
|
|
255
|
+
events.push({ '@type': 'chat', id: 42, title: 'test', unread_count: 0, '@extra': request['@extra'] })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (request['@type'] === 'editMessageText') {
|
|
259
|
+
editRequest = request
|
|
260
|
+
events.push({
|
|
261
|
+
'@type': 'message',
|
|
262
|
+
id: 555,
|
|
263
|
+
chat_id: 42,
|
|
264
|
+
date: 1_710_000_002,
|
|
265
|
+
is_outgoing: true,
|
|
266
|
+
sender_id: { '@type': 'messageSenderUser', user_id: 1 },
|
|
267
|
+
content: {
|
|
268
|
+
'@type': 'messageText',
|
|
269
|
+
text: { '@type': 'formattedText', text: 'edited text', entities: [] },
|
|
270
|
+
},
|
|
271
|
+
'@extra': request['@extra'],
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const receive = mock(() => events.shift() ?? null)
|
|
277
|
+
const client = new (TelegramTdlibClient as unknown as new (
|
|
278
|
+
account: TelegramAccount,
|
|
279
|
+
paths: TelegramAccountPaths,
|
|
280
|
+
tdjson: any,
|
|
281
|
+
) => TelegramTdlibClient)(mockAccount, mockPaths, {
|
|
282
|
+
createClientId,
|
|
283
|
+
send,
|
|
284
|
+
receive,
|
|
285
|
+
libraryPath: '/mock/lib',
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const result = await client.editMessage('42', 555, 'edited text')
|
|
289
|
+
|
|
290
|
+
expect(result.id).toBe(555)
|
|
291
|
+
expect(result.text).toBe('edited text')
|
|
292
|
+
expect(editRequest.chat_id).toBe(42)
|
|
293
|
+
expect(editRequest.message_id).toBe(555)
|
|
294
|
+
expect(editRequest.input_message_content['@type']).toBe('inputMessageText')
|
|
295
|
+
expect(editRequest.input_message_content.text.text).toBe('edited text')
|
|
296
|
+
})
|
|
297
|
+
})
|
|
@@ -312,6 +312,30 @@ export class TelegramTdlibClient {
|
|
|
312
312
|
return simplifyMessage(message, chat.id)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
async editMessage(reference: string, messageId: number, text: string): Promise<TelegramMessageSummary> {
|
|
316
|
+
await this.ensureReady()
|
|
317
|
+
const chat = await this.resolveChat(reference)
|
|
318
|
+
|
|
319
|
+
const message = (await this.call({
|
|
320
|
+
'@type': 'editMessageText',
|
|
321
|
+
chat_id: chat.id,
|
|
322
|
+
message_id: messageId,
|
|
323
|
+
reply_markup: null,
|
|
324
|
+
input_message_content: {
|
|
325
|
+
'@type': 'inputMessageText',
|
|
326
|
+
text: {
|
|
327
|
+
'@type': 'formattedText',
|
|
328
|
+
text,
|
|
329
|
+
entities: [],
|
|
330
|
+
},
|
|
331
|
+
link_preview_options: null,
|
|
332
|
+
clear_draft: false,
|
|
333
|
+
},
|
|
334
|
+
})) as TdMessage
|
|
335
|
+
|
|
336
|
+
return simplifyMessage(message, chat.id)
|
|
337
|
+
}
|
|
338
|
+
|
|
315
339
|
private async ensureReady(): Promise<void> {
|
|
316
340
|
const state = await this.connect()
|
|
317
341
|
if (state?.['@type'] !== 'authorizationStateReady') {
|