agent-messenger 2.12.2 → 2.13.1
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/index.d.ts +2 -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/listener.d.ts +4 -3
- package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/listener.js.map +1 -1
- package/dist/src/platforms/discordbot/types.d.ts +21 -0
- package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +18 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js +29 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -0
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +13 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +225 -8
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js +2 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/member.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/member.js +22 -0
- package/dist/src/platforms/kakaotalk/commands/member.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +2 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.js +5 -2
- package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +28 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +44 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +37 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +17 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/slackbot/client.d.ts +5 -0
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.js +5 -0
- package/dist/src/platforms/slackbot/client.js.map +1 -1
- package/dist/src/platforms/slackbot/index.d.ts +2 -2
- package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/index.js +1 -1
- package/dist/src/platforms/slackbot/index.js.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.js +3 -3
- package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +26 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +45 -13
- package/docs/content/docs/sdk/slackbot.mdx +11 -0
- package/package.json +1 -1
- package/scripts/kakao-loco-capture.ts +466 -0
- 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 +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +30 -3
- package/skills/agent-kakaotalk/references/common-patterns.md +49 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -2
- 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-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/index.test.ts +76 -0
- package/src/platforms/discordbot/index.ts +3 -0
- package/src/platforms/discordbot/listener.test.ts +41 -0
- package/src/platforms/discordbot/listener.ts +5 -1
- package/src/platforms/discordbot/types.ts +23 -1
- package/src/platforms/kakaotalk/chat-classifier.test.ts +33 -0
- package/src/platforms/kakaotalk/chat-classifier.ts +31 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client.test.ts +782 -1
- package/src/platforms/kakaotalk/client.ts +262 -10
- package/src/platforms/kakaotalk/commands/chat.ts +3 -1
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/member.test.ts +102 -0
- package/src/platforms/kakaotalk/commands/member.ts +32 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +4 -0
- package/src/platforms/kakaotalk/listener.test.ts +29 -0
- package/src/platforms/kakaotalk/listener.ts +5 -2
- package/src/platforms/kakaotalk/protocol/session.ts +44 -0
- package/src/platforms/kakaotalk/types.ts +39 -0
- package/src/platforms/slackbot/client.test.ts +67 -0
- package/src/platforms/slackbot/client.ts +17 -1
- package/src/platforms/slackbot/index.test.ts +10 -0
- package/src/platforms/slackbot/index.ts +4 -0
- package/src/tui/adapters/kakaotalk-adapter.ts +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-kakaotalk
|
|
3
3
|
description: Interact with KakaoTalk - send messages, read chats, manage conversations
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.13.1
|
|
5
5
|
allowed-tools: Bash(agent-kakaotalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -288,15 +288,39 @@ agent-kakaotalk chat list
|
|
|
288
288
|
agent-kakaotalk chat list --pretty
|
|
289
289
|
agent-kakaotalk chat list --account <account-id>
|
|
290
290
|
agent-kakaotalk chat list --account <account-id> --pretty
|
|
291
|
+
|
|
292
|
+
# Resolve user-set room titles via CHATINFO (one extra LOCO call per chat;
|
|
293
|
+
# slower, but matches the room name shown in the official KakaoTalk app)
|
|
294
|
+
agent-kakaotalk chat list --resolve-titles
|
|
291
295
|
```
|
|
292
296
|
|
|
293
297
|
Output includes:
|
|
294
298
|
- `chat_id` — numeric chat room ID
|
|
295
299
|
- `type` — chat type (1:1, group, open chat)
|
|
296
300
|
- `display_name` — comma-separated member names
|
|
301
|
+
- `title` — user-set room title (only populated with `--resolve-titles`; otherwise `null`). For open chats (`OM` / `OD`) without a user-set title, falls back to the OpenLink room name (one extra `INFOLINK` LOCO call per such chat).
|
|
297
302
|
- `active_members` — number of active members
|
|
298
303
|
- `unread_count` — unread message count
|
|
299
|
-
- `last_message` — most recent message preview
|
|
304
|
+
- `last_message` — most recent message preview, including `author_name` when the sender's nickname is known from the chat list (otherwise `null`)
|
|
305
|
+
|
|
306
|
+
### Member Commands
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# List all members of a chat room (uses LOCO GETMEM — one call per invocation)
|
|
310
|
+
agent-kakaotalk member list <chat-id>
|
|
311
|
+
agent-kakaotalk member list <chat-id> --pretty
|
|
312
|
+
agent-kakaotalk member list <chat-id> --account <account-id>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Each member includes:
|
|
316
|
+
- `user_id` — numeric user ID (string for safety)
|
|
317
|
+
- `nickname` — display name in this chat (open chats may differ from the user's main Kakao nickname)
|
|
318
|
+
- `profile_image_url`, `full_profile_image_url`, `original_profile_image_url`
|
|
319
|
+
- `status_message`, `country_iso`
|
|
320
|
+
- `user_type` — KakaoTalk's user type (100 = friend, 1000 = open profile, etc.); `null` when the server omits the field
|
|
321
|
+
- `open_token`, `open_profile_link_id`, `open_permission` — open-chat-only fields (`null` for normal chats; `open_permission` is 1=OWNER, 2=NONE, 4=MANAGER, 8=BOT)
|
|
322
|
+
|
|
323
|
+
> **SDK-only**: `KakaoTalkClient.getMembersByIds(chatId, userIds)` is available for the >100-member case where you already have specific user IDs to resolve (typically from a CHATONROOM `mi` array). It is intentionally not exposed via the CLI because acquiring those IDs requires a CHATONROOM call that is also SDK-only. Use `agent-kakaotalk member list` for the common case.
|
|
300
324
|
|
|
301
325
|
### Message Commands
|
|
302
326
|
|
|
@@ -322,6 +346,7 @@ Each message includes:
|
|
|
322
346
|
- `log_id` — unique message identifier
|
|
323
347
|
- `type` — message type (1 = text, 2 = photo, etc.)
|
|
324
348
|
- `author_id` — sender's user ID
|
|
349
|
+
- `author_name` — sender's nickname when known from the chat list (otherwise `null`; only the room's "display members" are cached)
|
|
325
350
|
- `message` — message text content
|
|
326
351
|
- `sent_at` — Unix timestamp (milliseconds)
|
|
327
352
|
|
|
@@ -354,10 +379,12 @@ All commands output JSON by default for AI consumption:
|
|
|
354
379
|
"chat_id": "9876543210",
|
|
355
380
|
"type": 2,
|
|
356
381
|
"display_name": "Alice, Bob",
|
|
382
|
+
"title": null,
|
|
357
383
|
"active_members": 3,
|
|
358
384
|
"unread_count": 5,
|
|
359
385
|
"last_message": {
|
|
360
|
-
"author_id":
|
|
386
|
+
"author_id": 1111111111,
|
|
387
|
+
"author_name": "Alice",
|
|
361
388
|
"message": "Hello everyone!",
|
|
362
389
|
"sent_at": 1705312200000
|
|
363
390
|
}
|
|
@@ -55,6 +55,8 @@ agent-kakaotalk message send "$TARGET_CHAT" "Hey Alice!"
|
|
|
55
55
|
|
|
56
56
|
**When to use**: First time interacting with a chat, or when the user references a chat by name.
|
|
57
57
|
|
|
58
|
+
> Note: `display_name` joins the chat's member nicknames. For the user-set room title (matching the KakaoTalk app), see [Pattern 9](#pattern-9-resolve-canonical-room-titles).
|
|
59
|
+
|
|
58
60
|
## Pattern 3: Monitor Chat for New Messages
|
|
59
61
|
|
|
60
62
|
**Use case**: Watch a chat room and respond to new messages
|
|
@@ -221,7 +223,53 @@ echo "$UNREAD" | jq -r '.[] | " \(.display_name // "Unknown") — \(.unread_cou
|
|
|
221
223
|
|
|
222
224
|
**When to use**: Morning catch-up, checking for urgent messages, triage.
|
|
223
225
|
|
|
224
|
-
## Pattern 9:
|
|
226
|
+
## Pattern 9: Resolve Canonical Room Titles
|
|
227
|
+
|
|
228
|
+
**Use case**: Show user-set room names (matching the official KakaoTalk app) instead of comma-joined member nicknames
|
|
229
|
+
|
|
230
|
+
By default, `chat list` returns `display_name` built from the chat's "display members" (a comma-joined nickname list — e.g. `"Alice, Bob, Charlie"`). The `--resolve-titles` flag fetches each chat's user-set title via the LOCO `CHATINFO` command and surfaces it in a separate `title` field.
|
|
231
|
+
|
|
232
|
+
For open chats (`OM` / `OD`) without a user-set title, the CLI additionally consults the OpenLink record via `INFOLINK` and uses the link name as a fallback. This matches what KakaoTalk shows for open-chat rooms in the app sidebar.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
#!/bin/bash
|
|
236
|
+
|
|
237
|
+
# Without --resolve-titles: title is null, display_name is member nicknames
|
|
238
|
+
agent-kakaotalk chat list | jq '.[0] | {chat_id, display_name, title}'
|
|
239
|
+
# {
|
|
240
|
+
# "chat_id": "9876543210",
|
|
241
|
+
# "display_name": "Alice, Bob, Charlie",
|
|
242
|
+
# "title": null
|
|
243
|
+
# }
|
|
244
|
+
|
|
245
|
+
# With --resolve-titles: title is the user-set room name
|
|
246
|
+
agent-kakaotalk chat list --resolve-titles | jq '.[0] | {chat_id, display_name, title}'
|
|
247
|
+
# {
|
|
248
|
+
# "chat_id": "9876543210",
|
|
249
|
+
# "display_name": "Alice, Bob, Charlie",
|
|
250
|
+
# "title": "Project Standup"
|
|
251
|
+
# }
|
|
252
|
+
|
|
253
|
+
# Render the best available name (title preferred, display_name fallback)
|
|
254
|
+
agent-kakaotalk chat list --resolve-titles \
|
|
255
|
+
| jq -r '.[] | "\(.chat_id): \(.title // .display_name // "Untitled")"'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**When to use**: User-facing chat pickers, room-name displays in summaries, anywhere you want output that matches what the user sees in the KakaoTalk app.
|
|
259
|
+
|
|
260
|
+
**Cost**: One extra LOCO call per chat (CHATINFO). Open chats without a user-set title pay one additional call (INFOLINK). For large account snapshots this multiplies quickly — leave the flag off for hot paths.
|
|
261
|
+
|
|
262
|
+
**SDK equivalent**:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Resolve titles for the whole list
|
|
266
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
267
|
+
|
|
268
|
+
// Single-chat lookup (returns null on error or missing title)
|
|
269
|
+
const title = await client.getChatTitle('9876543210')
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Pattern 10: Error Handling and Retry
|
|
225
273
|
|
|
226
274
|
**Use case**: Robust message sending with retries
|
|
227
275
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-slackbot
|
|
3
3
|
description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.13.1
|
|
5
5
|
allowed-tools: Bash(agent-slackbot:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -365,7 +365,6 @@ Credentials stored in `~/.config/agent-messenger/slackbot-credentials.json` (060
|
|
|
365
365
|
- Bot can only edit/delete its own messages
|
|
366
366
|
- Bot must be invited to private channels
|
|
367
367
|
- No scheduled messages
|
|
368
|
-
- Plain text messages only (no blocks/formatting)
|
|
369
368
|
|
|
370
369
|
## Troubleshooting
|
|
371
370
|
|
|
@@ -17,6 +17,12 @@ import {
|
|
|
17
17
|
DiscordReactionSchema,
|
|
18
18
|
DiscordUserSchema,
|
|
19
19
|
} from '@/platforms/discordbot/index'
|
|
20
|
+
import type {
|
|
21
|
+
DiscordBotListenerOptions,
|
|
22
|
+
DiscordGatewayEmbed,
|
|
23
|
+
DiscordGatewayMessageCreateEvent,
|
|
24
|
+
DiscordGatewayStickerItem,
|
|
25
|
+
} from '@/platforms/discordbot/index'
|
|
20
26
|
|
|
21
27
|
it('DiscordBotClient is exported from barrel', () => {
|
|
22
28
|
expect(typeof DiscordBotClient).toBe('function')
|
|
@@ -34,6 +40,11 @@ it('DiscordBotListener is exported from barrel', () => {
|
|
|
34
40
|
expect(typeof DiscordBotListener).toBe('function')
|
|
35
41
|
})
|
|
36
42
|
|
|
43
|
+
it('DiscordBotListenerOptions type is exported from barrel', () => {
|
|
44
|
+
const options: DiscordBotListenerOptions = { intents: 0 }
|
|
45
|
+
expect(options.intents).toBe(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
37
48
|
it('DiscordGatewayOpcode is exported from barrel', () => {
|
|
38
49
|
expect(DiscordGatewayOpcode.Identify).toBe(2)
|
|
39
50
|
expect(DiscordGatewayOpcode.Hello).toBe(10)
|
|
@@ -80,3 +91,68 @@ it('DiscordReactionSchema is exported from barrel', () => {
|
|
|
80
91
|
it('DiscordFileSchema is exported from barrel', () => {
|
|
81
92
|
expect(typeof DiscordFileSchema.parse).toBe('function')
|
|
82
93
|
})
|
|
94
|
+
|
|
95
|
+
it('DiscordGatewayEmbed type is exported from barrel', () => {
|
|
96
|
+
const embed: DiscordGatewayEmbed = {
|
|
97
|
+
type: 'rich',
|
|
98
|
+
title: 'Release notes',
|
|
99
|
+
description: 'v2.13.1 is out',
|
|
100
|
+
url: 'https://example.com/release',
|
|
101
|
+
}
|
|
102
|
+
expect(embed.title).toBe('Release notes')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('DiscordGatewayStickerItem type is exported from barrel', () => {
|
|
106
|
+
const sticker: DiscordGatewayStickerItem = { id: '1', name: 'wave', format_type: 1 }
|
|
107
|
+
expect(sticker.format_type).toBe(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('DiscordGatewayMessageCreateEvent accepts the new optional fields', () => {
|
|
111
|
+
const event: DiscordGatewayMessageCreateEvent = {
|
|
112
|
+
type: 'MESSAGE_CREATE',
|
|
113
|
+
id: 'm1',
|
|
114
|
+
channel_id: 'C1',
|
|
115
|
+
author: { id: 'U1', username: 'alice', global_name: 'Alice', bot: false },
|
|
116
|
+
content: 'hello',
|
|
117
|
+
timestamp: '2026-05-11T00:00:00Z',
|
|
118
|
+
mention_everyone: false,
|
|
119
|
+
mention_roles: ['R1', 'R2'],
|
|
120
|
+
message_reference: { message_id: 'm0', channel_id: 'C1', guild_id: 'G1' },
|
|
121
|
+
embeds: [{ type: 'rich', title: 't', description: 'd', url: 'https://example.com' }],
|
|
122
|
+
sticker_items: [{ id: 's1', name: 'wave', format_type: 1 }],
|
|
123
|
+
}
|
|
124
|
+
expect(event.mention_everyone).toBe(false)
|
|
125
|
+
expect(event.mention_roles).toEqual(['R1', 'R2'])
|
|
126
|
+
expect(event.message_reference?.message_id).toBe('m0')
|
|
127
|
+
expect(event.embeds?.[0]?.title).toBe('t')
|
|
128
|
+
expect(event.sticker_items?.[0]?.name).toBe('wave')
|
|
129
|
+
expect(event.author.global_name).toBe('Alice')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('DiscordGatewayMessageCreateEvent treats new fields as optional', () => {
|
|
133
|
+
const event: DiscordGatewayMessageCreateEvent = {
|
|
134
|
+
type: 'MESSAGE_CREATE',
|
|
135
|
+
id: 'm1',
|
|
136
|
+
channel_id: 'C1',
|
|
137
|
+
author: { id: 'U1', username: 'alice' },
|
|
138
|
+
content: 'hello',
|
|
139
|
+
timestamp: '2026-05-11T00:00:00Z',
|
|
140
|
+
}
|
|
141
|
+
expect(event.mention_everyone).toBeUndefined()
|
|
142
|
+
expect(event.embeds).toBeUndefined()
|
|
143
|
+
expect(event.sticker_items).toBeUndefined()
|
|
144
|
+
expect(event.message_reference).toBeUndefined()
|
|
145
|
+
expect(event.author.global_name).toBeUndefined()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('DiscordGatewayMessageCreateEvent author.global_name accepts null', () => {
|
|
149
|
+
const event: DiscordGatewayMessageCreateEvent = {
|
|
150
|
+
type: 'MESSAGE_CREATE',
|
|
151
|
+
id: 'm1',
|
|
152
|
+
channel_id: 'C1',
|
|
153
|
+
author: { id: 'U1', username: 'alice', global_name: null },
|
|
154
|
+
content: 'hello',
|
|
155
|
+
timestamp: '2026-05-11T00:00:00Z',
|
|
156
|
+
}
|
|
157
|
+
expect(event.author.global_name).toBeNull()
|
|
158
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { DiscordBotClient } from './client'
|
|
2
2
|
export { DiscordBotCredentialManager } from './credential-manager'
|
|
3
3
|
export { DiscordBotListener } from './listener'
|
|
4
|
+
export type { DiscordBotListenerOptions } from './listener'
|
|
4
5
|
export type {
|
|
5
6
|
DiscordBotConfig,
|
|
6
7
|
DiscordBotCredentials,
|
|
@@ -9,6 +10,7 @@ export type {
|
|
|
9
10
|
DiscordChannel,
|
|
10
11
|
DiscordFile,
|
|
11
12
|
DiscordGatewayChannelEvent,
|
|
13
|
+
DiscordGatewayEmbed,
|
|
12
14
|
DiscordGatewayEvent,
|
|
13
15
|
DiscordGatewayGenericEvent,
|
|
14
16
|
DiscordGatewayGuildEvent,
|
|
@@ -19,6 +21,7 @@ export type {
|
|
|
19
21
|
DiscordGatewayMessageUpdateEvent,
|
|
20
22
|
DiscordGatewayPresenceEvent,
|
|
21
23
|
DiscordGatewayReactionEvent,
|
|
24
|
+
DiscordGatewayStickerItem,
|
|
22
25
|
DiscordGatewayTypingEvent,
|
|
23
26
|
DiscordGuild,
|
|
24
27
|
DiscordMessage,
|
|
@@ -235,6 +235,47 @@ describe('DiscordBotListener', () => {
|
|
|
235
235
|
expect(messages[0].channel_id).toBe('C123')
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
+
it('surfaces global_name, mentions, message_reference, embeds, and sticker_items', async () => {
|
|
239
|
+
const client = createMockClient()
|
|
240
|
+
listener = new DiscordBotListener(client)
|
|
241
|
+
|
|
242
|
+
const messages: DiscordGatewayMessageCreateEvent[] = []
|
|
243
|
+
listener.on('message_create', (event) => messages.push(event))
|
|
244
|
+
|
|
245
|
+
await listener.start()
|
|
246
|
+
mockWsInstance.simulateOpen()
|
|
247
|
+
mockWsInstance.simulateHello()
|
|
248
|
+
mockWsInstance.simulateReady()
|
|
249
|
+
mockWsInstance.simulateDispatch(
|
|
250
|
+
'MESSAGE_CREATE',
|
|
251
|
+
{
|
|
252
|
+
id: 'msg_2',
|
|
253
|
+
channel_id: 'C123',
|
|
254
|
+
author: { id: 'U456', username: 'user', global_name: 'User Display' },
|
|
255
|
+
content: 'reply text',
|
|
256
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
257
|
+
mention_everyone: false,
|
|
258
|
+
mention_roles: ['R1'],
|
|
259
|
+
message_reference: { message_id: 'msg_orig', channel_id: 'C123', guild_id: 'G1' },
|
|
260
|
+
embeds: [{ type: 'rich', title: 'embed-title', description: 'embed-desc', url: 'https://example.com' }],
|
|
261
|
+
sticker_items: [{ id: 'sk1', name: 'wave', format_type: 1 }],
|
|
262
|
+
},
|
|
263
|
+
2,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
expect(messages.length).toBe(1)
|
|
267
|
+
const event = messages[0]
|
|
268
|
+
expect(event.author.global_name).toBe('User Display')
|
|
269
|
+
expect(event.mention_everyone).toBe(false)
|
|
270
|
+
expect(event.mention_roles).toEqual(['R1'])
|
|
271
|
+
expect(event.message_reference?.message_id).toBe('msg_orig')
|
|
272
|
+
expect(event.message_reference?.guild_id).toBe('G1')
|
|
273
|
+
expect(event.embeds?.[0]?.title).toBe('embed-title')
|
|
274
|
+
expect(event.embeds?.[0]?.url).toBe('https://example.com')
|
|
275
|
+
expect(event.sticker_items?.[0]?.name).toBe('wave')
|
|
276
|
+
expect(event.sticker_items?.[0]?.format_type).toBe(1)
|
|
277
|
+
})
|
|
278
|
+
|
|
238
279
|
it('emits message_update events', async () => {
|
|
239
280
|
const client = createMockClient()
|
|
240
281
|
listener = new DiscordBotListener(client)
|
|
@@ -24,6 +24,10 @@ const DEFAULT_INTENTS =
|
|
|
24
24
|
|
|
25
25
|
type EventKey = keyof DiscordBotListenerEventMap
|
|
26
26
|
|
|
27
|
+
export interface DiscordBotListenerOptions {
|
|
28
|
+
intents?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
export class DiscordBotListener {
|
|
28
32
|
private client: DiscordBotClient
|
|
29
33
|
private intents: number
|
|
@@ -43,7 +47,7 @@ export class DiscordBotListener {
|
|
|
43
47
|
private cachedUser: { id: string; username: string } | null = null
|
|
44
48
|
private generation = 0
|
|
45
49
|
|
|
46
|
-
constructor(client: DiscordBotClient, options?:
|
|
50
|
+
constructor(client: DiscordBotClient, options?: DiscordBotListenerOptions) {
|
|
47
51
|
this.client = client
|
|
48
52
|
this.intents = options?.intents ?? DEFAULT_INTENTS
|
|
49
53
|
}
|
|
@@ -224,17 +224,39 @@ export const DiscordIntent = {
|
|
|
224
224
|
AutoModerationExecution: 1 << 21,
|
|
225
225
|
} as const
|
|
226
226
|
|
|
227
|
+
export interface DiscordGatewayEmbed {
|
|
228
|
+
type?: string
|
|
229
|
+
title?: string
|
|
230
|
+
description?: string
|
|
231
|
+
url?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface DiscordGatewayStickerItem {
|
|
235
|
+
id: string
|
|
236
|
+
name: string
|
|
237
|
+
format_type?: number
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
export interface DiscordGatewayMessageCreateEvent {
|
|
228
241
|
type: 'MESSAGE_CREATE'
|
|
229
242
|
id: string
|
|
230
243
|
channel_id: string
|
|
231
244
|
guild_id?: string
|
|
232
|
-
author: { id: string; username: string; bot?: boolean }
|
|
245
|
+
author: { id: string; username: string; global_name?: string | null; bot?: boolean }
|
|
233
246
|
content: string
|
|
234
247
|
timestamp: string
|
|
235
248
|
edited_timestamp?: string
|
|
236
249
|
mentions?: DiscordUser[]
|
|
250
|
+
mention_everyone?: boolean
|
|
251
|
+
mention_roles?: string[]
|
|
252
|
+
message_reference?: {
|
|
253
|
+
message_id?: string
|
|
254
|
+
channel_id?: string
|
|
255
|
+
guild_id?: string
|
|
256
|
+
}
|
|
237
257
|
attachments?: DiscordFile[]
|
|
258
|
+
embeds?: DiscordGatewayEmbed[]
|
|
259
|
+
sticker_items?: DiscordGatewayStickerItem[]
|
|
238
260
|
}
|
|
239
261
|
|
|
240
262
|
export interface DiscordGatewayMessageUpdateEvent {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { classifyKakaoChat } from './chat-classifier'
|
|
4
|
+
|
|
5
|
+
describe('classifyKakaoChat', () => {
|
|
6
|
+
it('classifies a normal DM (type 11, 2 members) as dm', () => {
|
|
7
|
+
expect(classifyKakaoChat({ type: 11, active_members: 2 })).toBe('dm')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('classifies a normal group (type 10, 5 members) as group', () => {
|
|
11
|
+
expect(classifyKakaoChat({ type: 10, active_members: 5 })).toBe('group')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('classifies legacy DM (type 9, 2 members) as dm via member-count fallback', () => {
|
|
15
|
+
expect(classifyKakaoChat({ type: 9, active_members: 2 })).toBe('dm')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('classifies a 3-member group (type 10) as group, not dm', () => {
|
|
19
|
+
expect(classifyKakaoChat({ type: 10, active_members: 3 })).toBe('group')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
for (const type of [2, 13, 14, 15, 16]) {
|
|
23
|
+
it(`classifies open-chat type ${type} as open regardless of member count`, () => {
|
|
24
|
+
expect(classifyKakaoChat({ type, active_members: 1 })).toBe('open')
|
|
25
|
+
expect(classifyKakaoChat({ type, active_members: 2 })).toBe('open')
|
|
26
|
+
expect(classifyKakaoChat({ type, active_members: 50 })).toBe('open')
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('classifies a 1-member chat (lone room) as dm', () => {
|
|
31
|
+
expect(classifyKakaoChat({ type: 11, active_members: 1 })).toBe('dm')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { KakaoChat } from './types'
|
|
2
|
+
|
|
3
|
+
export type KakaoChatKind = 'dm' | 'group' | 'open' | 'unknown'
|
|
4
|
+
|
|
5
|
+
// OpenChat-family `type` codes observed on the wire. KakaoTalk's LOCO
|
|
6
|
+
// protocol exposes a numeric `type` field with no documented mapping; these
|
|
7
|
+
// five codes consistently identify OpenChat rooms across normal OpenChat,
|
|
8
|
+
// OpenChat DMs, and the various OpenChat sub-types seen in production.
|
|
9
|
+
const OPEN_CHAT_TYPE_CODES: ReadonlySet<number> = new Set([2, 13, 14, 15, 16])
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Classify a KakaoTalk chat as `'dm'`, `'group'`, `'open'`, or `'unknown'`.
|
|
13
|
+
*
|
|
14
|
+
* REGRESSION GUARD: An earlier implementation hard-coded `0=dm`, `1=group`,
|
|
15
|
+
* `2=open` on the raw `type` number. Modern KakaoTalk uses codes like `11`
|
|
16
|
+
* for normal DMs and `10` for normal groups, so the old mapping silently
|
|
17
|
+
* classified every real DM as `'unknown'` and bucketed it as a group. Do
|
|
18
|
+
* NOT "simplify" this back to a pure type-code mapping without verifying
|
|
19
|
+
* against a real KakaoTalk session.
|
|
20
|
+
*
|
|
21
|
+
* `'unknown'` is reserved for future protocol drift; the current heuristic
|
|
22
|
+
* never returns it, but it is part of the union so consumers can handle
|
|
23
|
+
* the case defensively.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyKakaoChat(chat: Pick<KakaoChat, 'type' | 'active_members'>): KakaoChatKind {
|
|
26
|
+
if (OPEN_CHAT_TYPE_CODES.has(chat.type)) return 'open'
|
|
27
|
+
// active_members counts the logged-in user, so a 1:1 DM is exactly 2
|
|
28
|
+
// (self + one other) and a "lone" room with only self is 1.
|
|
29
|
+
if (chat.active_members <= 2) return 'dm'
|
|
30
|
+
return 'group'
|
|
31
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { Command as CommandType } from 'commander'
|
|
|
4
4
|
import { Command } from 'commander'
|
|
5
5
|
|
|
6
6
|
import pkg from '../../../package.json' with { type: 'json' }
|
|
7
|
-
import { authCommand, chatCommand, messageCommand, whoamiCommand } from './commands/index'
|
|
7
|
+
import { authCommand, chatCommand, memberCommand, messageCommand, whoamiCommand } from './commands/index'
|
|
8
8
|
import { ensureKakaoAuth } from './ensure-auth'
|
|
9
9
|
|
|
10
10
|
function isAuthCommand(command: CommandType): boolean {
|
|
@@ -30,6 +30,7 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
|
30
30
|
|
|
31
31
|
program.addCommand(authCommand)
|
|
32
32
|
program.addCommand(chatCommand)
|
|
33
|
+
program.addCommand(memberCommand)
|
|
33
34
|
program.addCommand(messageCommand)
|
|
34
35
|
program.addCommand(whoamiCommand)
|
|
35
36
|
|