agent-messenger 2.12.1 → 2.13.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/dist/package.json +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 +35 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +318 -15
- 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 +4 -7
- package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.js +48 -74
- 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/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/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-listener-integration.test.ts +411 -0
- package/src/platforms/kakaotalk/client.test.ts +785 -1
- package/src/platforms/kakaotalk/client.ts +369 -18
- 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 +184 -149
- package/src/platforms/kakaotalk/listener.ts +51 -82
- 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/tui/adapters/kakaotalk-adapter.ts +3 -3
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
4
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
5
|
+
|
|
6
|
+
import { withKakaoClient } from './shared'
|
|
7
|
+
|
|
8
|
+
async function listAction(
|
|
9
|
+
chatId: string,
|
|
10
|
+
options: {
|
|
11
|
+
account?: string
|
|
12
|
+
pretty?: boolean
|
|
13
|
+
},
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const members = await withKakaoClient(options, (client) => client.getMembers(chatId))
|
|
17
|
+
console.log(formatOutput(members, options.pretty))
|
|
18
|
+
} catch (error) {
|
|
19
|
+
handleError(error as Error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const memberCommand = new Command('member')
|
|
24
|
+
.description('KakaoTalk member commands')
|
|
25
|
+
.addCommand(
|
|
26
|
+
new Command('list')
|
|
27
|
+
.description('List members of a chat room')
|
|
28
|
+
.argument('<chat-id>', 'Chat room ID')
|
|
29
|
+
.option('--account <id>', 'Use a specific KakaoTalk account')
|
|
30
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
31
|
+
.action(listAction),
|
|
32
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
classifyKakaoChat,
|
|
4
5
|
CredentialManager,
|
|
5
6
|
KakaoAccountCredentialsSchema,
|
|
6
7
|
KakaoCredentialManager,
|
|
@@ -72,3 +73,7 @@ it('KakaoTalkPushReadEventSchema is exported from barrel', () => {
|
|
|
72
73
|
it('KakaoProfileSchema is exported from barrel', () => {
|
|
73
74
|
expect(typeof KakaoProfileSchema.parse).toBe('function')
|
|
74
75
|
})
|
|
76
|
+
|
|
77
|
+
it('classifyKakaoChat is exported from barrel', () => {
|
|
78
|
+
expect(typeof classifyKakaoChat).toBe('function')
|
|
79
|
+
})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { KakaoTalkClient, KakaoTalkError } from './client'
|
|
2
|
+
export { classifyKakaoChat } from './chat-classifier'
|
|
3
|
+
export type { KakaoChatKind } from './chat-classifier'
|
|
2
4
|
export { KakaoCredentialManager, CredentialManager } from './credential-manager'
|
|
3
5
|
export { KakaoTalkListener } from './listener'
|
|
4
6
|
export type { PendingLoginState } from './credential-manager'
|
|
@@ -8,6 +10,7 @@ export type {
|
|
|
8
10
|
KakaoChat,
|
|
9
11
|
KakaoConfig,
|
|
10
12
|
KakaoDeviceType,
|
|
13
|
+
KakaoMember,
|
|
11
14
|
KakaoMessage,
|
|
12
15
|
KakaoProfile,
|
|
13
16
|
KakaoSendResult,
|
|
@@ -22,6 +25,7 @@ export {
|
|
|
22
25
|
KakaoAccountCredentialsSchema,
|
|
23
26
|
KakaoChatSchema,
|
|
24
27
|
KakaoConfigSchema,
|
|
28
|
+
KakaoMemberSchema,
|
|
25
29
|
KakaoMessageSchema,
|
|
26
30
|
KakaoProfileSchema,
|
|
27
31
|
KakaoSendResultSchema,
|
|
@@ -1,59 +1,62 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import type { KakaoSessionEvent, KakaoSessionEventHandler, KakaoPushHandler, KakaoTalkClient } from './client'
|
|
4
|
+
import { KakaoTalkListener } from './listener'
|
|
5
|
+
import type { LocoPacket } from './protocol/types'
|
|
5
6
|
import type {
|
|
6
7
|
KakaoTalkPushGenericEvent,
|
|
7
8
|
KakaoTalkPushMemberEvent,
|
|
8
9
|
KakaoTalkPushMessageEvent,
|
|
9
10
|
KakaoTalkPushReadEvent,
|
|
10
|
-
} from '
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
class FakeClient {
|
|
14
|
+
acquireCalls = 0
|
|
15
|
+
pushHandlers = new Set<KakaoPushHandler>()
|
|
16
|
+
sessionHandlers = new Set<KakaoSessionEventHandler>()
|
|
17
|
+
acquireImpl: () => Promise<void> = async () => {}
|
|
18
|
+
connected = false
|
|
19
|
+
lookupAuthorName: (chatId: string, authorId: number) => string | null = () => null
|
|
20
|
+
|
|
21
|
+
async acquireSession(): Promise<unknown> {
|
|
22
|
+
this.acquireCalls++
|
|
23
|
+
await this.acquireImpl()
|
|
24
|
+
this.connected = true
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
login = mockLogin
|
|
21
|
-
close = mockSessionClose
|
|
28
|
+
isConnected(): boolean {
|
|
29
|
+
return this.connected
|
|
30
|
+
}
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mockSessionInstance = this
|
|
32
|
+
getCredentials(): { oauthToken: string; userId: string; deviceUuid: string; deviceType: 'tablet' } {
|
|
33
|
+
return { oauthToken: 'token', userId: 'user1', deviceUuid: 'device1', deviceType: 'tablet' }
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
onPush(handler:
|
|
29
|
-
this.
|
|
36
|
+
onPush(handler: KakaoPushHandler): () => void {
|
|
37
|
+
this.pushHandlers.add(handler)
|
|
38
|
+
return () => this.pushHandlers.delete(handler)
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
this.
|
|
41
|
+
onSessionEvent(handler: KakaoSessionEventHandler): () => void {
|
|
42
|
+
this.sessionHandlers.add(handler)
|
|
43
|
+
return () => this.sessionHandlers.delete(handler)
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
emitPush(method: string, body: Record<string, unknown>): void {
|
|
47
|
+
const packet: LocoPacket = { packetId: 0, statusCode: 0, method, bodyType: 0, body }
|
|
48
|
+
for (const handler of this.pushHandlers) handler(packet)
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
this.
|
|
51
|
+
emitSessionEvent(event: KakaoSessionEvent): void {
|
|
52
|
+
for (const handler of this.sessionHandlers) handler(event)
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
getCredentials: mock(() => ({
|
|
50
|
-
oauthToken: 'token',
|
|
51
|
-
userId: 'user1',
|
|
52
|
-
deviceUuid: 'device1',
|
|
53
|
-
deviceType: 'tablet' as const,
|
|
54
|
-
})),
|
|
55
|
-
...overrides,
|
|
56
|
-
} as any
|
|
56
|
+
function createListener(overrides: Partial<FakeClient> = {}): { listener: KakaoTalkListener; client: FakeClient } {
|
|
57
|
+
const client = Object.assign(new FakeClient(), overrides)
|
|
58
|
+
const listener = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
59
|
+
return { listener, client }
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
describe('KakaoTalkListener', () => {
|
|
@@ -61,58 +64,69 @@ describe('KakaoTalkListener', () => {
|
|
|
61
64
|
|
|
62
65
|
afterEach(() => {
|
|
63
66
|
listener?.stop()
|
|
64
|
-
mockLogin.mockReset()
|
|
65
|
-
mockLogin.mockResolvedValue({})
|
|
66
|
-
mockSessionClose.mockReset()
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
describe('start', () => {
|
|
70
|
-
it('
|
|
71
|
-
const client =
|
|
72
|
-
listener =
|
|
70
|
+
it('acquires the shared session from the client', async () => {
|
|
71
|
+
const { listener: l, client } = createListener()
|
|
72
|
+
listener = l
|
|
73
73
|
|
|
74
74
|
await listener.start()
|
|
75
75
|
|
|
76
|
-
expect(
|
|
77
|
-
expect(mockLogin).toHaveBeenCalledWith('token', 'user1', 'device1', undefined, 'tablet')
|
|
76
|
+
expect(client.acquireCalls).toBe(1)
|
|
78
77
|
})
|
|
79
78
|
|
|
80
79
|
it('is idempotent', async () => {
|
|
81
|
-
const client =
|
|
82
|
-
listener =
|
|
80
|
+
const { listener: l, client } = createListener()
|
|
81
|
+
listener = l
|
|
83
82
|
|
|
84
83
|
await listener.start()
|
|
85
84
|
await listener.start()
|
|
86
85
|
|
|
87
|
-
expect(
|
|
86
|
+
expect(client.acquireCalls).toBe(1)
|
|
88
87
|
})
|
|
89
88
|
})
|
|
90
89
|
|
|
91
90
|
describe('connected event', () => {
|
|
92
|
-
it('emits connected
|
|
93
|
-
const client =
|
|
94
|
-
listener =
|
|
91
|
+
it('emits connected after acquiring an already-active session', async () => {
|
|
92
|
+
const { listener: l, client } = createListener()
|
|
93
|
+
listener = l
|
|
94
|
+
client.connected = true
|
|
95
|
+
|
|
96
|
+
const events: Array<{ userId: string }> = []
|
|
97
|
+
listener.on('connected', (info) => events.push(info))
|
|
98
|
+
|
|
99
|
+
await listener.start()
|
|
100
|
+
|
|
101
|
+
expect(events.length).toBe(1)
|
|
102
|
+
expect(events[0].userId).toBe('user1')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('emits connected from session-event when client connects after listener starts', async () => {
|
|
106
|
+
const { listener: l, client } = createListener()
|
|
107
|
+
listener = l
|
|
95
108
|
|
|
96
|
-
const
|
|
97
|
-
listener.on('connected', (info) =>
|
|
109
|
+
const events: Array<{ userId: string }> = []
|
|
110
|
+
listener.on('connected', (info) => events.push(info))
|
|
98
111
|
|
|
99
112
|
await listener.start()
|
|
113
|
+
client.emitSessionEvent({ type: 'connected', userId: 'user1' })
|
|
100
114
|
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
115
|
+
expect(events.length).toBe(1)
|
|
116
|
+
expect(events[0].userId).toBe('user1')
|
|
103
117
|
})
|
|
104
118
|
})
|
|
105
119
|
|
|
106
120
|
describe('message events', () => {
|
|
107
121
|
it('emits message on MSG push with parsed fields', async () => {
|
|
108
|
-
const client =
|
|
109
|
-
listener =
|
|
122
|
+
const { listener: l, client } = createListener()
|
|
123
|
+
listener = l
|
|
110
124
|
|
|
111
125
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
112
126
|
listener.on('message', (event) => messages.push(event))
|
|
113
127
|
|
|
114
128
|
await listener.start()
|
|
115
|
-
|
|
129
|
+
client.emitPush('MSG', {
|
|
116
130
|
chatId: { high: 0, low: 100 },
|
|
117
131
|
chatLog: {
|
|
118
132
|
logId: { high: 0, low: 200 },
|
|
@@ -128,22 +142,50 @@ describe('KakaoTalkListener', () => {
|
|
|
128
142
|
expect(messages[0].chat_id).toBe('100')
|
|
129
143
|
expect(messages[0].log_id).toBe('200')
|
|
130
144
|
expect(messages[0].author_id).toBe(42)
|
|
145
|
+
expect(messages[0].author_name).toBeNull()
|
|
131
146
|
expect(messages[0].message).toBe('hello world')
|
|
132
147
|
expect(messages[0].message_type).toBe(1)
|
|
133
148
|
expect(messages[0].sent_at).toBe(1700000000)
|
|
134
149
|
})
|
|
150
|
+
|
|
151
|
+
it('resolves author_name from client.lookupAuthorName when available', async () => {
|
|
152
|
+
const { listener: l, client } = createListener()
|
|
153
|
+
listener = l
|
|
154
|
+
|
|
155
|
+
client.lookupAuthorName = (chatId: string, authorId: number) => {
|
|
156
|
+
if (chatId === '100' && authorId === 42) return 'Alice'
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const messages: KakaoTalkPushMessageEvent[] = []
|
|
161
|
+
listener.on('message', (event) => messages.push(event))
|
|
162
|
+
|
|
163
|
+
await listener.start()
|
|
164
|
+
client.emitPush('MSG', {
|
|
165
|
+
chatId: { high: 0, low: 100 },
|
|
166
|
+
chatLog: {
|
|
167
|
+
logId: { high: 0, low: 200 },
|
|
168
|
+
authorId: 42,
|
|
169
|
+
message: 'hello',
|
|
170
|
+
type: 1,
|
|
171
|
+
sendAt: 1700000000,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(messages[0].author_name).toBe('Alice')
|
|
176
|
+
})
|
|
135
177
|
})
|
|
136
178
|
|
|
137
179
|
describe('member events', () => {
|
|
138
180
|
it('emits member_joined on NEWMEM push', async () => {
|
|
139
|
-
const client =
|
|
140
|
-
listener =
|
|
181
|
+
const { listener: l, client } = createListener()
|
|
182
|
+
listener = l
|
|
141
183
|
|
|
142
184
|
const joined: KakaoTalkPushMemberEvent[] = []
|
|
143
185
|
listener.on('member_joined', (event) => joined.push(event))
|
|
144
186
|
|
|
145
187
|
await listener.start()
|
|
146
|
-
|
|
188
|
+
client.emitPush('NEWMEM', {
|
|
147
189
|
chatId: { high: 0, low: 100 },
|
|
148
190
|
chatLog: { authorId: 42 },
|
|
149
191
|
})
|
|
@@ -155,14 +197,14 @@ describe('KakaoTalkListener', () => {
|
|
|
155
197
|
})
|
|
156
198
|
|
|
157
199
|
it('emits member_left on DELMEM push', async () => {
|
|
158
|
-
const client =
|
|
159
|
-
listener =
|
|
200
|
+
const { listener: l, client } = createListener()
|
|
201
|
+
listener = l
|
|
160
202
|
|
|
161
203
|
const left: KakaoTalkPushMemberEvent[] = []
|
|
162
204
|
listener.on('member_left', (event) => left.push(event))
|
|
163
205
|
|
|
164
206
|
await listener.start()
|
|
165
|
-
|
|
207
|
+
client.emitPush('DELMEM', {
|
|
166
208
|
chatId: { high: 0, low: 100 },
|
|
167
209
|
chatLog: { authorId: 42 },
|
|
168
210
|
})
|
|
@@ -176,14 +218,14 @@ describe('KakaoTalkListener', () => {
|
|
|
176
218
|
|
|
177
219
|
describe('read events', () => {
|
|
178
220
|
it('emits read on DECUNREAD push with watermark', async () => {
|
|
179
|
-
const client =
|
|
180
|
-
listener =
|
|
221
|
+
const { listener: l, client } = createListener()
|
|
222
|
+
listener = l
|
|
181
223
|
|
|
182
224
|
const reads: KakaoTalkPushReadEvent[] = []
|
|
183
225
|
listener.on('read', (event) => reads.push(event))
|
|
184
226
|
|
|
185
227
|
await listener.start()
|
|
186
|
-
|
|
228
|
+
client.emitPush('DECUNREAD', {
|
|
187
229
|
chatId: { high: 0, low: 100 },
|
|
188
230
|
userId: 42,
|
|
189
231
|
watermark: { high: 0, low: 999 },
|
|
@@ -199,22 +241,22 @@ describe('KakaoTalkListener', () => {
|
|
|
199
241
|
|
|
200
242
|
describe('kakaotalk_event catch-all', () => {
|
|
201
243
|
it('emits kakaotalk_event for every push event', async () => {
|
|
202
|
-
const client =
|
|
203
|
-
listener =
|
|
244
|
+
const { listener: l, client } = createListener()
|
|
245
|
+
listener = l
|
|
204
246
|
|
|
205
247
|
const events: KakaoTalkPushGenericEvent[] = []
|
|
206
248
|
listener.on('kakaotalk_event', (event) => events.push(event))
|
|
207
249
|
|
|
208
250
|
await listener.start()
|
|
209
|
-
|
|
251
|
+
client.emitPush('MSG', {
|
|
210
252
|
chatId: { high: 0, low: 1 },
|
|
211
253
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'hi', type: 1, sendAt: 1 },
|
|
212
254
|
})
|
|
213
|
-
|
|
255
|
+
client.emitPush('NEWMEM', {
|
|
214
256
|
chatId: { high: 0, low: 1 },
|
|
215
257
|
chatLog: { authorId: 1 },
|
|
216
258
|
})
|
|
217
|
-
|
|
259
|
+
client.emitPush('CUSTOM_EVENT', { some: 'data' })
|
|
218
260
|
|
|
219
261
|
expect(events.length).toBe(3)
|
|
220
262
|
expect(events[0].type).toBe('MSG')
|
|
@@ -224,112 +266,72 @@ describe('KakaoTalkListener', () => {
|
|
|
224
266
|
})
|
|
225
267
|
|
|
226
268
|
describe('stop', () => {
|
|
227
|
-
it('
|
|
228
|
-
const client =
|
|
229
|
-
listener =
|
|
269
|
+
it('unsubscribes from client push and session events', async () => {
|
|
270
|
+
const { listener: l, client } = createListener()
|
|
271
|
+
listener = l
|
|
230
272
|
|
|
231
273
|
await listener.start()
|
|
274
|
+
expect(client.pushHandlers.size).toBe(1)
|
|
275
|
+
expect(client.sessionHandlers.size).toBe(1)
|
|
232
276
|
|
|
233
277
|
listener.stop()
|
|
234
|
-
mockSessionInstance.simulateClose()
|
|
235
278
|
|
|
236
|
-
|
|
237
|
-
expect(
|
|
279
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
280
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
238
281
|
})
|
|
239
282
|
})
|
|
240
283
|
|
|
241
|
-
describe('
|
|
242
|
-
it('
|
|
243
|
-
const client =
|
|
244
|
-
listener =
|
|
284
|
+
describe('disconnected event', () => {
|
|
285
|
+
it('emits disconnected when the client session drops', async () => {
|
|
286
|
+
const { listener: l, client } = createListener()
|
|
287
|
+
listener = l
|
|
245
288
|
|
|
246
|
-
const
|
|
247
|
-
listener.on('disconnected', () =>
|
|
289
|
+
const disconnects: number[] = []
|
|
290
|
+
listener.on('disconnected', () => disconnects.push(1))
|
|
248
291
|
|
|
249
292
|
await listener.start()
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
expect(disconnected.length).toBe(1)
|
|
293
|
+
client.emitSessionEvent({ type: 'disconnected' })
|
|
253
294
|
|
|
254
|
-
|
|
255
|
-
expect(mockLogin.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('emits error and reconnects on login failure', async () => {
|
|
259
|
-
let callCount = 0
|
|
260
|
-
mockLogin.mockImplementation(() => {
|
|
261
|
-
callCount++
|
|
262
|
-
if (callCount === 1) return Promise.reject(new Error('network_error'))
|
|
263
|
-
return Promise.resolve({})
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
const client = createMockClient()
|
|
267
|
-
listener = new KakaoTalkListener(client)
|
|
268
|
-
|
|
269
|
-
const errors: Error[] = []
|
|
270
|
-
listener.on('error', (err) => errors.push(err))
|
|
271
|
-
|
|
272
|
-
await listener.start()
|
|
273
|
-
|
|
274
|
-
await new Promise((r) => setTimeout(r, 1500))
|
|
275
|
-
|
|
276
|
-
expect(errors.length).toBe(1)
|
|
277
|
-
expect(errors[0].message).toBe('network_error')
|
|
278
|
-
expect(mockLogin.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
279
|
-
})
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
describe('CHANGESVR', () => {
|
|
283
|
-
it('resets reconnect attempts to 0 on CHANGESVR push', async () => {
|
|
284
|
-
const client = createMockClient()
|
|
285
|
-
listener = new KakaoTalkListener(client)
|
|
286
|
-
|
|
287
|
-
await listener.start()
|
|
288
|
-
;(listener as any).reconnectAttempts = 5
|
|
289
|
-
|
|
290
|
-
mockSessionInstance.simulatePush('CHANGESVR', {})
|
|
291
|
-
|
|
292
|
-
expect((listener as any).reconnectAttempts).toBe(0)
|
|
295
|
+
expect(disconnects.length).toBe(1)
|
|
293
296
|
})
|
|
294
297
|
})
|
|
295
298
|
|
|
296
299
|
describe('KICKOUT', () => {
|
|
297
|
-
it('emits error and stops
|
|
298
|
-
const client =
|
|
299
|
-
listener =
|
|
300
|
+
it('emits error and stops the listener when the client reports a kicked session', async () => {
|
|
301
|
+
const { listener: l, client } = createListener()
|
|
302
|
+
listener = l
|
|
300
303
|
|
|
301
304
|
const errors: Error[] = []
|
|
302
305
|
listener.on('error', (err) => errors.push(err))
|
|
303
306
|
|
|
304
307
|
await listener.start()
|
|
305
|
-
|
|
308
|
+
client.emitSessionEvent({ type: 'kicked', reason: 'Session kicked — another device logged in' })
|
|
306
309
|
|
|
307
310
|
expect(errors.length).toBe(1)
|
|
308
311
|
expect(errors[0].message).toContain('kicked')
|
|
309
|
-
expect((listener as
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
expect(mockLogin).toHaveBeenCalledTimes(1)
|
|
312
|
+
expect((listener as unknown as { running: boolean }).running).toBe(false)
|
|
313
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
314
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
313
315
|
})
|
|
314
316
|
})
|
|
315
317
|
|
|
316
318
|
describe('on/off/once', () => {
|
|
317
319
|
it('off removes listener', async () => {
|
|
318
|
-
const client =
|
|
319
|
-
listener =
|
|
320
|
+
const { listener: l, client } = createListener()
|
|
321
|
+
listener = l
|
|
320
322
|
|
|
321
323
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
322
324
|
const handler = (event: KakaoTalkPushMessageEvent) => messages.push(event)
|
|
323
325
|
listener.on('message', handler)
|
|
324
326
|
|
|
325
327
|
await listener.start()
|
|
326
|
-
|
|
328
|
+
client.emitPush('MSG', {
|
|
327
329
|
chatId: { high: 0, low: 1 },
|
|
328
330
|
chatLog: { logId: { high: 0, low: 1 }, authorId: 1, message: 'first', type: 1, sendAt: 1 },
|
|
329
331
|
})
|
|
330
332
|
|
|
331
333
|
listener.off('message', handler)
|
|
332
|
-
|
|
334
|
+
client.emitPush('MSG', {
|
|
333
335
|
chatId: { high: 0, low: 1 },
|
|
334
336
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'second', type: 1, sendAt: 2 },
|
|
335
337
|
})
|
|
@@ -339,18 +341,18 @@ describe('KakaoTalkListener', () => {
|
|
|
339
341
|
})
|
|
340
342
|
|
|
341
343
|
it('once fires only once', async () => {
|
|
342
|
-
const client =
|
|
343
|
-
listener =
|
|
344
|
+
const { listener: l, client } = createListener()
|
|
345
|
+
listener = l
|
|
344
346
|
|
|
345
347
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
346
348
|
listener.once('message', (event) => messages.push(event))
|
|
347
349
|
|
|
348
350
|
await listener.start()
|
|
349
|
-
|
|
351
|
+
client.emitPush('MSG', {
|
|
350
352
|
chatId: { high: 0, low: 1 },
|
|
351
353
|
chatLog: { logId: { high: 0, low: 1 }, authorId: 1, message: 'first', type: 1, sendAt: 1 },
|
|
352
354
|
})
|
|
353
|
-
|
|
355
|
+
client.emitPush('MSG', {
|
|
354
356
|
chatId: { high: 0, low: 1 },
|
|
355
357
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'second', type: 1, sendAt: 2 },
|
|
356
358
|
})
|
|
@@ -360,17 +362,50 @@ describe('KakaoTalkListener', () => {
|
|
|
360
362
|
})
|
|
361
363
|
})
|
|
362
364
|
|
|
363
|
-
describe('
|
|
364
|
-
it('
|
|
365
|
-
const client =
|
|
366
|
-
|
|
365
|
+
describe('error during start', () => {
|
|
366
|
+
it('emits error and tears down subscriptions when acquireSession fails', async () => {
|
|
367
|
+
const client = new FakeClient()
|
|
368
|
+
client.acquireImpl = async () => {
|
|
369
|
+
throw new Error('login_failed')
|
|
370
|
+
}
|
|
371
|
+
const l = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
372
|
+
listener = l
|
|
373
|
+
|
|
374
|
+
const errors: Error[] = []
|
|
375
|
+
listener.on('error', (err) => errors.push(err))
|
|
367
376
|
|
|
368
377
|
await listener.start()
|
|
369
|
-
;(listener as any).reconnectAttempts = 5
|
|
370
|
-
listener.stop()
|
|
371
378
|
|
|
379
|
+
expect(errors.length).toBe(1)
|
|
380
|
+
expect(errors[0].message).toBe('login_failed')
|
|
381
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
382
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('can be restarted after a failed start()', async () => {
|
|
386
|
+
// given — a client whose first acquire fails but later succeeds
|
|
387
|
+
const client = new FakeClient()
|
|
388
|
+
let attempts = 0
|
|
389
|
+
client.acquireImpl = async () => {
|
|
390
|
+
attempts++
|
|
391
|
+
if (attempts === 1) throw new Error('login_failed')
|
|
392
|
+
}
|
|
393
|
+
const l = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
394
|
+
listener = l
|
|
395
|
+
|
|
396
|
+
const errors: Error[] = []
|
|
397
|
+
listener.on('error', (err) => errors.push(err))
|
|
398
|
+
|
|
399
|
+
// when — first start() fails, then we try again
|
|
372
400
|
await listener.start()
|
|
373
|
-
expect(
|
|
401
|
+
expect(errors.length).toBe(1)
|
|
402
|
+
|
|
403
|
+
await listener.start()
|
|
404
|
+
|
|
405
|
+
// then — second start succeeds (subscriptions re-attached, acquire was retried)
|
|
406
|
+
expect(attempts).toBe(2)
|
|
407
|
+
expect(client.pushHandlers.size).toBe(1)
|
|
408
|
+
expect(client.sessionHandlers.size).toBe(1)
|
|
374
409
|
})
|
|
375
410
|
})
|
|
376
411
|
})
|