agent-messenger 2.19.1 → 2.19.3
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/line/client.d.ts +27 -0
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +94 -7
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/listener.d.ts +3 -0
- package/dist/src/platforms/line/listener.d.ts.map +1 -1
- package/dist/src/platforms/line/listener.js +68 -47
- package/dist/src/platforms/line/listener.js.map +1 -1
- package/dist/src/platforms/line/types.d.ts +1 -0
- package/dist/src/platforms/line/types.d.ts.map +1 -1
- package/dist/src/platforms/line/types.js.map +1 -1
- package/docs/content/docs/cli/line.mdx +10 -9
- package/package.json +1 -1
- 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 +1 -1
- package/skills/agent-line/SKILL.md +2 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/line/client.test.ts +212 -1
- package/src/platforms/line/client.ts +114 -7
- package/src/platforms/line/listener.test.ts +164 -31
- package/src/platforms/line/listener.ts +66 -51
- package/src/platforms/line/types.ts +2 -0
|
@@ -1,60 +1,95 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import type { LineRawEvent } from '@/platforms/line/client'
|
|
3
4
|
import { LineListener } from '@/platforms/line/listener'
|
|
4
5
|
import type { LinePushGenericEvent, LinePushMessageEvent } from '@/platforms/line/types'
|
|
5
6
|
|
|
6
|
-
const mockGetProfile = mock(() => Promise.resolve({ mid: 'u123',
|
|
7
|
+
const mockGetProfile = mock(() => Promise.resolve({ mid: 'u123', display_name: 'Test User' }))
|
|
7
8
|
|
|
8
|
-
let mockInternalClientInstance:
|
|
9
|
+
let mockInternalClientInstance: MockEventSource
|
|
9
10
|
|
|
10
|
-
class
|
|
11
|
-
|
|
12
|
-
private
|
|
11
|
+
class MockEventSource {
|
|
12
|
+
private queue: LineRawEvent[] = []
|
|
13
|
+
private resolveNext: ((value: IteratorResult<LineRawEvent>) => void) | null = null
|
|
14
|
+
private streamError: Error | null = null
|
|
15
|
+
private done = false
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
async *streamEvents(signal: AbortSignal): AsyncGenerator<LineRawEvent, void, unknown> {
|
|
18
|
+
signal.addEventListener('abort', () => {
|
|
19
|
+
const err = new Error('The operation was aborted')
|
|
20
|
+
err.name = 'AbortError'
|
|
21
|
+
this.fail(err)
|
|
22
|
+
})
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
while (!this.done) {
|
|
25
|
+
if (this.streamError) {
|
|
26
|
+
const err = this.streamError
|
|
27
|
+
this.streamError = null
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
30
|
+
if (this.queue.length > 0) {
|
|
31
|
+
yield this.queue.shift()!
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
const next = await new Promise<IteratorResult<LineRawEvent>>((resolve) => {
|
|
35
|
+
this.resolveNext = resolve
|
|
26
36
|
})
|
|
27
|
-
|
|
37
|
+
if (next.done) return
|
|
38
|
+
if (this.streamError) {
|
|
39
|
+
const err = this.streamError
|
|
40
|
+
this.streamError = null
|
|
41
|
+
throw err
|
|
42
|
+
}
|
|
43
|
+
yield next.value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private push(event: LineRawEvent): void {
|
|
48
|
+
if (this.resolveNext) {
|
|
49
|
+
const resolve = this.resolveNext
|
|
50
|
+
this.resolveNext = null
|
|
51
|
+
resolve({ value: event, done: false })
|
|
52
|
+
} else {
|
|
53
|
+
this.queue.push(event)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fail(error: Error): void {
|
|
58
|
+
this.streamError = error
|
|
59
|
+
if (this.resolveNext) {
|
|
60
|
+
const resolve = this.resolveNext
|
|
61
|
+
this.resolveNext = null
|
|
62
|
+
resolve({ value: undefined as never, done: false })
|
|
63
|
+
}
|
|
28
64
|
}
|
|
29
65
|
|
|
30
|
-
simulateMessage(
|
|
31
|
-
this.
|
|
66
|
+
simulateMessage(message: unknown): void {
|
|
67
|
+
this.push({ kind: 'message', message: message as never })
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
simulateEvent(op: unknown): void {
|
|
35
|
-
this.
|
|
71
|
+
this.push({ kind: 'event', op: op as never })
|
|
36
72
|
}
|
|
37
73
|
|
|
38
74
|
simulateListenError(error: Error): void {
|
|
39
|
-
this.
|
|
75
|
+
this.fail(error)
|
|
40
76
|
}
|
|
77
|
+
}
|
|
41
78
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
79
|
+
function flush(): Promise<void> {
|
|
80
|
+
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
const mockLogin = mock((): Promise<void> => {
|
|
48
|
-
mockInternalClientInstance = new
|
|
84
|
+
mockInternalClientInstance = new MockEventSource()
|
|
49
85
|
return Promise.resolve()
|
|
50
86
|
})
|
|
51
87
|
|
|
52
88
|
function createMockLineClient() {
|
|
53
89
|
return {
|
|
54
90
|
login: mockLogin,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
},
|
|
91
|
+
getProfile: mockGetProfile,
|
|
92
|
+
streamEvents: (signal: AbortSignal) => mockInternalClientInstance.streamEvents(signal),
|
|
58
93
|
} as any
|
|
59
94
|
}
|
|
60
95
|
|
|
@@ -65,11 +100,11 @@ describe('LineListener', () => {
|
|
|
65
100
|
listener?.stop()
|
|
66
101
|
mockLogin.mockReset()
|
|
67
102
|
mockLogin.mockImplementation((): Promise<void> => {
|
|
68
|
-
mockInternalClientInstance = new
|
|
103
|
+
mockInternalClientInstance = new MockEventSource()
|
|
69
104
|
return Promise.resolve()
|
|
70
105
|
})
|
|
71
106
|
mockGetProfile.mockReset()
|
|
72
|
-
mockGetProfile.mockResolvedValue({ mid: 'u123',
|
|
107
|
+
mockGetProfile.mockResolvedValue({ mid: 'u123', display_name: 'Test User' })
|
|
73
108
|
})
|
|
74
109
|
|
|
75
110
|
describe('start', () => {
|
|
@@ -128,6 +163,7 @@ describe('LineListener', () => {
|
|
|
128
163
|
createdTime: 1700000000000,
|
|
129
164
|
},
|
|
130
165
|
})
|
|
166
|
+
await flush()
|
|
131
167
|
|
|
132
168
|
expect(messages.length).toBe(1)
|
|
133
169
|
expect(messages[0].type).toBe('message')
|
|
@@ -136,6 +172,64 @@ describe('LineListener', () => {
|
|
|
136
172
|
expect(messages[0].author_id).toBe('u456')
|
|
137
173
|
expect(messages[0].text).toBe('hello world')
|
|
138
174
|
expect(messages[0].content_type).toBe('NONE')
|
|
175
|
+
expect(messages[0].content_metadata).toEqual({})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('forwards contentMetadata for non-text messages', async () => {
|
|
179
|
+
const client = createMockLineClient()
|
|
180
|
+
listener = new LineListener(client)
|
|
181
|
+
|
|
182
|
+
const messages: LinePushMessageEvent[] = []
|
|
183
|
+
listener.on('message', (event) => messages.push(event))
|
|
184
|
+
|
|
185
|
+
await listener.start()
|
|
186
|
+
mockInternalClientInstance.simulateMessage({
|
|
187
|
+
isMyMessage: false,
|
|
188
|
+
from: { type: 'USER', id: 'u456' },
|
|
189
|
+
to: { type: 'USER', id: 'u123' },
|
|
190
|
+
text: null,
|
|
191
|
+
raw: {
|
|
192
|
+
id: 'msg010',
|
|
193
|
+
contentType: 'STICKER',
|
|
194
|
+
createdTime: 1700000007000,
|
|
195
|
+
contentMetadata: { STKID: '123', STKPKGID: '456', STKVER: '1' },
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
await flush()
|
|
199
|
+
|
|
200
|
+
expect(messages.length).toBe(1)
|
|
201
|
+
expect(messages[0].text).toBeNull()
|
|
202
|
+
expect(messages[0].content_type).toBe('STICKER')
|
|
203
|
+
expect(messages[0].content_metadata).toEqual({ STKID: '123', STKPKGID: '456', STKVER: '1' })
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('coerces non-string contentMetadata values to strings', async () => {
|
|
207
|
+
const client = createMockLineClient()
|
|
208
|
+
listener = new LineListener(client)
|
|
209
|
+
|
|
210
|
+
const messages: LinePushMessageEvent[] = []
|
|
211
|
+
listener.on('message', (event) => messages.push(event))
|
|
212
|
+
|
|
213
|
+
await listener.start()
|
|
214
|
+
mockInternalClientInstance.simulateMessage({
|
|
215
|
+
isMyMessage: false,
|
|
216
|
+
from: { type: 'USER', id: 'u456' },
|
|
217
|
+
to: { type: 'USER', id: 'u123' },
|
|
218
|
+
text: null,
|
|
219
|
+
raw: {
|
|
220
|
+
id: 'msg011',
|
|
221
|
+
contentType: 'IMAGE',
|
|
222
|
+
createdTime: 1700000008000,
|
|
223
|
+
contentMetadata: { FILE_SIZE: 2048, PREVIEW_URL: 'https://example.com/p.jpg', EMPTY: null },
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
await flush()
|
|
227
|
+
|
|
228
|
+
expect(messages.length).toBe(1)
|
|
229
|
+
expect(messages[0].content_metadata).toEqual({
|
|
230
|
+
FILE_SIZE: '2048',
|
|
231
|
+
PREVIEW_URL: 'https://example.com/p.jpg',
|
|
232
|
+
})
|
|
139
233
|
})
|
|
140
234
|
|
|
141
235
|
it('uses to.id as chat_id for own messages', async () => {
|
|
@@ -153,6 +247,7 @@ describe('LineListener', () => {
|
|
|
153
247
|
text: 'sent by me',
|
|
154
248
|
raw: { id: 'msg002', contentType: 'NONE', createdTime: 1700000001000 },
|
|
155
249
|
})
|
|
250
|
+
await flush()
|
|
156
251
|
|
|
157
252
|
expect(messages.length).toBe(1)
|
|
158
253
|
expect(messages[0].chat_id).toBe('u456')
|
|
@@ -175,6 +270,7 @@ describe('LineListener', () => {
|
|
|
175
270
|
text: 'hi',
|
|
176
271
|
raw: { id: 'msg003', contentType: 'NONE', createdTime: 1700000002000 },
|
|
177
272
|
})
|
|
273
|
+
await flush()
|
|
178
274
|
|
|
179
275
|
expect(events.length).toBe(1)
|
|
180
276
|
expect(events[0].type).toBe('message')
|
|
@@ -189,6 +285,7 @@ describe('LineListener', () => {
|
|
|
189
285
|
|
|
190
286
|
await listener.start()
|
|
191
287
|
mockInternalClientInstance.simulateEvent({ type: 'NOTIFIED_READ_MESSAGE', revision: 42 })
|
|
288
|
+
await flush()
|
|
192
289
|
|
|
193
290
|
expect(events.length).toBe(1)
|
|
194
291
|
expect(events[0].type).toBe('NOTIFIED_READ_MESSAGE')
|
|
@@ -233,7 +330,7 @@ describe('LineListener', () => {
|
|
|
233
330
|
mockLogin.mockImplementation((): Promise<void> => {
|
|
234
331
|
callCount++
|
|
235
332
|
if (callCount === 1) return Promise.reject(new Error('network_error'))
|
|
236
|
-
mockInternalClientInstance = new
|
|
333
|
+
mockInternalClientInstance = new MockEventSource()
|
|
237
334
|
return Promise.resolve()
|
|
238
335
|
})
|
|
239
336
|
|
|
@@ -283,6 +380,7 @@ describe('LineListener', () => {
|
|
|
283
380
|
text: 'first',
|
|
284
381
|
raw: { id: 'msg004', contentType: 'NONE', createdTime: 1700000003000 },
|
|
285
382
|
})
|
|
383
|
+
await flush()
|
|
286
384
|
|
|
287
385
|
listener.off('message', handler)
|
|
288
386
|
mockInternalClientInstance.simulateMessage({
|
|
@@ -292,6 +390,7 @@ describe('LineListener', () => {
|
|
|
292
390
|
text: 'second',
|
|
293
391
|
raw: { id: 'msg005', contentType: 'NONE', createdTime: 1700000004000 },
|
|
294
392
|
})
|
|
393
|
+
await flush()
|
|
295
394
|
|
|
296
395
|
expect(messages.length).toBe(1)
|
|
297
396
|
expect(messages[0].text).toBe('first')
|
|
@@ -319,6 +418,7 @@ describe('LineListener', () => {
|
|
|
319
418
|
text: 'second',
|
|
320
419
|
raw: { id: 'msg007', contentType: 'NONE', createdTime: 1700000006000 },
|
|
321
420
|
})
|
|
421
|
+
await flush()
|
|
322
422
|
|
|
323
423
|
expect(messages.length).toBe(1)
|
|
324
424
|
expect(messages[0].text).toBe('first')
|
|
@@ -338,4 +438,37 @@ describe('LineListener', () => {
|
|
|
338
438
|
expect((listener as any).reconnectAttempts).toBe(0)
|
|
339
439
|
})
|
|
340
440
|
})
|
|
441
|
+
|
|
442
|
+
describe('event source consumption', () => {
|
|
443
|
+
it('consumes streamEvents and stops pumping after abort', async () => {
|
|
444
|
+
const streamCalls: AbortSignal[] = []
|
|
445
|
+
const client = {
|
|
446
|
+
login: mockLogin,
|
|
447
|
+
getProfile: mockGetProfile,
|
|
448
|
+
streamEvents: (signal: AbortSignal) => {
|
|
449
|
+
streamCalls.push(signal)
|
|
450
|
+
return mockInternalClientInstance.streamEvents(signal)
|
|
451
|
+
},
|
|
452
|
+
} as any
|
|
453
|
+
listener = new LineListener(client)
|
|
454
|
+
|
|
455
|
+
const messages: LinePushMessageEvent[] = []
|
|
456
|
+
listener.on('message', (event) => messages.push(event))
|
|
457
|
+
|
|
458
|
+
await listener.start()
|
|
459
|
+
expect(streamCalls.length).toBe(1)
|
|
460
|
+
|
|
461
|
+
listener.stop()
|
|
462
|
+
mockInternalClientInstance.simulateMessage({
|
|
463
|
+
isMyMessage: false,
|
|
464
|
+
from: { type: 'USER', id: 'u456' },
|
|
465
|
+
to: { type: 'USER', id: 'u123' },
|
|
466
|
+
text: 'after stop',
|
|
467
|
+
raw: { id: 'msg100', contentType: 'NONE', createdTime: 1700000010000 },
|
|
468
|
+
})
|
|
469
|
+
await flush()
|
|
470
|
+
|
|
471
|
+
expect(messages.length).toBe(0)
|
|
472
|
+
})
|
|
473
|
+
})
|
|
341
474
|
})
|
|
@@ -2,7 +2,6 @@ import { EventEmitter } from 'events'
|
|
|
2
2
|
|
|
3
3
|
import type { LineClient } from './client'
|
|
4
4
|
import type { LineListenerEventMap, LinePushGenericEvent, LinePushMessageEvent } from './types'
|
|
5
|
-
import { LineError } from './types'
|
|
6
5
|
|
|
7
6
|
const RECONNECT_BASE_DELAY = 1_000
|
|
8
7
|
const RECONNECT_MAX_DELAY = 30_000
|
|
@@ -61,59 +60,13 @@ export class LineListener {
|
|
|
61
60
|
await this.lineClient.login()
|
|
62
61
|
if (!this.running) return
|
|
63
62
|
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
66
|
-
throw new LineError('not_connected', 'Failed to get internal LINE client')
|
|
67
|
-
}
|
|
68
|
-
|
|
63
|
+
const profile = await this.lineClient.getProfile()
|
|
64
|
+
if (!this.running) return
|
|
69
65
|
this.abortController = new AbortController()
|
|
70
|
-
|
|
71
|
-
internalClient.on('message', (msg: any) => {
|
|
72
|
-
try {
|
|
73
|
-
const toType = msg.raw.toType
|
|
74
|
-
const isGroupOrRoom = toType === 'GROUP' || toType === 'ROOM' || toType === 0 || toType === 1
|
|
75
|
-
const chatId = isGroupOrRoom ? msg.to.id : msg.isMyMessage ? msg.to.id : msg.from.id
|
|
76
|
-
|
|
77
|
-
const event: LinePushMessageEvent = {
|
|
78
|
-
type: 'message',
|
|
79
|
-
chat_id: chatId,
|
|
80
|
-
message_id: String(msg.raw.id),
|
|
81
|
-
author_id: msg.from.id,
|
|
82
|
-
text: msg.text ?? null,
|
|
83
|
-
content_type: String(msg.raw.contentType ?? 'NONE'),
|
|
84
|
-
sent_at: new Date(Number(msg.raw.createdTime)).toISOString(),
|
|
85
|
-
}
|
|
86
|
-
this.emitter.emit('message', event)
|
|
87
|
-
this.emitter.emit('line_event', { ...event })
|
|
88
|
-
} catch (error) {
|
|
89
|
-
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
internalClient.on('event', (op: any) => {
|
|
94
|
-
const event: LinePushGenericEvent = {
|
|
95
|
-
type: String(op.type ?? 'unknown'),
|
|
96
|
-
...(op.revision !== undefined && { revision: String(op.revision) }),
|
|
97
|
-
...(op.createdTime !== undefined && {
|
|
98
|
-
created_time: new Date(Number(op.createdTime)).toISOString(),
|
|
99
|
-
}),
|
|
100
|
-
}
|
|
101
|
-
this.emitter.emit('line_event', event)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
internalClient.listen({ signal: this.abortController.signal })?.catch((error: unknown) => {
|
|
105
|
-
if (!this.running) return
|
|
106
|
-
const err = error instanceof Error ? error : new Error(String(error))
|
|
107
|
-
if (err.name === 'AbortError') return
|
|
108
|
-
this.emitter.emit('error', err)
|
|
109
|
-
this.emitter.emit('disconnected')
|
|
110
|
-
this.scheduleReconnect()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
66
|
this.reconnectAttempts = 0
|
|
114
|
-
|
|
115
|
-
const profile = await internalClient.base.talk.getProfile()
|
|
116
67
|
this.emitter.emit('connected', { account_id: profile.mid })
|
|
68
|
+
|
|
69
|
+
void this.pump(this.abortController.signal)
|
|
117
70
|
} catch (error) {
|
|
118
71
|
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
119
72
|
if (this.running) {
|
|
@@ -122,6 +75,59 @@ export class LineListener {
|
|
|
122
75
|
}
|
|
123
76
|
}
|
|
124
77
|
|
|
78
|
+
private async pump(signal: AbortSignal): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
for await (const event of this.lineClient.streamEvents(signal)) {
|
|
81
|
+
if (event.kind === 'message') {
|
|
82
|
+
this.emitMessage(event.message)
|
|
83
|
+
} else {
|
|
84
|
+
this.emitOperation(event.op)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!this.running || signal.aborted) return
|
|
89
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
90
|
+
if (err.name === 'AbortError') return
|
|
91
|
+
this.emitter.emit('error', err)
|
|
92
|
+
this.emitter.emit('disconnected')
|
|
93
|
+
this.scheduleReconnect()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private emitMessage(msg: any): void {
|
|
98
|
+
try {
|
|
99
|
+
const toType = msg.raw.toType
|
|
100
|
+
const isGroupOrRoom = toType === 'GROUP' || toType === 'ROOM' || toType === 0 || toType === 1
|
|
101
|
+
const chatId = isGroupOrRoom ? msg.to.id : msg.isMyMessage ? msg.to.id : msg.from.id
|
|
102
|
+
|
|
103
|
+
const event: LinePushMessageEvent = {
|
|
104
|
+
type: 'message',
|
|
105
|
+
chat_id: chatId,
|
|
106
|
+
message_id: String(msg.raw.id),
|
|
107
|
+
author_id: msg.from.id,
|
|
108
|
+
text: msg.text ?? null,
|
|
109
|
+
content_type: String(msg.raw.contentType ?? 'NONE'),
|
|
110
|
+
content_metadata: normalizeContentMetadata(msg.raw.contentMetadata),
|
|
111
|
+
sent_at: new Date(Number(msg.raw.createdTime)).toISOString(),
|
|
112
|
+
}
|
|
113
|
+
this.emitter.emit('message', event)
|
|
114
|
+
this.emitter.emit('line_event', { ...event })
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private emitOperation(op: any): void {
|
|
121
|
+
const event: LinePushGenericEvent = {
|
|
122
|
+
type: String(op.type ?? 'unknown'),
|
|
123
|
+
...(op.revision !== undefined && { revision: String(op.revision) }),
|
|
124
|
+
...(op.createdTime !== undefined && {
|
|
125
|
+
created_time: new Date(Number(op.createdTime)).toISOString(),
|
|
126
|
+
}),
|
|
127
|
+
}
|
|
128
|
+
this.emitter.emit('line_event', event)
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
private scheduleReconnect(): void {
|
|
126
132
|
this.clearTimers()
|
|
127
133
|
const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
|
|
@@ -136,3 +142,12 @@ export class LineListener {
|
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
}
|
|
145
|
+
|
|
146
|
+
function normalizeContentMetadata(raw: unknown): Record<string, string> {
|
|
147
|
+
if (!raw || typeof raw !== 'object') return {}
|
|
148
|
+
const result: Record<string, string> = {}
|
|
149
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
150
|
+
if (value !== null && value !== undefined) result[key] = String(value)
|
|
151
|
+
}
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
@@ -138,6 +138,8 @@ export interface LinePushMessageEvent {
|
|
|
138
138
|
author_id: string
|
|
139
139
|
text: string | null
|
|
140
140
|
content_type: string
|
|
141
|
+
// Raw LINE contentMetadata (sticker IDs, file name/size, media URLs); empty for plain text.
|
|
142
|
+
content_metadata: Record<string, string>
|
|
141
143
|
sent_at: string
|
|
142
144
|
}
|
|
143
145
|
|