agent-messenger 2.30.0 → 2.32.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 +14 -1
- package/bun.lock +42 -2
- package/dist/package.json +2 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/channeltalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/client.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/bot.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/chat.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/group.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/manager.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/message.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/shared.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/whoami.js.map +1 -1
- package/dist/src/platforms/channeltalk/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/channeltalk/types.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/client.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/client.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/types.d.ts +1 -1
- package/dist/src/platforms/channeltalkbot/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/cli.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.d.ts +11 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +145 -21
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/mention.d.ts +5 -0
- package/dist/src/platforms/discord/commands/mention.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/mention.js +60 -4
- package/dist/src/platforms/discord/commands/mention.js.map +1 -1
- package/dist/src/platforms/discord/commands/reaction.js +1 -1
- package/dist/src/platforms/discord/commands/reaction.js.map +1 -1
- package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.d.ts +1 -1
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/listener.d.ts +4 -1
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +28 -1
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discord/remote-auth.js.map +1 -1
- package/dist/src/platforms/discord/super-properties.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +56 -9
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js +28 -7
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/credential-manager.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/src/platforms/imessage/cli.d.ts.map +1 -1
- package/dist/src/platforms/imessage/client.d.ts.map +1 -1
- package/dist/src/platforms/imessage/client.js.map +1 -1
- package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/imessage/errors.js.map +1 -1
- package/dist/src/platforms/imessage/rpc.d.ts.map +1 -1
- package/dist/src/platforms/imessage/rpc.js.map +1 -1
- package/dist/src/platforms/imessage/types.d.ts.map +1 -1
- package/dist/src/platforms/imessage/types.js.map +1 -1
- package/dist/src/platforms/instagram/cli.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -1
- package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -1
- package/dist/src/platforms/instagram/listener.d.ts.map +1 -1
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -1
- package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -1
- package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -1
- package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -1
- package/dist/src/platforms/instagram/realtime-listener.js.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +5 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +55 -0
- 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 +16 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
- package/dist/src/platforms/kakaotalk/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +1 -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.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/crypto.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +23 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +50 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +30 -10
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +10 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/line/cli.d.ts.map +1 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/line/listener.d.ts.map +1 -1
- package/dist/src/platforms/line/types.d.ts +6 -6
- package/dist/src/platforms/line/types.d.ts.map +1 -1
- package/dist/src/platforms/slack/cli.d.ts.map +1 -1
- package/dist/src/platforms/slack/client.d.ts.map +1 -1
- package/dist/src/platforms/slack/client.js.map +1 -1
- package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
- package/dist/src/platforms/slack/listener.d.ts.map +1 -1
- package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/listener.js.map +1 -1
- package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/app-config.js.map +1 -1
- package/dist/src/platforms/teams/cli.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/device-code.js.map +1 -1
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -1
- package/dist/src/platforms/teams/device-login.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-provider.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +14 -14
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/telegram/cli.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/shared.js.map +1 -1
- package/dist/src/platforms/telegram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/telegram/tdlib.d.ts.map +1 -1
- package/dist/src/platforms/telegram/types.d.ts.map +1 -1
- package/dist/src/platforms/telegram/types.js.map +1 -1
- package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/client.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/cli.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -1
- package/dist/src/platforms/webex/listener.d.ts.map +1 -1
- package/dist/src/platforms/webex/listener.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +4 -4
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/types.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/client.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/types.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/cli.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/types.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/types.js.map +1 -1
- package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/types.d.ts.map +1 -1
- package/dist/src/shared/chromium/browsers.js.map +1 -1
- package/dist/src/shared/chromium/cli-options.js.map +1 -1
- package/dist/src/shared/chromium/cookie-reader.d.ts.map +1 -1
- package/dist/src/shared/chromium/decryptor.d.ts.map +1 -1
- package/dist/src/shared/sqlite.js.map +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
- package/dist/src/tui/adapters/channeltalk-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/discord-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/line-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/slack-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/telegram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/telegram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/whatsapp-adapter.d.ts.map +1 -1
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/views/channel-picker.d.ts.map +1 -1
- package/dist/src/tui/views/workspace-picker.d.ts.map +1 -1
- package/docs/content/docs/cli/discord.mdx +7 -0
- package/docs/content/docs/cli/kakaotalk.mdx +16 -1
- package/docs/content/docs/sdk/discord.mdx +38 -0
- package/docs/content/docs/sdk/kakaotalk.mdx +39 -0
- package/e2e/README.md +2 -2
- package/e2e/discord.e2e.test.ts +16 -2
- package/package.json +2 -2
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +19 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +19 -2
- 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 +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +351 -9
- package/src/platforms/discord/client.ts +166 -26
- package/src/platforms/discord/commands/mention.test.ts +84 -0
- package/src/platforms/discord/commands/mention.ts +78 -15
- package/src/platforms/discord/commands/reaction.test.ts +1 -1
- package/src/platforms/discord/commands/reaction.ts +1 -1
- package/src/platforms/discord/index.ts +3 -0
- package/src/platforms/discord/listener.test.ts +69 -2
- package/src/platforms/discord/listener.ts +31 -2
- package/src/platforms/discord/types.test.ts +11 -0
- package/src/platforms/discord/types.ts +66 -10
- package/src/platforms/kakaotalk/client.test.ts +194 -1
- package/src/platforms/kakaotalk/client.ts +58 -0
- package/src/platforms/kakaotalk/commands/chat.test.ts +24 -0
- package/src/platforms/kakaotalk/commands/chat.ts +23 -0
- package/src/platforms/kakaotalk/index.test.ts +10 -0
- package/src/platforms/kakaotalk/index.ts +4 -0
- package/src/platforms/kakaotalk/protocol/session.test.ts +113 -0
- package/src/platforms/kakaotalk/protocol/session.ts +56 -0
- package/src/platforms/kakaotalk/types.ts +24 -0
- package/tsconfig.json +8 -8
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import { DiscordClient } from '../client'
|
|
4
4
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
5
|
+
import { unreadAction } from './mention'
|
|
5
6
|
|
|
6
7
|
let clientGetMentionsSpy: ReturnType<typeof spyOn>
|
|
7
8
|
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
@@ -128,3 +129,86 @@ it('getMentions: includes mention metadata', async () => {
|
|
|
128
129
|
expect(mention.mention_everyone).toBeDefined()
|
|
129
130
|
expect(mention.mentions).toBeDefined()
|
|
130
131
|
})
|
|
132
|
+
|
|
133
|
+
it('unread command: emits snake_case JSON and forwards guild/limit options', async () => {
|
|
134
|
+
// given: an authenticated client whose getUnreadMentions returns one unread mention
|
|
135
|
+
credManagerLoadSpy.mockResolvedValue({
|
|
136
|
+
token: 'test-token',
|
|
137
|
+
current_server: null,
|
|
138
|
+
servers: {},
|
|
139
|
+
})
|
|
140
|
+
const loginSpy = spyOn(DiscordClient.prototype, 'login').mockImplementation(async function (this: DiscordClient) {
|
|
141
|
+
return this
|
|
142
|
+
})
|
|
143
|
+
const getUnreadSpy = spyOn(DiscordClient.prototype, 'getUnreadMentions').mockResolvedValue({
|
|
144
|
+
mentions: [
|
|
145
|
+
{
|
|
146
|
+
id: 'msg-1',
|
|
147
|
+
channel_id: 'ch-1',
|
|
148
|
+
author: { id: 'user-1', username: 'alice' },
|
|
149
|
+
content: 'Hey @testuser',
|
|
150
|
+
timestamp: '2024-01-29T10:00:00Z',
|
|
151
|
+
mention_everyone: false,
|
|
152
|
+
mentions: [{ id: 'user-me', username: 'testuser' }],
|
|
153
|
+
guild_id: 'guild-1',
|
|
154
|
+
mention_count: 2,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
count: 1,
|
|
158
|
+
badgeCount: 2,
|
|
159
|
+
complete: true,
|
|
160
|
+
windowDays: 7,
|
|
161
|
+
})
|
|
162
|
+
const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
163
|
+
|
|
164
|
+
// when: running the unread action with guild and limit
|
|
165
|
+
await unreadAction({ guild: 'guild-1', limit: 200 })
|
|
166
|
+
|
|
167
|
+
// then: options are forwarded and the emitted JSON uses documented snake_case keys
|
|
168
|
+
expect(getUnreadSpy).toHaveBeenCalledWith({ guildId: 'guild-1', limit: 200 })
|
|
169
|
+
const lastCall = consoleLogSpy.mock.calls.at(-1) as [string]
|
|
170
|
+
const emitted = JSON.parse(lastCall[0])
|
|
171
|
+
expect(emitted).toEqual({
|
|
172
|
+
mentions: [
|
|
173
|
+
{
|
|
174
|
+
id: 'msg-1',
|
|
175
|
+
channel_id: 'ch-1',
|
|
176
|
+
author: 'alice',
|
|
177
|
+
content: 'Hey @testuser',
|
|
178
|
+
timestamp: '2024-01-29T10:00:00Z',
|
|
179
|
+
mention_everyone: false,
|
|
180
|
+
mentioned_users: ['testuser'],
|
|
181
|
+
guild_id: 'guild-1',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
count: 1,
|
|
185
|
+
badge_count: 2,
|
|
186
|
+
complete: true,
|
|
187
|
+
window_days: 7,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
loginSpy.mockRestore()
|
|
191
|
+
getUnreadSpy.mockRestore()
|
|
192
|
+
consoleLogSpy.mockRestore()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('unread command: exits with error when not authenticated', async () => {
|
|
196
|
+
// given: no token in credentials
|
|
197
|
+
credManagerLoadSpy.mockResolvedValue({
|
|
198
|
+
token: undefined,
|
|
199
|
+
current_server: null,
|
|
200
|
+
servers: {},
|
|
201
|
+
})
|
|
202
|
+
const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
203
|
+
const processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
|
|
204
|
+
|
|
205
|
+
// when: running the unread action
|
|
206
|
+
await unreadAction({})
|
|
207
|
+
|
|
208
|
+
// then: it reports the auth error and exits
|
|
209
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify({ error: 'Not authenticated. Run "auth extract" first.' }))
|
|
210
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
211
|
+
|
|
212
|
+
consoleLogSpy.mockRestore()
|
|
213
|
+
processExitSpy.mockRestore()
|
|
214
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
1
|
+
import { Command, InvalidArgumentError } from 'commander'
|
|
2
2
|
|
|
3
3
|
import { handleError } from '@/shared/utils/error-handler'
|
|
4
4
|
import { formatOutput } from '@/shared/utils/output'
|
|
@@ -6,6 +6,14 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
6
6
|
import { DiscordClient } from '../client'
|
|
7
7
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
8
8
|
|
|
9
|
+
function parseLimit(value: string): number {
|
|
10
|
+
const parsed = Number(value)
|
|
11
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
12
|
+
throw new InvalidArgumentError('Must be a positive integer.')
|
|
13
|
+
}
|
|
14
|
+
return parsed
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export async function listAction(options: { limit?: number; guild?: string; pretty?: boolean }): Promise<void> {
|
|
10
18
|
try {
|
|
11
19
|
const credManager = new DiscordCredentialManager()
|
|
@@ -39,17 +47,72 @@ export async function listAction(options: { limit?: number; guild?: string; pret
|
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
export async function unreadAction(options: { guild?: string; limit?: number; pretty?: boolean }): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const credManager = new DiscordCredentialManager()
|
|
53
|
+
const config = await credManager.load()
|
|
54
|
+
|
|
55
|
+
if (!config.token) {
|
|
56
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
61
|
+
const result = await client.getUnreadMentions({
|
|
62
|
+
guildId: options.guild,
|
|
63
|
+
limit: options.limit,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const output = {
|
|
67
|
+
mentions: result.mentions.map((mention) => ({
|
|
68
|
+
id: mention.id,
|
|
69
|
+
channel_id: mention.channel_id,
|
|
70
|
+
author: mention.author.username,
|
|
71
|
+
content: mention.content,
|
|
72
|
+
timestamp: mention.timestamp,
|
|
73
|
+
mention_everyone: mention.mention_everyone,
|
|
74
|
+
mentioned_users: mention.mentions.map((u) => u.username),
|
|
75
|
+
guild_id: mention.guild_id || null,
|
|
76
|
+
})),
|
|
77
|
+
count: result.count,
|
|
78
|
+
badge_count: result.badgeCount,
|
|
79
|
+
complete: result.complete,
|
|
80
|
+
window_days: result.windowDays,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(formatOutput(output, options.pretty))
|
|
84
|
+
} catch (error) {
|
|
85
|
+
handleError(error as Error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const mentionCommand = new Command('mention')
|
|
90
|
+
.description('Mention commands')
|
|
91
|
+
.addCommand(
|
|
92
|
+
new Command('list')
|
|
93
|
+
.description('List mentions')
|
|
94
|
+
.option('--limit <n>', 'Number of mentions to fetch', parseLimit, 25)
|
|
95
|
+
.option('--guild <guild-id>', 'Filter by guild ID')
|
|
96
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
97
|
+
.action((options) => {
|
|
98
|
+
listAction({
|
|
99
|
+
limit: options.limit,
|
|
100
|
+
guild: options.guild,
|
|
101
|
+
pretty: options.pretty,
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
.addCommand(
|
|
106
|
+
new Command('unread')
|
|
107
|
+
.description('List unread mentions (correlates mention history with per-channel read state)')
|
|
108
|
+
.option('--limit <n>', 'Max number of mentions to scan', parseLimit)
|
|
109
|
+
.option('--guild <guild-id>', 'Filter by guild ID')
|
|
110
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
111
|
+
.action((options) => {
|
|
112
|
+
unreadAction({
|
|
113
|
+
limit: options.limit,
|
|
114
|
+
guild: options.guild,
|
|
115
|
+
pretty: options.pretty,
|
|
116
|
+
})
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
@@ -25,10 +25,13 @@ export type {
|
|
|
25
25
|
DiscordMention,
|
|
26
26
|
DiscordMessage,
|
|
27
27
|
DiscordReaction,
|
|
28
|
+
DiscordReadState,
|
|
28
29
|
DiscordRelationship,
|
|
29
30
|
DiscordSearchOptions,
|
|
30
31
|
DiscordSearchResponse,
|
|
31
32
|
DiscordSearchResult,
|
|
33
|
+
DiscordUnreadMention,
|
|
34
|
+
DiscordUnreadMentionsResult,
|
|
32
35
|
DiscordUser,
|
|
33
36
|
DiscordUserNote,
|
|
34
37
|
DiscordUserProfile,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { DiscordListener } from '@/platforms/discord/listener'
|
|
3
|
+
import { DiscordListener, parseReadStates } from '@/platforms/discord/listener'
|
|
4
4
|
import type {
|
|
5
5
|
DiscordGatewayGenericEvent,
|
|
6
6
|
DiscordGatewayMessageCreateEvent,
|
|
@@ -68,7 +68,7 @@ class MockWs {
|
|
|
68
68
|
this.simulateMessage({ op: 10, d: { heartbeat_interval: interval } })
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
simulateReady(sessionId = 'session_123') {
|
|
71
|
+
simulateReady(sessionId = 'session_123', readState?: unknown) {
|
|
72
72
|
this.simulateMessage({
|
|
73
73
|
op: 0,
|
|
74
74
|
t: 'READY',
|
|
@@ -77,6 +77,7 @@ class MockWs {
|
|
|
77
77
|
session_id: sessionId,
|
|
78
78
|
resume_gateway_url: 'wss://resume.discord.gg',
|
|
79
79
|
user: { id: 'U_SELF', username: 'testbot' },
|
|
80
|
+
...(readState === undefined ? {} : { read_state: readState }),
|
|
80
81
|
},
|
|
81
82
|
})
|
|
82
83
|
}
|
|
@@ -135,6 +136,33 @@ async function startConnecting(listener: DiscordListener): Promise<MockWs> {
|
|
|
135
136
|
return ws
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
describe('parseReadStates', () => {
|
|
140
|
+
it('filters out non-channel read state types', () => {
|
|
141
|
+
const result = parseReadStates([
|
|
142
|
+
{ id: 'c1', last_message_id: '100', mention_count: 1 },
|
|
143
|
+
{ id: 'c2', read_state_type: 2, mention_count: 5 },
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('normalizes missing last_message_id and mention_count', () => {
|
|
150
|
+
const result = parseReadStates([{ id: 'c1' }])
|
|
151
|
+
|
|
152
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 0 }])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('skips malformed entries but keeps valid ones', () => {
|
|
156
|
+
const result = parseReadStates([{ id: 'c1', mention_count: 1 }, { mention_count: 3 }])
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 1 }])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('throws on a non-array, non-versioned payload', () => {
|
|
162
|
+
expect(() => parseReadStates({ foo: 'bar' })).toThrow(/invalid read_state/)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
138
166
|
describe('DiscordListener', () => {
|
|
139
167
|
let listener: DiscordListener
|
|
140
168
|
|
|
@@ -235,6 +263,45 @@ describe('DiscordListener', () => {
|
|
|
235
263
|
})
|
|
236
264
|
})
|
|
237
265
|
|
|
266
|
+
describe('read state', () => {
|
|
267
|
+
it('captures raw-array read_state from READY', async () => {
|
|
268
|
+
const client = createMockClient()
|
|
269
|
+
listener = new DiscordListener(client)
|
|
270
|
+
|
|
271
|
+
const started = listener.start()
|
|
272
|
+
const ws = await waitForSocket()
|
|
273
|
+
ws.simulateOpen()
|
|
274
|
+
ws.simulateHello()
|
|
275
|
+
ws.simulateReady('session_123', [{ id: 'c1', last_message_id: '100', mention_count: 2 }])
|
|
276
|
+
await started
|
|
277
|
+
|
|
278
|
+
expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 2 }])
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('captures versioned { entries } read_state from READY', async () => {
|
|
282
|
+
const client = createMockClient()
|
|
283
|
+
listener = new DiscordListener(client)
|
|
284
|
+
|
|
285
|
+
const started = listener.start()
|
|
286
|
+
const ws = await waitForSocket()
|
|
287
|
+
ws.simulateOpen()
|
|
288
|
+
ws.simulateHello()
|
|
289
|
+
ws.simulateReady('session_123', { version: 7, entries: [{ id: 'c1', last_message_id: '100', mention_count: 1 }] })
|
|
290
|
+
await started
|
|
291
|
+
|
|
292
|
+
expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('defaults to an empty read state when READY omits read_state', async () => {
|
|
296
|
+
const client = createMockClient()
|
|
297
|
+
listener = new DiscordListener(client)
|
|
298
|
+
|
|
299
|
+
await startAndReady(listener)
|
|
300
|
+
|
|
301
|
+
expect(listener.getReadState()).toEqual([])
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
238
305
|
describe('identify', () => {
|
|
239
306
|
it('sends Identify after Hello', async () => {
|
|
240
307
|
const client = createMockClient()
|
|
@@ -3,8 +3,30 @@ import { EventEmitter } from 'events'
|
|
|
3
3
|
import WebSocket from 'ws'
|
|
4
4
|
|
|
5
5
|
import type { DiscordClient } from './client'
|
|
6
|
-
import type { DiscordListenerEventMap, DiscordGatewayGenericEvent } from './types'
|
|
7
|
-
import { DiscordGatewayOpcode } from './types'
|
|
6
|
+
import type { DiscordListenerEventMap, DiscordGatewayGenericEvent, DiscordReadState } from './types'
|
|
7
|
+
import { DiscordGatewayOpcode, DiscordRawReadStateSchema, DiscordReadyReadStateSchema } from './types'
|
|
8
|
+
|
|
9
|
+
export function parseReadStates(value: unknown): DiscordReadState[] {
|
|
10
|
+
const parsed = DiscordReadyReadStateSchema.safeParse(value)
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
throw new Error('Discord READY contained an invalid read_state payload')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const entries = Array.isArray(parsed.data) ? parsed.data : parsed.data.entries
|
|
16
|
+
|
|
17
|
+
const readStates: DiscordReadState[] = []
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const state = DiscordRawReadStateSchema.safeParse(entry)
|
|
20
|
+
if (!state.success) continue
|
|
21
|
+
if ((state.data.read_state_type ?? 0) !== 0) continue
|
|
22
|
+
readStates.push({
|
|
23
|
+
channelId: state.data.id,
|
|
24
|
+
lastMessageId: state.data.last_message_id ?? null,
|
|
25
|
+
mentionCount: state.data.mention_count ?? 0,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
return readStates
|
|
29
|
+
}
|
|
8
30
|
|
|
9
31
|
const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
|
|
10
32
|
const GATEWAY_QUERY = '?v=10&encoding=json'
|
|
@@ -57,6 +79,7 @@ export class DiscordListener {
|
|
|
57
79
|
private resumeGatewayUrl: string | null = null
|
|
58
80
|
private token: string | null = null
|
|
59
81
|
private cachedUser: { id: string; username: string } | null = null
|
|
82
|
+
private readState: DiscordReadState[] | null = null
|
|
60
83
|
private generation = 0
|
|
61
84
|
private startPromise: Promise<void> | null = null
|
|
62
85
|
private pendingStartReject: ((error: Error) => void) | null = null
|
|
@@ -151,6 +174,7 @@ export class DiscordListener {
|
|
|
151
174
|
this.resumeGatewayUrl = null
|
|
152
175
|
this.token = null
|
|
153
176
|
this.cachedUser = null
|
|
177
|
+
this.readState = null
|
|
154
178
|
rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
|
|
155
179
|
}
|
|
156
180
|
|
|
@@ -174,6 +198,10 @@ export class DiscordListener {
|
|
|
174
198
|
return this
|
|
175
199
|
}
|
|
176
200
|
|
|
201
|
+
getReadState(): DiscordReadState[] | null {
|
|
202
|
+
return this.readState
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
private isCurrent(generation: number, ws?: WebSocket): boolean {
|
|
178
206
|
if (generation !== this.generation || !this.running) return false
|
|
179
207
|
if (ws !== undefined && this.ws !== ws) return false
|
|
@@ -298,6 +326,7 @@ export class DiscordListener {
|
|
|
298
326
|
this.sessionId = d.session_id
|
|
299
327
|
this.resumeGatewayUrl = d.resume_gateway_url
|
|
300
328
|
this.cachedUser = d.user
|
|
329
|
+
this.readState = d.read_state === undefined ? [] : parseReadStates(d.read_state)
|
|
301
330
|
this.reconnectAttempts = 0
|
|
302
331
|
this.emitter.emit('connected', { user: d.user, sessionId: d.session_id })
|
|
303
332
|
return
|
|
@@ -150,6 +150,17 @@ it('DiscordReactionSchema validates reaction with custom emoji', () => {
|
|
|
150
150
|
expect(result.success).toBe(true)
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
+
it('DiscordReactionSchema validates nullable emoji fields', () => {
|
|
154
|
+
const result = DiscordReactionSchema.safeParse({
|
|
155
|
+
emoji: {
|
|
156
|
+
id: null,
|
|
157
|
+
name: null,
|
|
158
|
+
},
|
|
159
|
+
count: 1,
|
|
160
|
+
})
|
|
161
|
+
expect(result.success).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
153
164
|
it('DiscordReactionSchema rejects missing required fields', () => {
|
|
154
165
|
const result = DiscordReactionSchema.safeParse({
|
|
155
166
|
emoji: {
|
|
@@ -38,6 +38,7 @@ export interface DiscordMessage {
|
|
|
38
38
|
timestamp: string
|
|
39
39
|
edited_timestamp?: string
|
|
40
40
|
thread_id?: string
|
|
41
|
+
reactions?: DiscordReaction[]
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export interface DiscordUser {
|
|
@@ -58,8 +59,8 @@ export interface DiscordDMChannel {
|
|
|
58
59
|
|
|
59
60
|
export interface DiscordReaction {
|
|
60
61
|
emoji: {
|
|
61
|
-
id?: string
|
|
62
|
-
name: string
|
|
62
|
+
id?: string | null
|
|
63
|
+
name: string | null
|
|
63
64
|
}
|
|
64
65
|
count: number
|
|
65
66
|
}
|
|
@@ -91,6 +92,26 @@ export interface DiscordUserNote {
|
|
|
91
92
|
note: string
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export interface DiscordReadState {
|
|
96
|
+
channelId: string
|
|
97
|
+
lastMessageId: string | null
|
|
98
|
+
mentionCount: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DiscordUnreadMention extends DiscordMention {
|
|
102
|
+
mention_count: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DiscordUnreadMentionsResult {
|
|
106
|
+
mentions: DiscordUnreadMention[]
|
|
107
|
+
count: number
|
|
108
|
+
// Account-wide unread-mention badge total; not narrowed by guildId because
|
|
109
|
+
// READY read states carry no guild association.
|
|
110
|
+
badgeCount: number
|
|
111
|
+
complete: boolean
|
|
112
|
+
windowDays: number
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
export interface DiscordSearchResult {
|
|
95
116
|
id: string
|
|
96
117
|
channel_id: string
|
|
@@ -109,6 +130,15 @@ export interface DiscordSearchResponse {
|
|
|
109
130
|
messages: DiscordSearchResult[][]
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
export interface DiscordSearchIndexNotReadyResponse {
|
|
134
|
+
message: string
|
|
135
|
+
code: 110000
|
|
136
|
+
documents_indexed: number
|
|
137
|
+
retry_after: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type DiscordSearchApiResponse = DiscordSearchResponse | DiscordSearchIndexNotReadyResponse
|
|
141
|
+
|
|
112
142
|
export interface DiscordSearchOptions {
|
|
113
143
|
channelId?: string
|
|
114
144
|
authorId?: string
|
|
@@ -181,6 +211,14 @@ export const DiscordChannelSchema = z.object({
|
|
|
181
211
|
position: z.number().optional(),
|
|
182
212
|
})
|
|
183
213
|
|
|
214
|
+
export const DiscordReactionSchema = z.object({
|
|
215
|
+
emoji: z.object({
|
|
216
|
+
id: z.string().nullish(),
|
|
217
|
+
name: z.string().nullable(),
|
|
218
|
+
}),
|
|
219
|
+
count: z.number(),
|
|
220
|
+
})
|
|
221
|
+
|
|
184
222
|
export const DiscordMessageSchema = z.object({
|
|
185
223
|
id: z.string(),
|
|
186
224
|
channel_id: z.string(),
|
|
@@ -192,6 +230,7 @@ export const DiscordMessageSchema = z.object({
|
|
|
192
230
|
timestamp: z.string(),
|
|
193
231
|
edited_timestamp: z.string().optional(),
|
|
194
232
|
thread_id: z.string().optional(),
|
|
233
|
+
reactions: z.array(DiscordReactionSchema).optional(),
|
|
195
234
|
})
|
|
196
235
|
|
|
197
236
|
export const DiscordUserSchema = z.object({
|
|
@@ -210,14 +249,6 @@ export const DiscordDMChannelSchema = z.object({
|
|
|
210
249
|
name: z.string().optional(),
|
|
211
250
|
})
|
|
212
251
|
|
|
213
|
-
export const DiscordReactionSchema = z.object({
|
|
214
|
-
emoji: z.object({
|
|
215
|
-
id: z.string().optional(),
|
|
216
|
-
name: z.string(),
|
|
217
|
-
}),
|
|
218
|
-
count: z.number(),
|
|
219
|
-
})
|
|
220
|
-
|
|
221
252
|
export const DiscordFileSchema = z.object({
|
|
222
253
|
id: z.string(),
|
|
223
254
|
filename: z.string(),
|
|
@@ -267,6 +298,13 @@ export const DiscordSearchResponseSchema = z.object({
|
|
|
267
298
|
messages: z.array(z.array(DiscordSearchResultSchema)),
|
|
268
299
|
})
|
|
269
300
|
|
|
301
|
+
export const DiscordSearchIndexNotReadyResponseSchema = z.object({
|
|
302
|
+
message: z.string(),
|
|
303
|
+
code: z.literal(110000),
|
|
304
|
+
documents_indexed: z.number(),
|
|
305
|
+
retry_after: z.number().nonnegative(),
|
|
306
|
+
})
|
|
307
|
+
|
|
270
308
|
export const DiscordCredentialsSchema = z.object({
|
|
271
309
|
token: z.string(),
|
|
272
310
|
})
|
|
@@ -386,6 +424,24 @@ export interface DiscordGatewayGenericEvent {
|
|
|
386
424
|
[key: string]: unknown
|
|
387
425
|
}
|
|
388
426
|
|
|
427
|
+
// read_state_type 0 = channel read state (carries last_message_id); non-zero types
|
|
428
|
+
// carry last_acked_id instead and must be filtered out for mention correlation.
|
|
429
|
+
export const DiscordRawReadStateSchema = z.object({
|
|
430
|
+
id: z.string(),
|
|
431
|
+
read_state_type: z.number().optional(),
|
|
432
|
+
last_message_id: z.string().nullish(),
|
|
433
|
+
mention_count: z.number().optional(),
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
export type DiscordRawReadState = z.infer<typeof DiscordRawReadStateSchema>
|
|
437
|
+
|
|
438
|
+
// READY sends read_state as a raw array, or as { entries, version } when the
|
|
439
|
+
// VERSIONED_READ_STATES gateway capability is negotiated.
|
|
440
|
+
export const DiscordReadyReadStateSchema = z.union([
|
|
441
|
+
z.array(z.unknown()),
|
|
442
|
+
z.object({ entries: z.array(z.unknown()), version: z.number().optional() }),
|
|
443
|
+
])
|
|
444
|
+
|
|
389
445
|
export type DiscordGatewayEvent =
|
|
390
446
|
| DiscordGatewayMessageCreateEvent
|
|
391
447
|
| DiscordGatewayMessageUpdateEvent
|