agent-messenger 2.17.0 → 2.19.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/bunfig.toml +1 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/discordbot/client.d.ts +1 -0
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +3 -0
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts +1 -0
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +2 -0
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +1 -3
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +16 -2
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.js +2 -17
- package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.js +23 -1
- package/dist/src/platforms/kakaotalk/commands/message.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +15 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +18 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +1 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +2 -4
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.js +5 -7
- package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts +5 -2
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +59 -1
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.d.ts +11 -0
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +37 -0
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.js +2 -4
- package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
- package/dist/src/shared/chromium/browsers.js +1 -1
- package/dist/src/shared/chromium/browsers.js.map +1 -1
- package/dist/src/shared/utils/interactive.d.ts +3 -0
- package/dist/src/shared/utils/interactive.d.ts.map +1 -0
- package/dist/src/shared/utils/interactive.js +16 -0
- package/dist/src/shared/utils/interactive.js.map +1 -0
- package/docs/content/docs/cli/discordbot.mdx +6 -0
- package/docs/content/docs/sdk/discordbot.mdx +4 -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 +1 -1
- package/skills/agent-discordbot/SKILL.md +9 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +24 -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 +32 -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/commands/dm.test.ts +28 -20
- package/src/platforms/discord/commands/reaction.test.ts +12 -7
- package/src/platforms/discordbot/client.test.ts +17 -0
- package/src/platforms/discordbot/client.ts +9 -2
- package/src/platforms/discordbot/commands/message.test.ts +28 -11
- package/src/platforms/discordbot/commands/message.ts +4 -2
- package/src/platforms/instagram/commands/auth.test.ts +11 -9
- package/src/platforms/instagram/commands/auth.ts +1 -4
- package/src/platforms/instagram/commands/chat.test.ts +8 -6
- package/src/platforms/instagram/commands/message.test.ts +8 -6
- package/src/platforms/kakaotalk/client.test.ts +57 -0
- package/src/platforms/kakaotalk/client.ts +23 -2
- package/src/platforms/kakaotalk/commands/auth.ts +2 -18
- package/src/platforms/kakaotalk/commands/message.test.ts +42 -0
- package/src/platforms/kakaotalk/commands/message.ts +33 -2
- package/src/platforms/kakaotalk/index.ts +2 -0
- package/src/platforms/kakaotalk/protocol/session.ts +15 -1
- package/src/platforms/kakaotalk/types.ts +24 -0
- package/src/platforms/line/commands/auth.ts +2 -5
- package/src/platforms/telegram/commands/auth.ts +5 -8
- package/src/platforms/webex/commands/auth.test.ts +178 -14
- package/src/platforms/webex/commands/auth.ts +102 -3
- package/src/platforms/webex/commands/member.test.ts +14 -20
- package/src/platforms/webex/commands/message.test.ts +11 -20
- package/src/platforms/webex/commands/snapshot.test.ts +11 -20
- package/src/platforms/webex/commands/space.test.ts +15 -23
- package/src/platforms/webex/commands/whoami.test.ts +8 -22
- package/src/platforms/webex/credential-manager.test.ts +78 -0
- package/src/platforms/webex/credential-manager.ts +59 -0
- package/src/platforms/whatsapp/commands/auth.ts +2 -5
- package/src/shared/chromium/browsers.ts +1 -1
- package/src/shared/utils/interactive.test.ts +55 -0
- package/src/shared/utils/interactive.ts +15 -0
- package/src/test-setup.ts +5 -0
- package/tsconfig.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-webex
|
|
3
3
|
description: Interact with Cisco Webex - send messages, read spaces, manage memberships
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.19.0
|
|
5
5
|
allowed-tools: Bash(agent-webex:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -76,6 +76,37 @@ Note: Messages sent via OAuth Device Grant show "via agent-messenger" because th
|
|
|
76
76
|
|
|
77
77
|
Optionally, pass `--token <bot-token>` for bot token auth. Or pass `--client-id <id> --client-secret <secret>` to use your own Webex Integration credentials instead of the built-in ones.
|
|
78
78
|
|
|
79
|
+
**For AI agents (non-TTY)**: `agent-webex auth login` exposes the OAuth Device Grant flow as a stateless two-call sequence — no hangs, no polling loops, no on-disk state. Just structured JSON every time.
|
|
80
|
+
|
|
81
|
+
**Call 1** (no `--device-code` passed): the command requests a device code from Webex and returns immediately:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"next_action": "authorize_in_browser",
|
|
86
|
+
"verification_uri": "https://login.webex.com/verify",
|
|
87
|
+
"verification_uri_complete": "https://login.webex.com/verify?userCode=ABC123",
|
|
88
|
+
"user_code": "ABC123",
|
|
89
|
+
"device_code": "d8eb0eca-2fee-428e-a59e-5e6d487b33ba",
|
|
90
|
+
"expires_at": 1779786537203,
|
|
91
|
+
"message": "Show the user `verification_uri` and `user_code` ..."
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Show the user `verification_uri_complete` (or `verification_uri` + `user_code`) in chat. **Remember the `device_code` value** — you will pass it back on the second call. Ask the user to confirm once they have approved access in any browser, on any device.
|
|
96
|
+
|
|
97
|
+
**Call 2** (`--device-code <device_code>`): pass the `device_code` from Call 1's response. The command makes one polling call to Webex:
|
|
98
|
+
|
|
99
|
+
- **Success** — returns `{ "authenticated": true, "user": { ... } }`, exit 0.
|
|
100
|
+
- **Still pending** — returns `{ "next_action": "still_pending", "device_code": "...", ... }`, exit 0. The user has not approved yet; confirm with them and retry with the same `--device-code` value.
|
|
101
|
+
- **Expired / failed** — returns `{ "next_action": "restart", "error": "..." }`, exit 1. The device code is no longer usable; start over with another `agent-webex auth login` (no flags) to get a fresh one.
|
|
102
|
+
|
|
103
|
+
If you passed `--client-id` / `--client-secret` (custom Webex Integration) on Call 1, pass them again on Call 2.
|
|
104
|
+
|
|
105
|
+
Alternatives that skip the Device Grant flow entirely:
|
|
106
|
+
|
|
107
|
+
- `agent-webex auth login --token <bot-or-personal-access-token>` — fully unattended, no human required.
|
|
108
|
+
- `agent-webex auth extract` — read an existing browser session token (no auth flow at all).
|
|
109
|
+
|
|
79
110
|
Env vars `AGENT_WEBEX_CLIENT_ID` / `AGENT_WEBEX_CLIENT_SECRET` can also override the built-in credentials.
|
|
80
111
|
|
|
81
112
|
```bash
|
|
@@ -5,6 +5,12 @@ import { DiscordCredentialManager } from '../credential-manager'
|
|
|
5
5
|
import type { DiscordDMChannel } from '../types'
|
|
6
6
|
import { createAction, listAction } from './dm'
|
|
7
7
|
|
|
8
|
+
class ProcessExit extends Error {
|
|
9
|
+
constructor(readonly code?: string | number | null) {
|
|
10
|
+
super(`process.exit(${code})`)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
let clientListDMChannelsSpy: ReturnType<typeof spyOn>
|
|
9
15
|
let clientCreateDMSpy: ReturnType<typeof spyOn>
|
|
10
16
|
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
@@ -86,21 +92,22 @@ describe('dm commands', () => {
|
|
|
86
92
|
servers: {},
|
|
87
93
|
})
|
|
88
94
|
|
|
89
|
-
const exitSpy =
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
96
|
+
throw new ProcessExit(code)
|
|
97
|
+
})
|
|
92
98
|
|
|
93
99
|
const consoleSpy = mock(() => {})
|
|
94
100
|
const originalLog = console.log
|
|
95
101
|
console.log = consoleSpy
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
try {
|
|
104
|
+
await expect(listAction({ pretty: false })).rejects.toThrow(ProcessExit)
|
|
105
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Not authenticated'))
|
|
106
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
107
|
+
} finally {
|
|
108
|
+
console.log = originalLog
|
|
109
|
+
exitSpy.mockRestore()
|
|
110
|
+
}
|
|
104
111
|
})
|
|
105
112
|
})
|
|
106
113
|
|
|
@@ -125,21 +132,22 @@ describe('dm commands', () => {
|
|
|
125
132
|
servers: {},
|
|
126
133
|
})
|
|
127
134
|
|
|
128
|
-
const exitSpy =
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
136
|
+
throw new ProcessExit(code)
|
|
137
|
+
})
|
|
131
138
|
|
|
132
139
|
const consoleSpy = mock(() => {})
|
|
133
140
|
const originalLog = console.log
|
|
134
141
|
console.log = consoleSpy
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
try {
|
|
144
|
+
await expect(createAction('456', { pretty: false })).rejects.toThrow(ProcessExit)
|
|
145
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Not authenticated'))
|
|
146
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
147
|
+
} finally {
|
|
148
|
+
console.log = originalLog
|
|
149
|
+
exitSpy.mockRestore()
|
|
150
|
+
}
|
|
143
151
|
})
|
|
144
152
|
})
|
|
145
153
|
})
|
|
@@ -4,6 +4,12 @@ import { DiscordClient } from '../client'
|
|
|
4
4
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
5
5
|
import { addAction, listAction, removeAction } from './reaction'
|
|
6
6
|
|
|
7
|
+
class ProcessExit extends Error {
|
|
8
|
+
constructor(readonly code?: string | number | null) {
|
|
9
|
+
super(`process.exit(${code})`)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
let clientAddReactionSpy: ReturnType<typeof spyOn>
|
|
8
14
|
let clientRemoveReactionSpy: ReturnType<typeof spyOn>
|
|
9
15
|
let clientGetMessageSpy: ReturnType<typeof spyOn>
|
|
@@ -114,21 +120,20 @@ it('add: handles missing token gracefully', async () => {
|
|
|
114
120
|
|
|
115
121
|
const consoleSpy = mock((_msg: string) => {})
|
|
116
122
|
const originalLog = console.log
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
_exitCode = code
|
|
121
|
-
}) as any
|
|
123
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
124
|
+
throw new ProcessExit(code)
|
|
125
|
+
})
|
|
122
126
|
|
|
123
127
|
console.log = consoleSpy
|
|
124
128
|
|
|
125
129
|
try {
|
|
126
|
-
await addAction('ch123', 'msg123', 'thumbsup', { pretty: false })
|
|
130
|
+
await expect(addAction('ch123', 'msg123', 'thumbsup', { pretty: false })).rejects.toThrow(ProcessExit)
|
|
127
131
|
expect(consoleSpy).toHaveBeenCalled()
|
|
128
132
|
const output = JSON.parse(consoleSpy.mock.calls[0][0])
|
|
129
133
|
expect(output.error).toBeDefined()
|
|
134
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
130
135
|
} finally {
|
|
131
136
|
console.log = originalLog
|
|
132
|
-
|
|
137
|
+
exitSpy.mockRestore()
|
|
133
138
|
}
|
|
134
139
|
})
|
|
@@ -167,6 +167,23 @@ describe('DiscordBotClient', () => {
|
|
|
167
167
|
|
|
168
168
|
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Thread reply', thread_id: 'thread123' }))
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
it('includes message_reference when reply_to is provided', async () => {
|
|
172
|
+
mockResponse({
|
|
173
|
+
id: 'msg1',
|
|
174
|
+
channel_id: 'ch1',
|
|
175
|
+
author: { id: '123', username: 'bot' },
|
|
176
|
+
content: 'Reply text',
|
|
177
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const client = await new DiscordBotClient().login({ token: 'bot-token' })
|
|
181
|
+
await client.sendMessage('ch1', 'Reply text', { reply_to: 'parent123' })
|
|
182
|
+
|
|
183
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
184
|
+
JSON.stringify({ content: 'Reply text', message_reference: { message_id: 'parent123' } }),
|
|
185
|
+
)
|
|
186
|
+
})
|
|
170
187
|
})
|
|
171
188
|
|
|
172
189
|
describe('editMessage', () => {
|
|
@@ -249,11 +249,18 @@ export class DiscordBotClient {
|
|
|
249
249
|
return this.request<DiscordChannel>('GET', `/channels/${channelId}`)
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
async sendMessage(
|
|
253
|
-
|
|
252
|
+
async sendMessage(
|
|
253
|
+
channelId: string,
|
|
254
|
+
content: string,
|
|
255
|
+
options?: { thread_id?: string; reply_to?: string },
|
|
256
|
+
): Promise<DiscordMessage> {
|
|
257
|
+
const body: Record<string, unknown> = { content }
|
|
254
258
|
if (options?.thread_id) {
|
|
255
259
|
body.thread_id = options.thread_id
|
|
256
260
|
}
|
|
261
|
+
if (options?.reply_to) {
|
|
262
|
+
body.message_reference = { message_id: options.reply_to }
|
|
263
|
+
}
|
|
257
264
|
return this.request<DiscordMessage>('POST', `/channels/${channelId}/messages`, body)
|
|
258
265
|
}
|
|
259
266
|
|
|
@@ -4,14 +4,15 @@ import { mkdir } from 'node:fs/promises'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
|
-
const mockSendMessage = mock(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
const mockSendMessage = mock(
|
|
8
|
+
(_channelId: string, content: string, _options?: { thread_id?: string; reply_to?: string }) =>
|
|
9
|
+
Promise.resolve({
|
|
10
|
+
id: 'msg1',
|
|
11
|
+
channel_id: 'ch1',
|
|
12
|
+
content,
|
|
13
|
+
author: { id: 'bot1', username: 'testbot' },
|
|
14
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
15
|
+
}),
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
const mockGetMessages = mock((_channelId: string, _limit?: number) =>
|
|
@@ -127,7 +128,7 @@ describe('message commands', () => {
|
|
|
127
128
|
expect(result.content).toBe('hello world')
|
|
128
129
|
expect(result.author).toBe('testbot')
|
|
129
130
|
expect(mockResolveChannel).toHaveBeenCalledWith('guild1', 'general')
|
|
130
|
-
expect(mockSendMessage).toHaveBeenCalledWith('ch1', 'hello world', { thread_id: undefined })
|
|
131
|
+
expect(mockSendMessage).toHaveBeenCalledWith('ch1', 'hello world', { thread_id: undefined, reply_to: undefined })
|
|
131
132
|
})
|
|
132
133
|
|
|
133
134
|
it('sends message to channel by ID', async () => {
|
|
@@ -135,7 +136,7 @@ describe('message commands', () => {
|
|
|
135
136
|
|
|
136
137
|
expect(result.id).toBe('msg1')
|
|
137
138
|
expect(mockResolveChannel).toHaveBeenCalledWith('guild1', '123456')
|
|
138
|
-
expect(mockSendMessage).toHaveBeenCalledWith('123456', 'hi', { thread_id: undefined })
|
|
139
|
+
expect(mockSendMessage).toHaveBeenCalledWith('123456', 'hi', { thread_id: undefined, reply_to: undefined })
|
|
139
140
|
})
|
|
140
141
|
|
|
141
142
|
it('sends message to thread', async () => {
|
|
@@ -145,7 +146,23 @@ describe('message commands', () => {
|
|
|
145
146
|
})
|
|
146
147
|
|
|
147
148
|
expect(result.id).toBe('msg1')
|
|
148
|
-
expect(mockSendMessage).toHaveBeenCalledWith('ch1', 'thread reply', {
|
|
149
|
+
expect(mockSendMessage).toHaveBeenCalledWith('ch1', 'thread reply', {
|
|
150
|
+
thread_id: 'thread123',
|
|
151
|
+
reply_to: undefined,
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('replies to a message', async () => {
|
|
156
|
+
const result = await sendAction('general', 'reply text', {
|
|
157
|
+
_credManager: manager,
|
|
158
|
+
reply: 'parent123',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(result.id).toBe('msg1')
|
|
162
|
+
expect(mockSendMessage).toHaveBeenCalledWith('ch1', 'reply text', {
|
|
163
|
+
thread_id: undefined,
|
|
164
|
+
reply_to: 'parent123',
|
|
165
|
+
})
|
|
149
166
|
})
|
|
150
167
|
|
|
151
168
|
it('returns error on channel not found', async () => {
|
|
@@ -28,7 +28,7 @@ interface MessageResult {
|
|
|
28
28
|
export async function sendAction(
|
|
29
29
|
channel: string,
|
|
30
30
|
text: string,
|
|
31
|
-
options: BotOption & { thread?: string },
|
|
31
|
+
options: BotOption & { thread?: string; reply?: string },
|
|
32
32
|
): Promise<MessageResult> {
|
|
33
33
|
try {
|
|
34
34
|
const client = await getClient(options)
|
|
@@ -36,6 +36,7 @@ export async function sendAction(
|
|
|
36
36
|
const channelId = await client.resolveChannel(serverId, channel)
|
|
37
37
|
const message = await client.sendMessage(channelId, text, {
|
|
38
38
|
thread_id: options.thread,
|
|
39
|
+
reply_to: options.reply,
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
return {
|
|
@@ -173,10 +174,11 @@ export const messageCommand = new Command('message')
|
|
|
173
174
|
.argument('<channel>', 'Channel ID or name')
|
|
174
175
|
.argument('<text>', 'Message text')
|
|
175
176
|
.option('--thread <id>', 'Thread ID for replies')
|
|
177
|
+
.option('--reply <message-id>', 'Reply to a message by ID')
|
|
176
178
|
.option('--bot <id>', 'Use specific bot')
|
|
177
179
|
.option('--server <id>', 'Server ID')
|
|
178
180
|
.option('--pretty', 'Pretty print JSON output')
|
|
179
|
-
.action(async (channel: string, text: string, opts: BotOption & { thread?: string }) => {
|
|
181
|
+
.action(async (channel: string, text: string, opts: BotOption & { thread?: string; reply?: string }) => {
|
|
180
182
|
cliOutput(await sendAction(channel, text, opts), opts.pretty)
|
|
181
183
|
}),
|
|
182
184
|
)
|
|
@@ -3,20 +3,13 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:te
|
|
|
3
3
|
const originalConsoleLog = console.log
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
5
|
|
|
6
|
+
import { InstagramCredentialManager } from '../credential-manager'
|
|
7
|
+
|
|
6
8
|
const mockGetAccount = mock(() => Promise.resolve(null))
|
|
7
9
|
const mockListAccounts = mock(() => Promise.resolve([]))
|
|
8
10
|
const mockSetCurrent = mock(() => Promise.resolve(true))
|
|
9
11
|
const mockRemoveAccount = mock(() => Promise.resolve(true))
|
|
10
12
|
|
|
11
|
-
mock.module('../credential-manager', () => ({
|
|
12
|
-
InstagramCredentialManager: class {
|
|
13
|
-
getAccount = mockGetAccount
|
|
14
|
-
listAccounts = mockListAccounts
|
|
15
|
-
setCurrent = mockSetCurrent
|
|
16
|
-
removeAccount = mockRemoveAccount
|
|
17
|
-
},
|
|
18
|
-
}))
|
|
19
|
-
|
|
20
13
|
import { authCommand } from './auth'
|
|
21
14
|
|
|
22
15
|
function resetCommandState(cmd: Command): void {
|
|
@@ -33,6 +26,7 @@ function resetCommandState(cmd: Command): void {
|
|
|
33
26
|
describe('auth commands', () => {
|
|
34
27
|
let consoleLogSpy: ReturnType<typeof mock>
|
|
35
28
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
29
|
+
const managerSpies: ReturnType<typeof spyOn>[] = []
|
|
36
30
|
|
|
37
31
|
beforeEach(() => {
|
|
38
32
|
resetCommandState(authCommand)
|
|
@@ -42,6 +36,13 @@ describe('auth commands', () => {
|
|
|
42
36
|
mockSetCurrent.mockReset()
|
|
43
37
|
mockRemoveAccount.mockReset()
|
|
44
38
|
|
|
39
|
+
managerSpies.push(
|
|
40
|
+
spyOn(InstagramCredentialManager.prototype, 'getAccount').mockImplementation(mockGetAccount),
|
|
41
|
+
spyOn(InstagramCredentialManager.prototype, 'listAccounts').mockImplementation(mockListAccounts),
|
|
42
|
+
spyOn(InstagramCredentialManager.prototype, 'setCurrent').mockImplementation(mockSetCurrent),
|
|
43
|
+
spyOn(InstagramCredentialManager.prototype, 'removeAccount').mockImplementation(mockRemoveAccount),
|
|
44
|
+
)
|
|
45
|
+
|
|
45
46
|
consoleLogSpy = mock((..._args: unknown[]) => {})
|
|
46
47
|
console.log = consoleLogSpy
|
|
47
48
|
processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
@@ -51,6 +52,7 @@ describe('auth commands', () => {
|
|
|
51
52
|
|
|
52
53
|
afterEach(() => {
|
|
53
54
|
console.log = originalConsoleLog
|
|
55
|
+
for (const spy of managerSpies.splice(0)) spy.mockRestore()
|
|
54
56
|
processExitSpy.mockRestore()
|
|
55
57
|
})
|
|
56
58
|
|
|
@@ -6,6 +6,7 @@ import { Command } from 'commander'
|
|
|
6
6
|
|
|
7
7
|
import { collectBrowserProfileOption } from '@/shared/chromium'
|
|
8
8
|
import { handleError } from '@/shared/utils/error-handler'
|
|
9
|
+
import { isInteractive } from '@/shared/utils/interactive'
|
|
9
10
|
import { formatOutput } from '@/shared/utils/output'
|
|
10
11
|
import { info, warn, error as stderrError, debug } from '@/shared/utils/stderr'
|
|
11
12
|
|
|
@@ -14,10 +15,6 @@ import { InstagramCredentialManager } from '../credential-manager'
|
|
|
14
15
|
import { InstagramTokenExtractor } from '../token-extractor'
|
|
15
16
|
import { createAccountId } from '../types'
|
|
16
17
|
|
|
17
|
-
function isInteractive(): boolean {
|
|
18
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
async function promptText(message: string): Promise<string | undefined> {
|
|
22
19
|
const { createInterface } = await import('node:readline/promises')
|
|
23
20
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
|
|
@@ -3,6 +3,9 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:te
|
|
|
3
3
|
const originalConsoleLog = console.log
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
5
|
|
|
6
|
+
import { InstagramClient } from '../client'
|
|
7
|
+
import * as sharedModule from './shared'
|
|
8
|
+
|
|
6
9
|
const mockListChats = mock(() =>
|
|
7
10
|
Promise.resolve([
|
|
8
11
|
{ id: 'thread-1', title: 'Alice', last_message: 'Hi' },
|
|
@@ -17,12 +20,6 @@ const mockClient = {
|
|
|
17
20
|
searchChats: mockSearchChats,
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
mock.module('./shared', () => ({
|
|
21
|
-
withInstagramClient: async (_options: unknown, fn: (client: typeof mockClient) => Promise<unknown>) => {
|
|
22
|
-
return fn(mockClient)
|
|
23
|
-
},
|
|
24
|
-
}))
|
|
25
|
-
|
|
26
23
|
import { chatCommand } from './chat'
|
|
27
24
|
|
|
28
25
|
function resetCommandState(cmd: Command): void {
|
|
@@ -39,6 +36,7 @@ function resetCommandState(cmd: Command): void {
|
|
|
39
36
|
describe('chat commands', () => {
|
|
40
37
|
let consoleLogSpy: ReturnType<typeof mock>
|
|
41
38
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
39
|
+
let withInstagramClientSpy: ReturnType<typeof spyOn>
|
|
42
40
|
|
|
43
41
|
beforeEach(() => {
|
|
44
42
|
resetCommandState(chatCommand)
|
|
@@ -56,6 +54,9 @@ describe('chat commands', () => {
|
|
|
56
54
|
|
|
57
55
|
consoleLogSpy = mock((..._args: unknown[]) => {})
|
|
58
56
|
console.log = consoleLogSpy
|
|
57
|
+
withInstagramClientSpy = spyOn(sharedModule, 'withInstagramClient').mockImplementation(async (_options, fn) => {
|
|
58
|
+
return fn(Object.assign(Object.create(InstagramClient.prototype), mockClient) as InstagramClient)
|
|
59
|
+
})
|
|
59
60
|
processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
60
61
|
throw new Error('process.exit called')
|
|
61
62
|
})
|
|
@@ -63,6 +64,7 @@ describe('chat commands', () => {
|
|
|
63
64
|
|
|
64
65
|
afterEach(() => {
|
|
65
66
|
console.log = originalConsoleLog
|
|
67
|
+
withInstagramClientSpy.mockRestore()
|
|
66
68
|
processExitSpy.mockRestore()
|
|
67
69
|
})
|
|
68
70
|
|
|
@@ -3,6 +3,9 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:te
|
|
|
3
3
|
const originalConsoleLog = console.log
|
|
4
4
|
import type { Command } from 'commander'
|
|
5
5
|
|
|
6
|
+
import { InstagramClient } from '../client'
|
|
7
|
+
import * as sharedModule from './shared'
|
|
8
|
+
|
|
6
9
|
const mockGetMessages = mock(() => Promise.resolve([{ id: 'msg-1', text: 'Hello' }]))
|
|
7
10
|
const mockSendMessage = mock(() => Promise.resolve({ id: 'msg-2', text: 'Sent' }))
|
|
8
11
|
const mockSendMessageToUser = mock(() => Promise.resolve({ id: 'msg-3', text: 'Sent to user' }))
|
|
@@ -17,12 +20,6 @@ const mockClient = {
|
|
|
17
20
|
searchUsers: mockSearchUsers,
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
mock.module('./shared', () => ({
|
|
21
|
-
withInstagramClient: async (_options: unknown, fn: (client: typeof mockClient) => Promise<unknown>) => {
|
|
22
|
-
return fn(mockClient)
|
|
23
|
-
},
|
|
24
|
-
}))
|
|
25
|
-
|
|
26
23
|
import { messageCommand } from './message'
|
|
27
24
|
|
|
28
25
|
function resetCommandState(cmd: Command): void {
|
|
@@ -39,6 +36,7 @@ function resetCommandState(cmd: Command): void {
|
|
|
39
36
|
describe('message commands', () => {
|
|
40
37
|
let consoleLogSpy: ReturnType<typeof mock>
|
|
41
38
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
39
|
+
let withInstagramClientSpy: ReturnType<typeof spyOn>
|
|
42
40
|
|
|
43
41
|
beforeEach(() => {
|
|
44
42
|
resetCommandState(messageCommand)
|
|
@@ -57,6 +55,9 @@ describe('message commands', () => {
|
|
|
57
55
|
|
|
58
56
|
consoleLogSpy = mock((..._args: unknown[]) => {})
|
|
59
57
|
console.log = consoleLogSpy
|
|
58
|
+
withInstagramClientSpy = spyOn(sharedModule, 'withInstagramClient').mockImplementation(async (_options, fn) => {
|
|
59
|
+
return fn(Object.assign(Object.create(InstagramClient.prototype), mockClient) as InstagramClient)
|
|
60
|
+
})
|
|
60
61
|
processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
61
62
|
throw new Error('process.exit called')
|
|
62
63
|
})
|
|
@@ -64,6 +65,7 @@ describe('message commands', () => {
|
|
|
64
65
|
|
|
65
66
|
afterEach(() => {
|
|
66
67
|
console.log = originalConsoleLog
|
|
68
|
+
withInstagramClientSpy.mockRestore()
|
|
67
69
|
processExitSpy.mockRestore()
|
|
68
70
|
})
|
|
69
71
|
|
|
@@ -13,6 +13,7 @@ const mockGetAllMembers = mock(() => Promise.resolve({}))
|
|
|
13
13
|
const mockGetMembersByIds = mock(() => Promise.resolve({}))
|
|
14
14
|
const mockSyncMessages = mock(() => Promise.resolve({}))
|
|
15
15
|
const mockSendMessage = mock(() => Promise.resolve({}))
|
|
16
|
+
const mockSendReply = mock(() => Promise.resolve({}))
|
|
16
17
|
const mockMarkRead = mock(() => Promise.resolve({}))
|
|
17
18
|
const mockClose = mock(() => {})
|
|
18
19
|
const mockOnClose = mock((_handler: () => void) => {})
|
|
@@ -30,6 +31,7 @@ mock.module('./protocol/session', () => ({
|
|
|
30
31
|
getMembersByIds = mockGetMembersByIds
|
|
31
32
|
syncMessages = mockSyncMessages
|
|
32
33
|
sendMessage = mockSendMessage
|
|
34
|
+
sendReply = mockSendReply
|
|
33
35
|
markRead = mockMarkRead
|
|
34
36
|
close = mockClose
|
|
35
37
|
onClose = mockOnClose
|
|
@@ -52,6 +54,7 @@ function resetAllMocks() {
|
|
|
52
54
|
mockGetMembersByIds.mockReset()
|
|
53
55
|
mockSyncMessages.mockReset()
|
|
54
56
|
mockSendMessage.mockReset()
|
|
57
|
+
mockSendReply.mockReset()
|
|
55
58
|
mockMarkRead.mockReset()
|
|
56
59
|
mockClose.mockReset()
|
|
57
60
|
mockOnClose.mockReset()
|
|
@@ -972,6 +975,60 @@ describe('KakaoTalkClient', () => {
|
|
|
972
975
|
|
|
973
976
|
client.close()
|
|
974
977
|
})
|
|
978
|
+
|
|
979
|
+
it('routes to sendReply with a built reply extra when replyTo is given', async () => {
|
|
980
|
+
// given
|
|
981
|
+
mockSendReply.mockResolvedValueOnce({ statusCode: 0, body: { logId: makeLong(50), sendAt: 1700000100 } })
|
|
982
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
983
|
+
|
|
984
|
+
// when
|
|
985
|
+
const result = await client.sendMessage('100', 'replying', {
|
|
986
|
+
replyTo: { log_id: '42', author_id: 7, message: 'original', type: 1 },
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
// then
|
|
990
|
+
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
991
|
+
expect(mockSendReply).toHaveBeenCalledTimes(1)
|
|
992
|
+
const [, text, extra] = mockSendReply.mock.calls[0] as [unknown, string, Record<string, unknown>]
|
|
993
|
+
expect(text).toBe('replying')
|
|
994
|
+
expect(extra).toEqual({
|
|
995
|
+
attach_only: false,
|
|
996
|
+
attach_type: 1,
|
|
997
|
+
src_logId: '42',
|
|
998
|
+
src_userId: 7,
|
|
999
|
+
src_message: 'original',
|
|
1000
|
+
src_type: 1,
|
|
1001
|
+
src_mentions: [],
|
|
1002
|
+
mentions: [],
|
|
1003
|
+
})
|
|
1004
|
+
expect(result).toEqual({
|
|
1005
|
+
success: true,
|
|
1006
|
+
status_code: 0,
|
|
1007
|
+
chat_id: '100',
|
|
1008
|
+
log_id: '50',
|
|
1009
|
+
sent_at: 1700000100,
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
client.close()
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
it('mirrors the source message type into attach_type/src_type', async () => {
|
|
1016
|
+
// given
|
|
1017
|
+
mockSendReply.mockResolvedValueOnce({ statusCode: 0, body: { logId: makeLong(51), sendAt: 1700000101 } })
|
|
1018
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1019
|
+
|
|
1020
|
+
// when — replying to a photo (type 2)
|
|
1021
|
+
await client.sendMessage('100', 'nice pic', {
|
|
1022
|
+
replyTo: { log_id: '99', author_id: 3, message: 'photo', type: 2 },
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
// then
|
|
1026
|
+
const [, , extra] = mockSendReply.mock.calls[0] as [unknown, string, Record<string, unknown>]
|
|
1027
|
+
expect(extra.attach_type).toBe(2)
|
|
1028
|
+
expect(extra.src_type).toBe(2)
|
|
1029
|
+
|
|
1030
|
+
client.close()
|
|
1031
|
+
})
|
|
975
1032
|
})
|
|
976
1033
|
|
|
977
1034
|
describe('markRead', () => {
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
type KakaoMessage,
|
|
24
24
|
type KakaoMultiPhotoExtra,
|
|
25
25
|
type KakaoProfile,
|
|
26
|
+
type KakaoReplyExtra,
|
|
27
|
+
type KakaoReplyTarget,
|
|
26
28
|
type KakaoSendResult,
|
|
27
29
|
} from './types'
|
|
28
30
|
|
|
@@ -445,6 +447,19 @@ function formatMessages(
|
|
|
445
447
|
}))
|
|
446
448
|
}
|
|
447
449
|
|
|
450
|
+
function buildReplyExtra(target: KakaoReplyTarget): KakaoReplyExtra {
|
|
451
|
+
return {
|
|
452
|
+
attach_only: false,
|
|
453
|
+
attach_type: target.type,
|
|
454
|
+
src_logId: target.log_id,
|
|
455
|
+
src_userId: target.author_id,
|
|
456
|
+
src_message: target.message,
|
|
457
|
+
src_type: target.type,
|
|
458
|
+
src_mentions: [],
|
|
459
|
+
mentions: [],
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
448
463
|
export class KakaoTalkClient {
|
|
449
464
|
private oauthToken: string | null = null
|
|
450
465
|
private userId: string | null = null
|
|
@@ -883,10 +898,16 @@ export class KakaoTalkClient {
|
|
|
883
898
|
})
|
|
884
899
|
}
|
|
885
900
|
|
|
886
|
-
async sendMessage(chatId: string, text: string): Promise<KakaoSendResult> {
|
|
901
|
+
async sendMessage(chatId: string, text: string, options?: { replyTo?: KakaoReplyTarget }): Promise<KakaoSendResult> {
|
|
887
902
|
return this.executeWithReconnect(async ({ session }) => {
|
|
888
903
|
try {
|
|
889
|
-
const response =
|
|
904
|
+
const response = options?.replyTo
|
|
905
|
+
? await session.sendReply(
|
|
906
|
+
parseLong(chatId),
|
|
907
|
+
text,
|
|
908
|
+
buildReplyExtra(options.replyTo) as unknown as Record<string, unknown>,
|
|
909
|
+
)
|
|
910
|
+
: await session.sendMessage(parseLong(chatId), text)
|
|
890
911
|
|
|
891
912
|
return {
|
|
892
913
|
success: response.statusCode === 0,
|