agent-messenger 2.29.1 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/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 +1 -0
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +39 -17
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/message.d.ts +3 -0
- package/dist/src/platforms/discord/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/message.js +30 -0
- package/dist/src/platforms/discord/commands/message.js.map +1 -1
- package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.d.ts.map +1 -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 +13 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js +6 -0
- 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/commands/message.d.ts +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +4 -4
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/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/commands/message.js +4 -4
- package/dist/src/platforms/slack/commands/message.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/commands/message.js +4 -4
- package/dist/src/platforms/slackbot/commands/message.js.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 +1 -0
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +19 -0
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/chat.d.ts +3 -0
- package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/chat.js +34 -1
- package/dist/src/platforms/teams/commands/chat.js.map +1 -1
- package/dist/src/platforms/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 +1 -0
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js +21 -0
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/message.js +25 -1
- package/dist/src/platforms/telegram/commands/message.js.map +1 -1
- package/dist/src/platforms/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/commands/message.d.ts +1 -1
- package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/telegrambot/commands/message.js +3 -3
- package/dist/src/platforms/telegrambot/commands/message.js.map +1 -1
- package/dist/src/platforms/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 +3 -1
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +19 -3
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/message.js +18 -0
- package/dist/src/platforms/whatsapp/commands/message.js.map +1 -1
- package/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/discordbot.mdx +2 -2
- package/docs/content/docs/cli/kakaotalk.mdx +16 -1
- package/docs/content/docs/cli/slack.mdx +2 -2
- package/docs/content/docs/cli/slackbot.mdx +2 -2
- package/docs/content/docs/cli/telegrambot.mdx +1 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +39 -0
- package/e2e/README.md +3 -3
- package/e2e/discordbot.e2e.test.ts +2 -2
- package/e2e/slack.e2e.test.ts +2 -2
- package/e2e/slackbot.e2e.test.ts +2 -2
- package/e2e/telegrambot.e2e.test.ts +2 -2
- package/package.json +2 -2
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +5 -1
- package/skills/agent-discordbot/SKILL.md +3 -3
- package/skills/agent-discordbot/references/common-patterns.md +2 -2
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +19 -2
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +3 -3
- package/skills/agent-slack/references/common-patterns.md +2 -2
- package/skills/agent-slackbot/SKILL.md +3 -3
- package/skills/agent-slackbot/references/common-patterns.md +4 -4
- package/skills/agent-teams/SKILL.md +6 -1
- package/skills/agent-telegram/SKILL.md +4 -1
- package/skills/agent-telegrambot/SKILL.md +2 -2
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +25 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +143 -0
- package/src/platforms/discord/client.ts +49 -22
- package/src/platforms/discord/commands/message.test.ts +26 -1
- package/src/platforms/discord/commands/message.ts +41 -0
- package/src/platforms/discord/types.ts +16 -0
- package/src/platforms/discordbot/commands/message.test.ts +6 -6
- package/src/platforms/discordbot/commands/message.ts +4 -4
- 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/src/platforms/slack/commands/message.test.ts +4 -4
- package/src/platforms/slack/commands/message.ts +4 -4
- package/src/platforms/slackbot/commands/message.ts +4 -4
- package/src/platforms/teams/client.test.ts +24 -0
- package/src/platforms/teams/client.ts +28 -0
- package/src/platforms/teams/commands/chat.test.ts +23 -1
- package/src/platforms/teams/commands/chat.ts +44 -0
- package/src/platforms/telegram/client.test.ts +60 -0
- package/src/platforms/telegram/client.ts +24 -0
- package/src/platforms/telegram/commands/message.test.ts +38 -0
- package/src/platforms/telegram/commands/message.ts +39 -0
- package/src/platforms/telegrambot/commands/message.ts +3 -3
- package/src/platforms/whatsapp/client.test.ts +58 -0
- package/src/platforms/whatsapp/client.ts +23 -3
- package/src/platforms/whatsapp/commands/message.test.ts +43 -0
- package/src/platforms/whatsapp/commands/message.ts +25 -0
- package/tsconfig.json +8 -8
|
@@ -15,6 +15,8 @@ const mockSyncMessages = mock(() => Promise.resolve({}))
|
|
|
15
15
|
const mockSendMessage = mock(() => Promise.resolve({}))
|
|
16
16
|
const mockSendReply = mock(() => Promise.resolve({}))
|
|
17
17
|
const mockMarkRead = mock(() => Promise.resolve({}))
|
|
18
|
+
const mockLeaveChat = mock(() => Promise.resolve({}))
|
|
19
|
+
const mockSendTyping = mock(() => Promise.resolve({}))
|
|
18
20
|
const mockClose = mock(() => {})
|
|
19
21
|
const mockOnClose = mock((_handler: () => void) => {})
|
|
20
22
|
const mockOnPush = mock((_handler: (packet: unknown) => void) => {})
|
|
@@ -33,6 +35,8 @@ mock.module('./protocol/session', () => ({
|
|
|
33
35
|
sendMessage = mockSendMessage
|
|
34
36
|
sendReply = mockSendReply
|
|
35
37
|
markRead = mockMarkRead
|
|
38
|
+
leaveChat = mockLeaveChat
|
|
39
|
+
sendTyping = mockSendTyping
|
|
36
40
|
close = mockClose
|
|
37
41
|
onClose = mockOnClose
|
|
38
42
|
onPush = mockOnPush
|
|
@@ -56,6 +60,8 @@ function resetAllMocks() {
|
|
|
56
60
|
mockSendMessage.mockReset()
|
|
57
61
|
mockSendReply.mockReset()
|
|
58
62
|
mockMarkRead.mockReset()
|
|
63
|
+
mockLeaveChat.mockReset()
|
|
64
|
+
mockSendTyping.mockReset()
|
|
59
65
|
mockClose.mockReset()
|
|
60
66
|
mockOnClose.mockReset()
|
|
61
67
|
mockOnPush.mockReset()
|
|
@@ -96,7 +102,9 @@ const DEFAULT_LOGIN_RESULT = {
|
|
|
96
102
|
describe('KakaoTalkClient', () => {
|
|
97
103
|
beforeEach(() => {
|
|
98
104
|
resetAllMocks()
|
|
99
|
-
|
|
105
|
+
// Deep-clone so tests that mutate loginResult.chatDatas (e.g. leaveChat)
|
|
106
|
+
// don't leak into subsequent tests.
|
|
107
|
+
mockLogin.mockResolvedValue(structuredClone(DEFAULT_LOGIN_RESULT))
|
|
100
108
|
mockGetChatLogs.mockResolvedValue({ body: { status: 0, chatLogs: [], eof: true } })
|
|
101
109
|
mockGetChatInfo.mockResolvedValue({ body: { l: makeLong(99999) } })
|
|
102
110
|
})
|
|
@@ -1239,6 +1247,191 @@ describe('KakaoTalkClient', () => {
|
|
|
1239
1247
|
})
|
|
1240
1248
|
})
|
|
1241
1249
|
|
|
1250
|
+
describe('leaveChat', () => {
|
|
1251
|
+
it('calls session.leaveChat with parsed chatId and returns success', async () => {
|
|
1252
|
+
mockLeaveChat.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
1253
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1254
|
+
|
|
1255
|
+
const result = await client.leaveChat('100')
|
|
1256
|
+
|
|
1257
|
+
expect(result).toEqual({ success: true, status_code: 0, chat_id: '100' })
|
|
1258
|
+
expect(mockLeaveChat).toHaveBeenCalledWith(expect.objectContaining({ low: 100, high: 0 }))
|
|
1259
|
+
|
|
1260
|
+
client.close()
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
it('treats synthetic disconnect packet (statusCode: -1) as transport failure (throws)', async () => {
|
|
1264
|
+
mockLeaveChat.mockResolvedValueOnce({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1265
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
await client.leaveChat('100')
|
|
1269
|
+
throw new Error('expected to throw')
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1272
|
+
expect((e as KakaoTalkError).code).toBe('leave_chat_failed')
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
client.close()
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
it('evicts the departed chat from getChats cache', async () => {
|
|
1279
|
+
mockLeaveChat.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
1280
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1281
|
+
|
|
1282
|
+
const before = await client.getChats()
|
|
1283
|
+
expect(before).toHaveLength(2)
|
|
1284
|
+
|
|
1285
|
+
await client.leaveChat('100')
|
|
1286
|
+
|
|
1287
|
+
const after = await client.getChats()
|
|
1288
|
+
expect(after).toHaveLength(1)
|
|
1289
|
+
expect(after[0].chat_id).toBe('200')
|
|
1290
|
+
|
|
1291
|
+
client.close()
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
it('reconnects and retries leaveChat after silent disconnect (full executeWithReconnect path)', async () => {
|
|
1295
|
+
const closeHandlers: Array<() => void> = []
|
|
1296
|
+
mockOnClose.mockImplementation((handler: () => void) => {
|
|
1297
|
+
closeHandlers.push(handler)
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
mockLeaveChat.mockImplementationOnce(() => {
|
|
1301
|
+
const handler = closeHandlers[0]
|
|
1302
|
+
if (handler) handler()
|
|
1303
|
+
return Promise.resolve({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1304
|
+
})
|
|
1305
|
+
mockLeaveChat.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
1306
|
+
|
|
1307
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1308
|
+
const result = await client.leaveChat('100')
|
|
1309
|
+
|
|
1310
|
+
expect(result.success).toBe(true)
|
|
1311
|
+
expect(mockLeaveChat).toHaveBeenCalledTimes(2)
|
|
1312
|
+
expect(mockLogin).toHaveBeenCalledTimes(2)
|
|
1313
|
+
|
|
1314
|
+
client.close()
|
|
1315
|
+
})
|
|
1316
|
+
|
|
1317
|
+
it('throws KakaoTalkError(invalid_chat_id) for non-numeric chatId', async () => {
|
|
1318
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1319
|
+
|
|
1320
|
+
try {
|
|
1321
|
+
await client.leaveChat('not-a-number')
|
|
1322
|
+
throw new Error('expected to throw')
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1325
|
+
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
client.close()
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
it('wraps transport errors as KakaoTalkError(leave_chat_failed)', async () => {
|
|
1332
|
+
mockLeaveChat.mockRejectedValue(new Error('Socket closed'))
|
|
1333
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1334
|
+
|
|
1335
|
+
try {
|
|
1336
|
+
await client.leaveChat('100')
|
|
1337
|
+
throw new Error('expected to throw')
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1340
|
+
expect((e as KakaoTalkError).code).toBe('leave_chat_failed')
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
client.close()
|
|
1344
|
+
})
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
describe('sendTyping', () => {
|
|
1348
|
+
it('forwards parsed chatId to session.sendTyping and reports success on statusCode 0', async () => {
|
|
1349
|
+
mockSendTyping.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
1350
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1351
|
+
|
|
1352
|
+
const result = await client.sendTyping('12345')
|
|
1353
|
+
|
|
1354
|
+
expect(mockSendTyping).toHaveBeenCalledTimes(1)
|
|
1355
|
+
const [chatIdArg, linkIdArg] = mockSendTyping.mock.calls[0] as [{ toString(): string }, unknown]
|
|
1356
|
+
expect(chatIdArg.toString()).toBe('12345')
|
|
1357
|
+
expect(linkIdArg).toBeUndefined()
|
|
1358
|
+
expect(result).toEqual({ success: true, status_code: 0, chat_id: '12345' })
|
|
1359
|
+
|
|
1360
|
+
client.close()
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
it('passes linkId through when provided (open-chat typing)', async () => {
|
|
1364
|
+
mockSendTyping.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
1365
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1366
|
+
|
|
1367
|
+
await client.sendTyping('12345', { linkId: '77777' })
|
|
1368
|
+
|
|
1369
|
+
const [, linkIdArg] = mockSendTyping.mock.calls[0] as [unknown, { toString(): string }]
|
|
1370
|
+
expect(linkIdArg).toBeDefined()
|
|
1371
|
+
expect(linkIdArg.toString()).toBe('77777')
|
|
1372
|
+
|
|
1373
|
+
client.close()
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
it('throws KakaoTalkError(send_typing_failed) on transport-level failure (non-zero statusCode)', async () => {
|
|
1377
|
+
mockSendTyping.mockResolvedValueOnce({ statusCode: -100, body: {} })
|
|
1378
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1379
|
+
|
|
1380
|
+
try {
|
|
1381
|
+
await client.sendTyping('12345')
|
|
1382
|
+
throw new Error('expected to throw')
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1385
|
+
expect((e as KakaoTalkError).code).toBe('send_typing_failed')
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
client.close()
|
|
1389
|
+
})
|
|
1390
|
+
|
|
1391
|
+
it('throws KakaoTalkError(send_typing_failed) when the session throws (transport error)', async () => {
|
|
1392
|
+
mockSendTyping.mockRejectedValue(new Error('ACTION failed: statusCode=-1'))
|
|
1393
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1394
|
+
|
|
1395
|
+
try {
|
|
1396
|
+
await client.sendTyping('12345')
|
|
1397
|
+
throw new Error('expected to throw')
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1400
|
+
expect((e as KakaoTalkError).code).toBe('send_typing_failed')
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
client.close()
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('throws KakaoTalkError(invalid_chat_id) for non-numeric chatId', async () => {
|
|
1407
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1408
|
+
|
|
1409
|
+
try {
|
|
1410
|
+
await client.sendTyping('not-a-number')
|
|
1411
|
+
throw new Error('expected to throw')
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1414
|
+
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
client.close()
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
it('throws KakaoTalkError(invalid_link_id) for non-numeric linkId', async () => {
|
|
1421
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1422
|
+
|
|
1423
|
+
try {
|
|
1424
|
+
await client.sendTyping('12345', { linkId: 'not-a-number' })
|
|
1425
|
+
throw new Error('expected to throw')
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1428
|
+
expect((e as KakaoTalkError).code).toBe('invalid_link_id')
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
client.close()
|
|
1432
|
+
})
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1242
1435
|
describe('getMembers / getMembersByIds', () => {
|
|
1243
1436
|
it('returns formatted members from GETMEM with normalized fields', async () => {
|
|
1244
1437
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
KAKAO_MESSAGE_TYPE,
|
|
19
19
|
type KakaoChat,
|
|
20
20
|
type KakaoDeviceType,
|
|
21
|
+
type KakaoLeaveChatResult,
|
|
21
22
|
type KakaoMarkReadResult,
|
|
22
23
|
type KakaoMember,
|
|
23
24
|
type KakaoMessage,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
type KakaoReplyExtra,
|
|
27
28
|
type KakaoReplyTarget,
|
|
28
29
|
type KakaoSendResult,
|
|
30
|
+
type KakaoTypingResult,
|
|
29
31
|
} from './types'
|
|
30
32
|
|
|
31
33
|
export type KakaoSessionEvent =
|
|
@@ -982,6 +984,30 @@ export class KakaoTalkClient {
|
|
|
982
984
|
})
|
|
983
985
|
}
|
|
984
986
|
|
|
987
|
+
async sendTyping(chatId: string, opts?: { linkId?: string }): Promise<KakaoTypingResult> {
|
|
988
|
+
const parsedChatId = parseChatId(chatId)
|
|
989
|
+
const parsedLinkId = opts?.linkId !== undefined ? parseLinkId(opts.linkId) : undefined
|
|
990
|
+
|
|
991
|
+
return this.executeWithReconnect(async ({ session }) => {
|
|
992
|
+
try {
|
|
993
|
+
const response = await session.sendTyping(parsedChatId, parsedLinkId)
|
|
994
|
+
// Throw on transport-level failure (incl. synthetic { statusCode: -1 }
|
|
995
|
+
// from LocoConnection.handleClose) so executeWithReconnect retries on
|
|
996
|
+
// a fresh session — matches the markRead pattern.
|
|
997
|
+
if (response.statusCode !== 0) {
|
|
998
|
+
throw new Error(`ACTION failed: statusCode=${response.statusCode}`)
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
status_code: response.statusCode,
|
|
1003
|
+
chat_id: chatId,
|
|
1004
|
+
}
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
throw wrapError(error, 'send_typing_failed')
|
|
1007
|
+
}
|
|
1008
|
+
})
|
|
1009
|
+
}
|
|
1010
|
+
|
|
985
1011
|
async sendAttachment(
|
|
986
1012
|
chatId: string,
|
|
987
1013
|
data: Uint8Array | Buffer,
|
|
@@ -1317,6 +1343,38 @@ export class KakaoTalkClient {
|
|
|
1317
1343
|
})
|
|
1318
1344
|
}
|
|
1319
1345
|
|
|
1346
|
+
async leaveChat(chatId: string): Promise<KakaoLeaveChatResult> {
|
|
1347
|
+
const parsedChatId = parseChatId(chatId)
|
|
1348
|
+
|
|
1349
|
+
return this.executeWithReconnect(async ({ session, loginResult }) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const response = await session.leaveChat(parsedChatId)
|
|
1352
|
+
// Throw on transport-level failure (incl. synthetic { statusCode: -1 }
|
|
1353
|
+
// from LocoConnection.handleClose) so executeWithReconnect retries on
|
|
1354
|
+
// a fresh session. The LEAVE command result lives in statusCode.
|
|
1355
|
+
if (response.statusCode !== 0) {
|
|
1356
|
+
throw new Error(`LEAVE failed: statusCode=${response.statusCode}`)
|
|
1357
|
+
}
|
|
1358
|
+
// Evict the departed chat from in-memory caches so subsequent getChats()
|
|
1359
|
+
// calls do not return stale entries for this room. Normalize via
|
|
1360
|
+
// longToString(parsedChatId) so the cache key matches the canonical
|
|
1361
|
+
// format used by the server (e.g. "0100" → "100").
|
|
1362
|
+
const normalizedChatId = longToString(parsedChatId)
|
|
1363
|
+
const raw = (loginResult.chatDatas ?? []) as ChatData[]
|
|
1364
|
+
const idx = raw.findIndex((c) => longToString(c.c) === normalizedChatId)
|
|
1365
|
+
if (idx !== -1) raw.splice(idx, 1)
|
|
1366
|
+
this.nameCache.forget(normalizedChatId)
|
|
1367
|
+
return {
|
|
1368
|
+
success: true,
|
|
1369
|
+
status_code: 0,
|
|
1370
|
+
chat_id: chatId,
|
|
1371
|
+
}
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
throw wrapError(error, 'leave_chat_failed')
|
|
1374
|
+
}
|
|
1375
|
+
})
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1320
1378
|
async getProfile(): Promise<KakaoProfile> {
|
|
1321
1379
|
this.ensureAuth()
|
|
1322
1380
|
try {
|
|
@@ -13,8 +13,11 @@ const mockGetChats = mock(() =>
|
|
|
13
13
|
]),
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
const mockLeaveChat = mock(() => Promise.resolve({ success: true, status_code: 0, chat_id: 'chat-1' }))
|
|
17
|
+
|
|
16
18
|
const mockClient = {
|
|
17
19
|
getChats: mockGetChats,
|
|
20
|
+
leaveChat: mockLeaveChat,
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
mock.module('./shared', () => ({
|
|
@@ -29,6 +32,7 @@ describe('chat commands', () => {
|
|
|
29
32
|
beforeEach(() => {
|
|
30
33
|
mockWithKakaoClient.mockReset()
|
|
31
34
|
mockGetChats.mockReset()
|
|
35
|
+
mockLeaveChat.mockReset()
|
|
32
36
|
|
|
33
37
|
mockWithKakaoClient.mockImplementation(async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
|
|
34
38
|
return fn(mockClient)
|
|
@@ -39,6 +43,7 @@ describe('chat commands', () => {
|
|
|
39
43
|
{ chat_id: 'chat-2', name: 'Direct', type: 'direct', member_count: 2 },
|
|
40
44
|
]),
|
|
41
45
|
)
|
|
46
|
+
mockLeaveChat.mockImplementation(() => Promise.resolve({ success: true, status_code: 0, chat_id: 'chat-1' }))
|
|
42
47
|
|
|
43
48
|
consoleLogSpy = mock((..._args: unknown[]) => {})
|
|
44
49
|
console.log = consoleLogSpy
|
|
@@ -91,4 +96,23 @@ describe('chat commands', () => {
|
|
|
91
96
|
expect(output).toEqual([])
|
|
92
97
|
})
|
|
93
98
|
})
|
|
99
|
+
|
|
100
|
+
describe('leave', () => {
|
|
101
|
+
it('leaves a chat room', async () => {
|
|
102
|
+
await chatCommand.parseAsync(['leave', 'chat-1'], { from: 'user' })
|
|
103
|
+
|
|
104
|
+
expect(mockLeaveChat).toHaveBeenCalledWith('chat-1')
|
|
105
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
|
|
106
|
+
expect(output).toEqual({ success: true, status_code: 0, chat_id: 'chat-1' })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('passes account option to withKakaoClient', async () => {
|
|
110
|
+
await chatCommand.parseAsync(['leave', 'chat-1', '--account', 'my-account'], { from: 'user' })
|
|
111
|
+
|
|
112
|
+
expect(mockWithKakaoClient).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({ account: 'my-account' }),
|
|
114
|
+
expect.any(Function),
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
94
118
|
})
|
|
@@ -22,6 +22,21 @@ async function listAction(options: {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function leaveAction(
|
|
26
|
+
chatId: string,
|
|
27
|
+
options: {
|
|
28
|
+
account?: string
|
|
29
|
+
pretty?: boolean
|
|
30
|
+
},
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
const result = await withKakaoClient(options, (client) => client.leaveChat(chatId))
|
|
34
|
+
console.log(formatOutput(result, options.pretty))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
handleError(error as Error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
export const chatCommand = new Command('chat')
|
|
26
41
|
.description('KakaoTalk chat commands')
|
|
27
42
|
.addCommand(
|
|
@@ -34,3 +49,11 @@ export const chatCommand = new Command('chat')
|
|
|
34
49
|
.option('--pretty', 'Pretty print JSON output')
|
|
35
50
|
.action(listAction),
|
|
36
51
|
)
|
|
52
|
+
.addCommand(
|
|
53
|
+
new Command('leave')
|
|
54
|
+
.description('Leave a chat room')
|
|
55
|
+
.argument('<chat-id>', 'Chat room ID')
|
|
56
|
+
.option('--account <id>', 'Use a specific KakaoTalk account')
|
|
57
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
58
|
+
.action(leaveAction),
|
|
59
|
+
)
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
KakaoCredentialManager,
|
|
8
8
|
KakaoChatSchema,
|
|
9
9
|
KakaoConfigSchema,
|
|
10
|
+
KakaoLeaveChatResultSchema,
|
|
10
11
|
KakaoMessageSchema,
|
|
11
12
|
KakaoSendResultSchema,
|
|
12
13
|
KakaoTalkClient,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
KakaoTalkPushMessageEventSchema,
|
|
17
18
|
KakaoProfileSchema,
|
|
18
19
|
KakaoTalkPushReadEventSchema,
|
|
20
|
+
KakaoTypingResultSchema,
|
|
19
21
|
} from '@/platforms/kakaotalk/index'
|
|
20
22
|
|
|
21
23
|
it('KakaoTalkClient is exported from barrel', () => {
|
|
@@ -77,3 +79,11 @@ it('KakaoProfileSchema is exported from barrel', () => {
|
|
|
77
79
|
it('classifyKakaoChat is exported from barrel', () => {
|
|
78
80
|
expect(typeof classifyKakaoChat).toBe('function')
|
|
79
81
|
})
|
|
82
|
+
|
|
83
|
+
it('KakaoLeaveChatResultSchema is exported from barrel', () => {
|
|
84
|
+
expect(typeof KakaoLeaveChatResultSchema.parse).toBe('function')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('KakaoTypingResultSchema is exported from barrel', () => {
|
|
88
|
+
expect(typeof KakaoTypingResultSchema.parse).toBe('function')
|
|
89
|
+
})
|
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
KakaoEmoticonKind,
|
|
14
14
|
KakaoEmoticonMessageType,
|
|
15
15
|
KakaoFileExtra,
|
|
16
|
+
KakaoLeaveChatResult,
|
|
16
17
|
KakaoLoginResult,
|
|
17
18
|
KakaoMarkReadResult,
|
|
18
19
|
KakaoMember,
|
|
@@ -30,6 +31,7 @@ export type {
|
|
|
30
31
|
KakaoTalkPushMemberEvent,
|
|
31
32
|
KakaoTalkPushMessageEvent,
|
|
32
33
|
KakaoTalkPushReadEvent,
|
|
34
|
+
KakaoTypingResult,
|
|
33
35
|
} from './types'
|
|
34
36
|
export {
|
|
35
37
|
KAKAO_EMOTICON_KIND_BY_TYPE,
|
|
@@ -38,6 +40,7 @@ export {
|
|
|
38
40
|
KakaoAccountCredentialsSchema,
|
|
39
41
|
KakaoChatSchema,
|
|
40
42
|
KakaoConfigSchema,
|
|
43
|
+
KakaoLeaveChatResultSchema,
|
|
41
44
|
KakaoMarkReadResultSchema,
|
|
42
45
|
KakaoMemberSchema,
|
|
43
46
|
KakaoMessageSchema,
|
|
@@ -47,6 +50,7 @@ export {
|
|
|
47
50
|
KakaoTalkPushMemberEventSchema,
|
|
48
51
|
KakaoTalkPushMessageEventSchema,
|
|
49
52
|
KakaoTalkPushReadEventSchema,
|
|
53
|
+
KakaoTypingResultSchema,
|
|
50
54
|
} from './types'
|
|
51
55
|
export { attemptLogin, generateDeviceUuid, loginFlow, registerDevice, requestPasscode } from './auth/kakao-login'
|
|
52
56
|
export type { LoginCredentials } from './auth/kakao-login'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { Long } from 'bson'
|
|
4
|
+
|
|
5
|
+
import { buildTypingActionBody, sendTypingPacket, TYPING_ACTION_METHOD } from './session'
|
|
6
|
+
import type { LocoPacket } from './types'
|
|
7
|
+
|
|
8
|
+
type SentPacket = { method: string; body: Record<string, unknown> }
|
|
9
|
+
|
|
10
|
+
// Fake connection object that captures every sendPacket call. We test
|
|
11
|
+
// sendTypingPacket (the extracted wire-boundary function) directly rather than
|
|
12
|
+
// the LocoSession class method — LocoSession.sendTyping is a two-line delegation
|
|
13
|
+
// to this helper, and testing the class would require mock.module('./session',
|
|
14
|
+
// …) which collides with client.test.ts's LocoSession mock.
|
|
15
|
+
function fakeConnection(returnPacket: LocoPacket = { statusCode: 0, body: {} }): {
|
|
16
|
+
connection: { sendPacket: (method: string, body: Record<string, unknown>) => Promise<LocoPacket> }
|
|
17
|
+
sent: SentPacket[]
|
|
18
|
+
sendPacketMock: ReturnType<typeof mock>
|
|
19
|
+
} {
|
|
20
|
+
const sent: SentPacket[] = []
|
|
21
|
+
const sendPacketMock = mock(async (method: string, body: Record<string, unknown>) => {
|
|
22
|
+
sent.push({ method, body })
|
|
23
|
+
return returnPacket
|
|
24
|
+
})
|
|
25
|
+
return { connection: { sendPacket: sendPacketMock }, sent, sendPacketMock }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('typing indicator wire contract', () => {
|
|
29
|
+
it('TYPING_ACTION_METHOD is the exact string "ACTION" that KakaoTalk expects on the wire', () => {
|
|
30
|
+
expect(TYPING_ACTION_METHOD).toBe('ACTION')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('emits type=1 and omits linkId entirely for normal chats', () => {
|
|
34
|
+
const body = buildTypingActionBody(Long.fromString('459750513901477'))
|
|
35
|
+
|
|
36
|
+
expect((body.chatId as { toString(): string }).toString()).toBe('459750513901477')
|
|
37
|
+
expect(body.type).toBe(1)
|
|
38
|
+
expect('linkId' in body).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('includes linkId when provided (open-chat pulse)', () => {
|
|
42
|
+
const body = buildTypingActionBody(Long.fromString('123'), Long.fromString('77777'))
|
|
43
|
+
|
|
44
|
+
expect(body.type).toBe(1)
|
|
45
|
+
expect((body.linkId as { toString(): string }).toString()).toBe('77777')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('never sets linkId=null when caller passes undefined (would change server routing)', () => {
|
|
49
|
+
const body = buildTypingActionBody(Long.fromString('123'), undefined)
|
|
50
|
+
|
|
51
|
+
expect(body.linkId).toBeUndefined()
|
|
52
|
+
expect('linkId' in body).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('body only carries chatId + type for normal chats (no stray fields)', () => {
|
|
56
|
+
const body = buildTypingActionBody(Long.fromString('123'))
|
|
57
|
+
|
|
58
|
+
expect(Object.keys(body).sort()).toEqual(['chatId', 'type'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('body only carries chatId + type + linkId for open chats (no stray fields)', () => {
|
|
62
|
+
const body = buildTypingActionBody(Long.fromString('123'), Long.fromString('77777'))
|
|
63
|
+
|
|
64
|
+
expect(Object.keys(body).sort()).toEqual(['chatId', 'linkId', 'type'])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('sendTypingPacket → sendPacket wire boundary', () => {
|
|
69
|
+
it('calls connection.sendPacket("ACTION", body) exactly once for a normal chat, with full-width Long chatId preserved', async () => {
|
|
70
|
+
const { connection, sent, sendPacketMock } = fakeConnection()
|
|
71
|
+
|
|
72
|
+
await sendTypingPacket(connection, Long.fromString('459750513901477'))
|
|
73
|
+
|
|
74
|
+
expect(sendPacketMock).toHaveBeenCalledTimes(1)
|
|
75
|
+
expect(sent).toHaveLength(1)
|
|
76
|
+
const packet = sent[0]!
|
|
77
|
+
expect(packet.method).toBe('ACTION')
|
|
78
|
+
expect((packet.body.chatId as Long).toString()).toBe('459750513901477')
|
|
79
|
+
expect(packet.body.type).toBe(1)
|
|
80
|
+
expect('linkId' in packet.body).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('calls sendPacket with full-width Long linkId preserved for an open-chat pulse', async () => {
|
|
84
|
+
const { connection, sent } = fakeConnection()
|
|
85
|
+
|
|
86
|
+
await sendTypingPacket(connection, Long.fromString('459750513901477'), Long.fromString('280368495100000'))
|
|
87
|
+
|
|
88
|
+
const packet = sent[0]!
|
|
89
|
+
expect(packet.method).toBe('ACTION')
|
|
90
|
+
expect((packet.body.chatId as Long).toString()).toBe('459750513901477')
|
|
91
|
+
expect((packet.body.linkId as Long).toString()).toBe('280368495100000')
|
|
92
|
+
expect(packet.body.type).toBe(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('propagates the LocoPacket returned by sendPacket verbatim', async () => {
|
|
96
|
+
const expected = { statusCode: 0, body: { hello: 'world' } }
|
|
97
|
+
const { connection } = fakeConnection(expected)
|
|
98
|
+
|
|
99
|
+
const result = await sendTypingPacket(connection, Long.fromString('123'))
|
|
100
|
+
|
|
101
|
+
expect(result).toBe(expected)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('emits the same wire packet the reverse-engineered client builds (spec-lock)', async () => {
|
|
105
|
+
const { connection, sent } = fakeConnection()
|
|
106
|
+
|
|
107
|
+
await sendTypingPacket(connection, Long.fromString('123'), Long.fromString('456'))
|
|
108
|
+
|
|
109
|
+
const packet = sent[0]!
|
|
110
|
+
expect(packet.method).toBe(TYPING_ACTION_METHOD)
|
|
111
|
+
expect(packet.body).toEqual(buildTypingActionBody(Long.fromString('123'), Long.fromString('456')))
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -17,6 +17,36 @@ import {
|
|
|
17
17
|
import { LocoConnection } from './connection'
|
|
18
18
|
import type { BookingResponse, CheckinResponse, LoginListResponse, LocoPacket, SyncState } from './types'
|
|
19
19
|
|
|
20
|
+
// LOCO opcode string emitted on the wire for typing indicator pulses.
|
|
21
|
+
// Reverse-engineered from KakaoTalk 25.x — see `LocoSession.sendTyping` docs.
|
|
22
|
+
export const TYPING_ACTION_METHOD = 'ACTION'
|
|
23
|
+
|
|
24
|
+
// Builds the BSON body the official KakaoTalk client emits for the typing
|
|
25
|
+
// indicator subtype of the `ACTION` opcode. Exported so protocol tests can lock
|
|
26
|
+
// the exact field shape.
|
|
27
|
+
export function buildTypingActionBody(chatId: Long, linkId?: Long): Record<string, unknown> {
|
|
28
|
+
const body: Record<string, unknown> = { chatId, type: 1 }
|
|
29
|
+
// Must omit the field entirely (not `null`) for normal chats — the official
|
|
30
|
+
// client only populates linkId for OpenChat rooms, and sending null here
|
|
31
|
+
// would change server routing.
|
|
32
|
+
if (linkId !== undefined) body.linkId = linkId
|
|
33
|
+
return body
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The one-and-only delegation from `LocoSession.sendTyping` to a `sendPacket`
|
|
37
|
+
// implementation. Extracting this makes the wire boundary directly testable
|
|
38
|
+
// with a fake connection object — no `LocoSession` instantiation required,
|
|
39
|
+
// which side-steps the `mock.module('./session', …)` set up by `client.test.ts`.
|
|
40
|
+
// `LocoSession.sendTyping` is a 2-line delegation to this helper, so any test
|
|
41
|
+
// that pins this function's behaviour also pins the class method's contract.
|
|
42
|
+
export async function sendTypingPacket(
|
|
43
|
+
connection: { sendPacket: (method: string, body: Record<string, unknown>) => Promise<LocoPacket> },
|
|
44
|
+
chatId: Long,
|
|
45
|
+
linkId?: Long,
|
|
46
|
+
): Promise<LocoPacket> {
|
|
47
|
+
return connection.sendPacket(TYPING_ACTION_METHOD, buildTypingActionBody(chatId, linkId))
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
export class LocoSession {
|
|
21
51
|
private connection: LocoConnection | null = null
|
|
22
52
|
private pingTimer: ReturnType<typeof setInterval> | null = null
|
|
@@ -289,6 +319,11 @@ export class LocoSession {
|
|
|
289
319
|
* supply `linkId`; normal chats must omit it (the wire `li` key must be
|
|
290
320
|
* absent, not null/0).
|
|
291
321
|
*/
|
|
322
|
+
async leaveChat(chatId: Long): Promise<LocoPacket> {
|
|
323
|
+
if (!this.connection) throw new Error('Not connected')
|
|
324
|
+
return this.connection.sendPacket('LEAVE', { chatId })
|
|
325
|
+
}
|
|
326
|
+
|
|
292
327
|
async markRead(chatId: Long, watermark: Long, linkId?: Long): Promise<LocoPacket> {
|
|
293
328
|
if (!this.connection) throw new Error('Not connected')
|
|
294
329
|
const body: Record<string, unknown> = { chatId, watermark }
|
|
@@ -298,6 +333,27 @@ export class LocoSession {
|
|
|
298
333
|
return this.connection.sendPacket('NOTIREAD', body)
|
|
299
334
|
}
|
|
300
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Fire a single typing pulse to peers of a chat — this is what triggers the
|
|
338
|
+
* animated "…" dots the recipient sees while somebody is composing.
|
|
339
|
+
*
|
|
340
|
+
* Under the hood this sends the LOCO `ACTION` opcode with `type=1`. The
|
|
341
|
+
* KakaoTalk client-side lifetime is ~5s per pulse, so callers wanting a
|
|
342
|
+
* sustained indicator must re-fire faster than that.
|
|
343
|
+
*
|
|
344
|
+
* Reverse-engineered from KakaoTalk 25.x: the app's LOCO layer emits an
|
|
345
|
+
* `ACTION` packet with a BSON body of `{chatId, type, linkId?}` when the
|
|
346
|
+
* user types in a chat. `type=1` is the value the app uses for the typing
|
|
347
|
+
* indicator; other `type` values may exist for other in-composition states
|
|
348
|
+
* (recording audio, attaching media, etc.) but haven't been verified. The
|
|
349
|
+
* `linkId` field is omitted entirely (not sent as null) for normal chats;
|
|
350
|
+
* it is only populated for OpenChat rooms.
|
|
351
|
+
*/
|
|
352
|
+
async sendTyping(chatId: Long, linkId?: Long): Promise<LocoPacket> {
|
|
353
|
+
if (!this.connection) throw new Error('Not connected')
|
|
354
|
+
return sendTypingPacket(this.connection, chatId, linkId)
|
|
355
|
+
}
|
|
356
|
+
|
|
301
357
|
onPush(handler: (packet: LocoPacket) => void): void {
|
|
302
358
|
this.pushHandler = handler
|
|
303
359
|
if (this.connection) {
|
|
@@ -207,6 +207,18 @@ export interface KakaoMarkReadResult {
|
|
|
207
207
|
watermark: string
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
export interface KakaoLeaveChatResult {
|
|
211
|
+
success: boolean
|
|
212
|
+
status_code: number
|
|
213
|
+
chat_id: string
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface KakaoTypingResult {
|
|
217
|
+
success: boolean
|
|
218
|
+
status_code: number
|
|
219
|
+
chat_id: string
|
|
220
|
+
}
|
|
221
|
+
|
|
210
222
|
export const KakaoChatSchema = z.object({
|
|
211
223
|
chat_id: z.string(),
|
|
212
224
|
type: z.number(),
|
|
@@ -263,6 +275,18 @@ export const KakaoMarkReadResultSchema = z.object({
|
|
|
263
275
|
watermark: z.string(),
|
|
264
276
|
})
|
|
265
277
|
|
|
278
|
+
export const KakaoLeaveChatResultSchema = z.object({
|
|
279
|
+
success: z.boolean(),
|
|
280
|
+
status_code: z.number(),
|
|
281
|
+
chat_id: z.string(),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
export const KakaoTypingResultSchema = z.object({
|
|
285
|
+
success: z.boolean(),
|
|
286
|
+
status_code: z.number(),
|
|
287
|
+
chat_id: z.string(),
|
|
288
|
+
})
|
|
289
|
+
|
|
266
290
|
export interface KakaoProfile {
|
|
267
291
|
user_id: string
|
|
268
292
|
nickname: string
|