agent-messenger 2.12.0 → 2.12.2
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/README.md +11 -1
- package/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/CONTRIBUTING.md +12 -0
- package/README.md +30 -4
- package/dist/package.json +10 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +22 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +93 -7
- package/dist/src/platforms/kakaotalk/client.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 +43 -72
- package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
- package/dist/src/platforms/telegrambot/cli.d.ts +5 -0
- package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/cli.js +29 -0
- package/dist/src/platforms/telegrambot/cli.js.map +1 -0
- package/dist/src/platforms/telegrambot/client.d.ts +85 -0
- package/dist/src/platforms/telegrambot/client.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/client.js +282 -0
- package/dist/src/platforms/telegrambot/client.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/auth.d.ts +31 -0
- package/dist/src/platforms/telegrambot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/auth.js +173 -0
- package/dist/src/platforms/telegrambot/commands/auth.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/chat.d.ts +25 -0
- package/dist/src/platforms/telegrambot/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/chat.js +69 -0
- package/dist/src/platforms/telegrambot/commands/chat.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/index.d.ts +6 -0
- package/dist/src/platforms/telegrambot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/index.js +6 -0
- package/dist/src/platforms/telegrambot/commands/index.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/message.d.ts +39 -0
- package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/message.js +145 -0
- package/dist/src/platforms/telegrambot/commands/message.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/reaction.d.ts +16 -0
- package/dist/src/platforms/telegrambot/commands/reaction.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/reaction.js +49 -0
- package/dist/src/platforms/telegrambot/commands/reaction.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/shared.d.ts +12 -0
- package/dist/src/platforms/telegrambot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/shared.js +21 -0
- package/dist/src/platforms/telegrambot/commands/shared.js.map +1 -0
- package/dist/src/platforms/telegrambot/commands/whoami.d.ts +17 -0
- package/dist/src/platforms/telegrambot/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/commands/whoami.js +30 -0
- package/dist/src/platforms/telegrambot/commands/whoami.js.map +1 -0
- package/dist/src/platforms/telegrambot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/credential-manager.js +113 -0
- package/dist/src/platforms/telegrambot/credential-manager.js.map +1 -0
- package/dist/src/platforms/telegrambot/index.d.ts +7 -0
- package/dist/src/platforms/telegrambot/index.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/index.js +5 -0
- package/dist/src/platforms/telegrambot/index.js.map +1 -0
- package/dist/src/platforms/telegrambot/listener.d.ts +30 -0
- package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/listener.js +186 -0
- package/dist/src/platforms/telegrambot/listener.js.map +1 -0
- package/dist/src/platforms/telegrambot/types.d.ts +256 -0
- package/dist/src/platforms/telegrambot/types.d.ts.map +1 -0
- package/dist/src/platforms/telegrambot/types.js +96 -0
- package/dist/src/platforms/telegrambot/types.js.map +1 -0
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/telegrambot.mdx +149 -0
- package/docs/content/docs/index.mdx +10 -9
- package/docs/content/docs/quick-start.mdx +2 -0
- package/docs/content/docs/sdk/meta.json +1 -0
- package/docs/content/docs/sdk/telegrambot.mdx +216 -0
- package/e2e/config.ts +24 -0
- package/e2e/helpers.ts +1 -0
- package/e2e/telegrambot.e2e.test.ts +185 -0
- package/examples/telegrambot-listen.ts +54 -0
- package/package.json +10 -2
- package/scripts/postbuild.ts +1 -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 +12 -5
- 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 +357 -0
- 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/cli.ts +4 -0
- package/src/platforms/kakaotalk/client-listener-integration.test.ts +411 -0
- package/src/platforms/kakaotalk/client.test.ts +3 -0
- package/src/platforms/kakaotalk/client.ts +108 -9
- package/src/platforms/kakaotalk/listener.test.ts +155 -149
- package/src/platforms/kakaotalk/listener.ts +46 -80
- package/src/platforms/telegrambot/cli.ts +34 -0
- package/src/platforms/telegrambot/client.test.ts +454 -0
- package/src/platforms/telegrambot/client.ts +404 -0
- package/src/platforms/telegrambot/commands/auth.test.ts +244 -0
- package/src/platforms/telegrambot/commands/auth.ts +220 -0
- package/src/platforms/telegrambot/commands/chat.ts +96 -0
- package/src/platforms/telegrambot/commands/index.ts +5 -0
- package/src/platforms/telegrambot/commands/message.ts +235 -0
- package/src/platforms/telegrambot/commands/reaction.ts +70 -0
- package/src/platforms/telegrambot/commands/shared.ts +32 -0
- package/src/platforms/telegrambot/commands/whoami.ts +45 -0
- package/src/platforms/telegrambot/credential-manager.test.ts +196 -0
- package/src/platforms/telegrambot/credential-manager.ts +141 -0
- package/src/platforms/telegrambot/index.ts +44 -0
- package/src/platforms/telegrambot/listener.test.ts +398 -0
- package/src/platforms/telegrambot/listener.ts +198 -0
- package/src/platforms/telegrambot/types.test.ts +128 -0
- package/src/platforms/telegrambot/types.ts +282 -0
|
@@ -1,59 +1,61 @@
|
|
|
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
|
+
|
|
20
|
+
async acquireSession(): Promise<unknown> {
|
|
21
|
+
this.acquireCalls++
|
|
22
|
+
await this.acquireImpl()
|
|
23
|
+
this.connected = true
|
|
24
|
+
return {}
|
|
25
|
+
}
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
login = mockLogin
|
|
21
|
-
close = mockSessionClose
|
|
27
|
+
isConnected(): boolean {
|
|
28
|
+
return this.connected
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mockSessionInstance = this
|
|
31
|
+
getCredentials(): { oauthToken: string; userId: string; deviceUuid: string; deviceType: 'tablet' } {
|
|
32
|
+
return { oauthToken: 'token', userId: 'user1', deviceUuid: 'device1', deviceType: 'tablet' }
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
onPush(handler:
|
|
29
|
-
this.
|
|
35
|
+
onPush(handler: KakaoPushHandler): () => void {
|
|
36
|
+
this.pushHandlers.add(handler)
|
|
37
|
+
return () => this.pushHandlers.delete(handler)
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
this.
|
|
40
|
+
onSessionEvent(handler: KakaoSessionEventHandler): () => void {
|
|
41
|
+
this.sessionHandlers.add(handler)
|
|
42
|
+
return () => this.sessionHandlers.delete(handler)
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
emitPush(method: string, body: Record<string, unknown>): void {
|
|
46
|
+
const packet: LocoPacket = { packetId: 0, statusCode: 0, method, bodyType: 0, body }
|
|
47
|
+
for (const handler of this.pushHandlers) handler(packet)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
this.
|
|
50
|
+
emitSessionEvent(event: KakaoSessionEvent): void {
|
|
51
|
+
for (const handler of this.sessionHandlers) handler(event)
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
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
|
|
55
|
+
function createListener(overrides: Partial<FakeClient> = {}): { listener: KakaoTalkListener; client: FakeClient } {
|
|
56
|
+
const client = Object.assign(new FakeClient(), overrides)
|
|
57
|
+
const listener = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
58
|
+
return { listener, client }
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
describe('KakaoTalkListener', () => {
|
|
@@ -61,58 +63,69 @@ describe('KakaoTalkListener', () => {
|
|
|
61
63
|
|
|
62
64
|
afterEach(() => {
|
|
63
65
|
listener?.stop()
|
|
64
|
-
mockLogin.mockReset()
|
|
65
|
-
mockLogin.mockResolvedValue({})
|
|
66
|
-
mockSessionClose.mockReset()
|
|
67
66
|
})
|
|
68
67
|
|
|
69
68
|
describe('start', () => {
|
|
70
|
-
it('
|
|
71
|
-
const client =
|
|
72
|
-
listener =
|
|
69
|
+
it('acquires the shared session from the client', async () => {
|
|
70
|
+
const { listener: l, client } = createListener()
|
|
71
|
+
listener = l
|
|
73
72
|
|
|
74
73
|
await listener.start()
|
|
75
74
|
|
|
76
|
-
expect(
|
|
77
|
-
expect(mockLogin).toHaveBeenCalledWith('token', 'user1', 'device1', undefined, 'tablet')
|
|
75
|
+
expect(client.acquireCalls).toBe(1)
|
|
78
76
|
})
|
|
79
77
|
|
|
80
78
|
it('is idempotent', async () => {
|
|
81
|
-
const client =
|
|
82
|
-
listener =
|
|
79
|
+
const { listener: l, client } = createListener()
|
|
80
|
+
listener = l
|
|
83
81
|
|
|
84
82
|
await listener.start()
|
|
85
83
|
await listener.start()
|
|
86
84
|
|
|
87
|
-
expect(
|
|
85
|
+
expect(client.acquireCalls).toBe(1)
|
|
88
86
|
})
|
|
89
87
|
})
|
|
90
88
|
|
|
91
89
|
describe('connected event', () => {
|
|
92
|
-
it('emits connected
|
|
93
|
-
const client =
|
|
94
|
-
listener =
|
|
90
|
+
it('emits connected after acquiring an already-active session', async () => {
|
|
91
|
+
const { listener: l, client } = createListener()
|
|
92
|
+
listener = l
|
|
93
|
+
client.connected = true
|
|
94
|
+
|
|
95
|
+
const events: Array<{ userId: string }> = []
|
|
96
|
+
listener.on('connected', (info) => events.push(info))
|
|
97
|
+
|
|
98
|
+
await listener.start()
|
|
99
|
+
|
|
100
|
+
expect(events.length).toBe(1)
|
|
101
|
+
expect(events[0].userId).toBe('user1')
|
|
102
|
+
})
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
listener
|
|
104
|
+
it('emits connected from session-event when client connects after listener starts', async () => {
|
|
105
|
+
const { listener: l, client } = createListener()
|
|
106
|
+
listener = l
|
|
107
|
+
|
|
108
|
+
const events: Array<{ userId: string }> = []
|
|
109
|
+
listener.on('connected', (info) => events.push(info))
|
|
98
110
|
|
|
99
111
|
await listener.start()
|
|
112
|
+
client.emitSessionEvent({ type: 'connected', userId: 'user1' })
|
|
100
113
|
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
114
|
+
expect(events.length).toBe(1)
|
|
115
|
+
expect(events[0].userId).toBe('user1')
|
|
103
116
|
})
|
|
104
117
|
})
|
|
105
118
|
|
|
106
119
|
describe('message events', () => {
|
|
107
120
|
it('emits message on MSG push with parsed fields', async () => {
|
|
108
|
-
const client =
|
|
109
|
-
listener =
|
|
121
|
+
const { listener: l, client } = createListener()
|
|
122
|
+
listener = l
|
|
110
123
|
|
|
111
124
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
112
125
|
listener.on('message', (event) => messages.push(event))
|
|
113
126
|
|
|
114
127
|
await listener.start()
|
|
115
|
-
|
|
128
|
+
client.emitPush('MSG', {
|
|
116
129
|
chatId: { high: 0, low: 100 },
|
|
117
130
|
chatLog: {
|
|
118
131
|
logId: { high: 0, low: 200 },
|
|
@@ -136,14 +149,14 @@ describe('KakaoTalkListener', () => {
|
|
|
136
149
|
|
|
137
150
|
describe('member events', () => {
|
|
138
151
|
it('emits member_joined on NEWMEM push', async () => {
|
|
139
|
-
const client =
|
|
140
|
-
listener =
|
|
152
|
+
const { listener: l, client } = createListener()
|
|
153
|
+
listener = l
|
|
141
154
|
|
|
142
155
|
const joined: KakaoTalkPushMemberEvent[] = []
|
|
143
156
|
listener.on('member_joined', (event) => joined.push(event))
|
|
144
157
|
|
|
145
158
|
await listener.start()
|
|
146
|
-
|
|
159
|
+
client.emitPush('NEWMEM', {
|
|
147
160
|
chatId: { high: 0, low: 100 },
|
|
148
161
|
chatLog: { authorId: 42 },
|
|
149
162
|
})
|
|
@@ -155,14 +168,14 @@ describe('KakaoTalkListener', () => {
|
|
|
155
168
|
})
|
|
156
169
|
|
|
157
170
|
it('emits member_left on DELMEM push', async () => {
|
|
158
|
-
const client =
|
|
159
|
-
listener =
|
|
171
|
+
const { listener: l, client } = createListener()
|
|
172
|
+
listener = l
|
|
160
173
|
|
|
161
174
|
const left: KakaoTalkPushMemberEvent[] = []
|
|
162
175
|
listener.on('member_left', (event) => left.push(event))
|
|
163
176
|
|
|
164
177
|
await listener.start()
|
|
165
|
-
|
|
178
|
+
client.emitPush('DELMEM', {
|
|
166
179
|
chatId: { high: 0, low: 100 },
|
|
167
180
|
chatLog: { authorId: 42 },
|
|
168
181
|
})
|
|
@@ -176,14 +189,14 @@ describe('KakaoTalkListener', () => {
|
|
|
176
189
|
|
|
177
190
|
describe('read events', () => {
|
|
178
191
|
it('emits read on DECUNREAD push with watermark', async () => {
|
|
179
|
-
const client =
|
|
180
|
-
listener =
|
|
192
|
+
const { listener: l, client } = createListener()
|
|
193
|
+
listener = l
|
|
181
194
|
|
|
182
195
|
const reads: KakaoTalkPushReadEvent[] = []
|
|
183
196
|
listener.on('read', (event) => reads.push(event))
|
|
184
197
|
|
|
185
198
|
await listener.start()
|
|
186
|
-
|
|
199
|
+
client.emitPush('DECUNREAD', {
|
|
187
200
|
chatId: { high: 0, low: 100 },
|
|
188
201
|
userId: 42,
|
|
189
202
|
watermark: { high: 0, low: 999 },
|
|
@@ -199,22 +212,22 @@ describe('KakaoTalkListener', () => {
|
|
|
199
212
|
|
|
200
213
|
describe('kakaotalk_event catch-all', () => {
|
|
201
214
|
it('emits kakaotalk_event for every push event', async () => {
|
|
202
|
-
const client =
|
|
203
|
-
listener =
|
|
215
|
+
const { listener: l, client } = createListener()
|
|
216
|
+
listener = l
|
|
204
217
|
|
|
205
218
|
const events: KakaoTalkPushGenericEvent[] = []
|
|
206
219
|
listener.on('kakaotalk_event', (event) => events.push(event))
|
|
207
220
|
|
|
208
221
|
await listener.start()
|
|
209
|
-
|
|
222
|
+
client.emitPush('MSG', {
|
|
210
223
|
chatId: { high: 0, low: 1 },
|
|
211
224
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'hi', type: 1, sendAt: 1 },
|
|
212
225
|
})
|
|
213
|
-
|
|
226
|
+
client.emitPush('NEWMEM', {
|
|
214
227
|
chatId: { high: 0, low: 1 },
|
|
215
228
|
chatLog: { authorId: 1 },
|
|
216
229
|
})
|
|
217
|
-
|
|
230
|
+
client.emitPush('CUSTOM_EVENT', { some: 'data' })
|
|
218
231
|
|
|
219
232
|
expect(events.length).toBe(3)
|
|
220
233
|
expect(events[0].type).toBe('MSG')
|
|
@@ -224,112 +237,72 @@ describe('KakaoTalkListener', () => {
|
|
|
224
237
|
})
|
|
225
238
|
|
|
226
239
|
describe('stop', () => {
|
|
227
|
-
it('
|
|
228
|
-
const client =
|
|
229
|
-
listener =
|
|
240
|
+
it('unsubscribes from client push and session events', async () => {
|
|
241
|
+
const { listener: l, client } = createListener()
|
|
242
|
+
listener = l
|
|
230
243
|
|
|
231
244
|
await listener.start()
|
|
245
|
+
expect(client.pushHandlers.size).toBe(1)
|
|
246
|
+
expect(client.sessionHandlers.size).toBe(1)
|
|
232
247
|
|
|
233
248
|
listener.stop()
|
|
234
|
-
mockSessionInstance.simulateClose()
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
expect(
|
|
250
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
251
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
238
252
|
})
|
|
239
253
|
})
|
|
240
254
|
|
|
241
|
-
describe('
|
|
242
|
-
it('
|
|
243
|
-
const client =
|
|
244
|
-
listener =
|
|
245
|
-
|
|
246
|
-
const disconnected: boolean[] = []
|
|
247
|
-
listener.on('disconnected', () => disconnected.push(true))
|
|
248
|
-
|
|
249
|
-
await listener.start()
|
|
250
|
-
mockSessionInstance.simulateClose()
|
|
251
|
-
|
|
252
|
-
expect(disconnected.length).toBe(1)
|
|
253
|
-
|
|
254
|
-
await new Promise((r) => setTimeout(r, 1500))
|
|
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
|
-
})
|
|
255
|
+
describe('disconnected event', () => {
|
|
256
|
+
it('emits disconnected when the client session drops', async () => {
|
|
257
|
+
const { listener: l, client } = createListener()
|
|
258
|
+
listener = l
|
|
281
259
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const client = createMockClient()
|
|
285
|
-
listener = new KakaoTalkListener(client)
|
|
260
|
+
const disconnects: number[] = []
|
|
261
|
+
listener.on('disconnected', () => disconnects.push(1))
|
|
286
262
|
|
|
287
263
|
await listener.start()
|
|
288
|
-
|
|
264
|
+
client.emitSessionEvent({ type: 'disconnected' })
|
|
289
265
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
expect((listener as any).reconnectAttempts).toBe(0)
|
|
266
|
+
expect(disconnects.length).toBe(1)
|
|
293
267
|
})
|
|
294
268
|
})
|
|
295
269
|
|
|
296
270
|
describe('KICKOUT', () => {
|
|
297
|
-
it('emits error and stops
|
|
298
|
-
const client =
|
|
299
|
-
listener =
|
|
271
|
+
it('emits error and stops the listener when the client reports a kicked session', async () => {
|
|
272
|
+
const { listener: l, client } = createListener()
|
|
273
|
+
listener = l
|
|
300
274
|
|
|
301
275
|
const errors: Error[] = []
|
|
302
276
|
listener.on('error', (err) => errors.push(err))
|
|
303
277
|
|
|
304
278
|
await listener.start()
|
|
305
|
-
|
|
279
|
+
client.emitSessionEvent({ type: 'kicked', reason: 'Session kicked — another device logged in' })
|
|
306
280
|
|
|
307
281
|
expect(errors.length).toBe(1)
|
|
308
282
|
expect(errors[0].message).toContain('kicked')
|
|
309
|
-
expect((listener as
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
expect(mockLogin).toHaveBeenCalledTimes(1)
|
|
283
|
+
expect((listener as unknown as { running: boolean }).running).toBe(false)
|
|
284
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
285
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
313
286
|
})
|
|
314
287
|
})
|
|
315
288
|
|
|
316
289
|
describe('on/off/once', () => {
|
|
317
290
|
it('off removes listener', async () => {
|
|
318
|
-
const client =
|
|
319
|
-
listener =
|
|
291
|
+
const { listener: l, client } = createListener()
|
|
292
|
+
listener = l
|
|
320
293
|
|
|
321
294
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
322
295
|
const handler = (event: KakaoTalkPushMessageEvent) => messages.push(event)
|
|
323
296
|
listener.on('message', handler)
|
|
324
297
|
|
|
325
298
|
await listener.start()
|
|
326
|
-
|
|
299
|
+
client.emitPush('MSG', {
|
|
327
300
|
chatId: { high: 0, low: 1 },
|
|
328
301
|
chatLog: { logId: { high: 0, low: 1 }, authorId: 1, message: 'first', type: 1, sendAt: 1 },
|
|
329
302
|
})
|
|
330
303
|
|
|
331
304
|
listener.off('message', handler)
|
|
332
|
-
|
|
305
|
+
client.emitPush('MSG', {
|
|
333
306
|
chatId: { high: 0, low: 1 },
|
|
334
307
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'second', type: 1, sendAt: 2 },
|
|
335
308
|
})
|
|
@@ -339,18 +312,18 @@ describe('KakaoTalkListener', () => {
|
|
|
339
312
|
})
|
|
340
313
|
|
|
341
314
|
it('once fires only once', async () => {
|
|
342
|
-
const client =
|
|
343
|
-
listener =
|
|
315
|
+
const { listener: l, client } = createListener()
|
|
316
|
+
listener = l
|
|
344
317
|
|
|
345
318
|
const messages: KakaoTalkPushMessageEvent[] = []
|
|
346
319
|
listener.once('message', (event) => messages.push(event))
|
|
347
320
|
|
|
348
321
|
await listener.start()
|
|
349
|
-
|
|
322
|
+
client.emitPush('MSG', {
|
|
350
323
|
chatId: { high: 0, low: 1 },
|
|
351
324
|
chatLog: { logId: { high: 0, low: 1 }, authorId: 1, message: 'first', type: 1, sendAt: 1 },
|
|
352
325
|
})
|
|
353
|
-
|
|
326
|
+
client.emitPush('MSG', {
|
|
354
327
|
chatId: { high: 0, low: 1 },
|
|
355
328
|
chatLog: { logId: { high: 0, low: 2 }, authorId: 1, message: 'second', type: 1, sendAt: 2 },
|
|
356
329
|
})
|
|
@@ -360,17 +333,50 @@ describe('KakaoTalkListener', () => {
|
|
|
360
333
|
})
|
|
361
334
|
})
|
|
362
335
|
|
|
363
|
-
describe('
|
|
364
|
-
it('
|
|
365
|
-
const client =
|
|
366
|
-
|
|
336
|
+
describe('error during start', () => {
|
|
337
|
+
it('emits error and tears down subscriptions when acquireSession fails', async () => {
|
|
338
|
+
const client = new FakeClient()
|
|
339
|
+
client.acquireImpl = async () => {
|
|
340
|
+
throw new Error('login_failed')
|
|
341
|
+
}
|
|
342
|
+
const l = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
343
|
+
listener = l
|
|
344
|
+
|
|
345
|
+
const errors: Error[] = []
|
|
346
|
+
listener.on('error', (err) => errors.push(err))
|
|
367
347
|
|
|
368
348
|
await listener.start()
|
|
369
|
-
;(listener as any).reconnectAttempts = 5
|
|
370
|
-
listener.stop()
|
|
371
349
|
|
|
350
|
+
expect(errors.length).toBe(1)
|
|
351
|
+
expect(errors[0].message).toBe('login_failed')
|
|
352
|
+
expect(client.pushHandlers.size).toBe(0)
|
|
353
|
+
expect(client.sessionHandlers.size).toBe(0)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('can be restarted after a failed start()', async () => {
|
|
357
|
+
// given — a client whose first acquire fails but later succeeds
|
|
358
|
+
const client = new FakeClient()
|
|
359
|
+
let attempts = 0
|
|
360
|
+
client.acquireImpl = async () => {
|
|
361
|
+
attempts++
|
|
362
|
+
if (attempts === 1) throw new Error('login_failed')
|
|
363
|
+
}
|
|
364
|
+
const l = new KakaoTalkListener(client as unknown as KakaoTalkClient)
|
|
365
|
+
listener = l
|
|
366
|
+
|
|
367
|
+
const errors: Error[] = []
|
|
368
|
+
listener.on('error', (err) => errors.push(err))
|
|
369
|
+
|
|
370
|
+
// when — first start() fails, then we try again
|
|
372
371
|
await listener.start()
|
|
373
|
-
expect(
|
|
372
|
+
expect(errors.length).toBe(1)
|
|
373
|
+
|
|
374
|
+
await listener.start()
|
|
375
|
+
|
|
376
|
+
// then — second start succeeds (subscriptions re-attached, acquire was retried)
|
|
377
|
+
expect(attempts).toBe(2)
|
|
378
|
+
expect(client.pushHandlers.size).toBe(1)
|
|
379
|
+
expect(client.sessionHandlers.size).toBe(1)
|
|
374
380
|
})
|
|
375
381
|
})
|
|
376
382
|
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events'
|
|
2
2
|
|
|
3
|
-
import type { KakaoTalkClient } from './client'
|
|
4
|
-
import { LocoSession } from './protocol/session'
|
|
3
|
+
import type { KakaoSessionEvent, KakaoTalkClient } from './client'
|
|
5
4
|
import type { LocoPacket } from './protocol/types'
|
|
6
5
|
import type {
|
|
7
6
|
KakaoTalkListenerEventMap,
|
|
@@ -11,9 +10,6 @@ import type {
|
|
|
11
10
|
KakaoTalkPushReadEvent,
|
|
12
11
|
} from './types'
|
|
13
12
|
|
|
14
|
-
const RECONNECT_BASE_DELAY = 1_000
|
|
15
|
-
const RECONNECT_MAX_DELAY = 30_000
|
|
16
|
-
|
|
17
13
|
type EventKey = keyof KakaoTalkListenerEventMap
|
|
18
14
|
|
|
19
15
|
function longToString(v: unknown): string {
|
|
@@ -27,11 +23,9 @@ function longToString(v: unknown): string {
|
|
|
27
23
|
export class KakaoTalkListener {
|
|
28
24
|
private client: KakaoTalkClient
|
|
29
25
|
private running = false
|
|
30
|
-
private session: LocoSession | null = null
|
|
31
26
|
private emitter = new EventEmitter()
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private userId: string | null = null
|
|
27
|
+
private unsubscribePush: (() => void) | null = null
|
|
28
|
+
private unsubscribeSession: (() => void) | null = null
|
|
35
29
|
|
|
36
30
|
constructor(client: KakaoTalkClient) {
|
|
37
31
|
this.client = client
|
|
@@ -40,17 +34,33 @@ export class KakaoTalkListener {
|
|
|
40
34
|
async start(): Promise<void> {
|
|
41
35
|
if (this.running) return
|
|
42
36
|
this.running = true
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
|
|
38
|
+
this.unsubscribePush = this.client.onPush((packet) => this.handlePush(packet))
|
|
39
|
+
this.unsubscribeSession = this.client.onSessionEvent((event) => this.handleSessionEvent(event))
|
|
40
|
+
|
|
41
|
+
const alreadyConnected = this.client.isConnected()
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await this.client.acquireSession()
|
|
45
|
+
if (!this.running) return
|
|
46
|
+
if (alreadyConnected) {
|
|
47
|
+
const { userId } = this.client.getCredentials()
|
|
48
|
+
this.emitter.emit('connected', { userId })
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
52
|
+
this.running = false
|
|
53
|
+
this.teardown()
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
stop(): void {
|
|
48
|
-
this.running
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.session.close()
|
|
52
|
-
this.session = null
|
|
58
|
+
if (!this.running) {
|
|
59
|
+
this.teardown()
|
|
60
|
+
return
|
|
53
61
|
}
|
|
62
|
+
this.running = false
|
|
63
|
+
this.teardown()
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
on<K extends EventKey>(event: K, listener: (...args: KakaoTalkListenerEventMap[K]) => void): this {
|
|
@@ -68,41 +78,28 @@ export class KakaoTalkListener {
|
|
|
68
78
|
return this
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.userId = userId
|
|
79
|
-
const session = new LocoSession()
|
|
80
|
-
|
|
81
|
-
session.onPush((packet) => this.handlePush(packet))
|
|
82
|
-
session.onClose(() => {
|
|
83
|
-
if (this.session !== session) return
|
|
84
|
-
this.session = null
|
|
85
|
-
if (this.running) {
|
|
86
|
-
this.emitter.emit('disconnected')
|
|
87
|
-
this.scheduleReconnect()
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
await session.login(oauthToken, userId, deviceUuid, undefined, deviceType)
|
|
81
|
+
private teardown(): void {
|
|
82
|
+
this.unsubscribePush?.()
|
|
83
|
+
this.unsubscribePush = null
|
|
84
|
+
this.unsubscribeSession?.()
|
|
85
|
+
this.unsubscribeSession = null
|
|
86
|
+
}
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
96
|
-
}
|
|
88
|
+
private handleSessionEvent(event: KakaoSessionEvent): void {
|
|
89
|
+
if (!this.running) return
|
|
97
90
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
switch (event.type) {
|
|
92
|
+
case 'connected':
|
|
93
|
+
this.emitter.emit('connected', { userId: event.userId })
|
|
94
|
+
break
|
|
95
|
+
case 'disconnected':
|
|
96
|
+
this.emitter.emit('disconnected')
|
|
97
|
+
break
|
|
98
|
+
case 'kicked':
|
|
99
|
+
this.emitter.emit('error', new Error(event.reason))
|
|
100
|
+
this.running = false
|
|
101
|
+
this.teardown()
|
|
102
|
+
break
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
|
|
@@ -162,23 +159,6 @@ export class KakaoTalkListener {
|
|
|
162
159
|
break
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
case 'CHANGESVR': {
|
|
166
|
-
this.reconnectAttempts = 0
|
|
167
|
-
const prev = this.session
|
|
168
|
-
this.session = null
|
|
169
|
-
prev?.close()
|
|
170
|
-
this.connect()
|
|
171
|
-
break
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
case 'KICKOUT': {
|
|
175
|
-
this.emitter.emit('error', new Error('Session kicked — another device logged in'))
|
|
176
|
-
this.running = false
|
|
177
|
-
this.session?.close()
|
|
178
|
-
this.session = null
|
|
179
|
-
break
|
|
180
|
-
}
|
|
181
|
-
|
|
182
162
|
default: {
|
|
183
163
|
const event: KakaoTalkPushGenericEvent = { type: method, ...body }
|
|
184
164
|
this.emitter.emit('kakaotalk_event', event)
|
|
@@ -186,18 +166,4 @@ export class KakaoTalkListener {
|
|
|
186
166
|
}
|
|
187
167
|
}
|
|
188
168
|
}
|
|
189
|
-
|
|
190
|
-
private scheduleReconnect(): void {
|
|
191
|
-
this.clearTimers()
|
|
192
|
-
const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
|
|
193
|
-
this.reconnectAttempts++
|
|
194
|
-
this.reconnectTimer = setTimeout(() => this.connect(), delay)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private clearTimers(): void {
|
|
198
|
-
if (this.reconnectTimer) {
|
|
199
|
-
clearTimeout(this.reconnectTimer)
|
|
200
|
-
this.reconnectTimer = null
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
169
|
}
|