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.
Files changed (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/line/client.d.ts +27 -0
  4. package/dist/src/platforms/line/client.d.ts.map +1 -1
  5. package/dist/src/platforms/line/client.js +94 -7
  6. package/dist/src/platforms/line/client.js.map +1 -1
  7. package/dist/src/platforms/line/listener.d.ts +3 -0
  8. package/dist/src/platforms/line/listener.d.ts.map +1 -1
  9. package/dist/src/platforms/line/listener.js +68 -47
  10. package/dist/src/platforms/line/listener.js.map +1 -1
  11. package/dist/src/platforms/line/types.d.ts +1 -0
  12. package/dist/src/platforms/line/types.d.ts.map +1 -1
  13. package/dist/src/platforms/line/types.js.map +1 -1
  14. package/docs/content/docs/cli/line.mdx +10 -9
  15. package/package.json +1 -1
  16. package/skills/agent-channeltalk/SKILL.md +1 -1
  17. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  18. package/skills/agent-discord/SKILL.md +1 -1
  19. package/skills/agent-discordbot/SKILL.md +1 -1
  20. package/skills/agent-instagram/SKILL.md +1 -1
  21. package/skills/agent-kakaotalk/SKILL.md +1 -1
  22. package/skills/agent-line/SKILL.md +2 -1
  23. package/skills/agent-slack/SKILL.md +1 -1
  24. package/skills/agent-slackbot/SKILL.md +1 -1
  25. package/skills/agent-teams/SKILL.md +1 -1
  26. package/skills/agent-telegram/SKILL.md +1 -1
  27. package/skills/agent-telegrambot/SKILL.md +1 -1
  28. package/skills/agent-webex/SKILL.md +1 -1
  29. package/skills/agent-wechatbot/SKILL.md +1 -1
  30. package/skills/agent-whatsapp/SKILL.md +1 -1
  31. package/skills/agent-whatsappbot/SKILL.md +1 -1
  32. package/src/platforms/line/client.test.ts +212 -1
  33. package/src/platforms/line/client.ts +114 -7
  34. package/src/platforms/line/listener.test.ts +164 -31
  35. package/src/platforms/line/listener.ts +66 -51
  36. package/src/platforms/line/types.ts +2 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-kakaotalk
3
3
  description: Interact with KakaoTalk - send messages, read chats, manage conversations
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-kakaotalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -429,6 +429,7 @@ See the [LINE SDK documentation](https://agent-messenger.dev/docs/sdk/line) for
429
429
 
430
430
  - No auto-extraction of credentials (requires interactive login via QR code or email/password)
431
431
  - E2EE (Letter Sealing) may prevent reading some message content
432
+ - Sending to chats that **require** E2EE (Letter Sealing) is not supported on auth-token sessions; such sends fail with `e2ee_required`
432
433
  - No file upload support yet
433
434
  - No sticker or rich message sending (text only)
434
435
  - No group creation or management
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.19.1
4
+ version: 2.19.3
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,6 +1,7 @@
1
- import { describe, expect, it } from 'bun:test'
1
+ import { describe, expect, it, mock } from 'bun:test'
2
2
 
3
3
  import { LineClient } from './client'
4
+ import type { LineRawEvent } from './client'
4
5
  import { LineError } from './types'
5
6
  import type { LineChat, LineDevice, LineLoginResult, LineMessage, LineSendResult } from './types'
6
7
 
@@ -54,6 +55,216 @@ describe('LineClient', () => {
54
55
  })
55
56
  })
56
57
 
58
+ describe('getMessages()', () => {
59
+ function clientWithTalk(talk: Record<string, unknown>): LineClient {
60
+ const client = new LineClient()
61
+ ;(client as any).client = { base: { talk } }
62
+ return client
63
+ }
64
+
65
+ it('returns empty when the chat has no messages', async () => {
66
+ const client = clientWithTalk({
67
+ getServerTime: async () => 1700000000000,
68
+ getPreviousMessagesV2WithRequest: async () => [],
69
+ })
70
+ expect(await client.getMessages('chat1')).toEqual([])
71
+ })
72
+
73
+ it('queries from the latest message regardless of message-box position', async () => {
74
+ let request: any
75
+ const client = clientWithTalk({
76
+ getServerTime: async () => 1700000000000,
77
+ getPreviousMessagesV2WithRequest: async (args: any) => {
78
+ request = args.request
79
+ return [{ id: '30', from: 'u1', text: 'c', contentType: 'NONE', createdTime: 3 }]
80
+ },
81
+ })
82
+
83
+ const result = await client.getMessages('chat-not-in-top-boxes', { count: 10 })
84
+ expect(request.messageBoxId).toBe('chat-not-in-top-boxes')
85
+ expect(request.endMessageId.messageId).toBe(9223372036854775807n)
86
+ expect(result.map((m) => m.message_id)).toEqual(['30'])
87
+ })
88
+
89
+ it('maps vendor fields and preserves order/count from the request', async () => {
90
+ const raw = [
91
+ { id: '30', from: 'u1', text: 'c', contentType: 'NONE', createdTime: 1700000003000 },
92
+ { id: '20', from: 'u1', text: 'b', contentType: 'NONE', createdTime: 1700000002000 },
93
+ ]
94
+ let requestedCount: number | undefined
95
+ const client = clientWithTalk({
96
+ getServerTime: async () => 1700000000000,
97
+ getPreviousMessagesV2WithRequest: async (args: any) => {
98
+ requestedCount = args.request.messagesCount
99
+ return raw
100
+ },
101
+ })
102
+
103
+ const result = await client.getMessages('chat1', { count: 2 })
104
+ expect(requestedCount).toBe(2)
105
+ expect(result.map((m) => m.message_id)).toEqual(['30', '20'])
106
+ expect(result.map((m) => m.text)).toEqual(['c', 'b'])
107
+ })
108
+ })
109
+
110
+ describe('sendMessage()', () => {
111
+ function clientWithTalk(talk: Record<string, unknown>): LineClient {
112
+ const client = new LineClient()
113
+ ;(client as any).client = { base: { talk } }
114
+ return client
115
+ }
116
+
117
+ it('sends with E2EE when available', async () => {
118
+ const sendMessage = mock(async () => ({ id: '1', createdTime: 1700000000000 }))
119
+ const client = clientWithTalk({ sendMessage })
120
+ const result = await client.sendMessage('chat1', 'hi')
121
+
122
+ expect(result.success).toBe(true)
123
+ expect(result.message_id).toBe('1')
124
+ expect(sendMessage.mock.calls[0][0]).toMatchObject({ e2ee: true })
125
+ })
126
+
127
+ it('falls back to plain text when E2EE key is unavailable', async () => {
128
+ let call = 0
129
+ const sendMessage = mock(async () => {
130
+ call++
131
+ if (call === 1) throw new Error('E2EE Key has not been saved')
132
+ return { id: '2', createdTime: 1700000001000 }
133
+ })
134
+ const client = clientWithTalk({ sendMessage })
135
+ const result = await client.sendMessage('chat1', 'hi')
136
+
137
+ expect(result.message_id).toBe('2')
138
+ expect(sendMessage.mock.calls[1][0]).toMatchObject({ e2ee: false })
139
+ })
140
+
141
+ it('throws e2ee_required when the chat mandates encryption and plain is rejected', async () => {
142
+ const sendMessage = mock(async (args: { e2ee: boolean }) => {
143
+ if (args.e2ee) throw new Error('E2EE Key has not been saved')
144
+ throw new Error('{"code":"E2EE_RETRY_ENCRYPT","reason":"can not send using plain mode"}')
145
+ })
146
+ const client = clientWithTalk({ sendMessage })
147
+
148
+ await expect(client.sendMessage('chat1', 'hi')).rejects.toMatchObject({ code: 'e2ee_required' })
149
+ })
150
+
151
+ it('rethrows non-E2EE send errors unchanged', async () => {
152
+ const sendMessage = mock(async () => {
153
+ throw new Error('rate limited')
154
+ })
155
+ const client = clientWithTalk({ sendMessage })
156
+
157
+ await expect(client.sendMessage('chat1', 'hi')).rejects.toMatchObject({ code: 'send_message_failed' })
158
+ })
159
+ })
160
+
161
+ describe('streamEvents()', () => {
162
+ function clientWithStream(ops: unknown[], decrypt: (m: unknown) => Promise<unknown>): LineClient {
163
+ const client = new LineClient()
164
+ ;(client as any).client = {
165
+ base: {
166
+ profile: { mid: 'me' },
167
+ createPolling: () => ({
168
+ // eslint-disable-next-line require-yield
169
+ async *_listenTalkEvents() {
170
+ for (const op of ops) yield op
171
+ },
172
+ }),
173
+ e2ee: { decryptE2EEMessage: decrypt },
174
+ },
175
+ }
176
+ return client
177
+ }
178
+
179
+ async function collect(client: LineClient): Promise<LineRawEvent[]> {
180
+ const out: LineRawEvent[] = []
181
+ for await (const e of client.streamEvents(new AbortController().signal)) out.push(e)
182
+ return out
183
+ }
184
+
185
+ it('decrypts messages and emits event + message', async () => {
186
+ const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me' } }
187
+ const client = clientWithStream([op], async () => ({ id: '1', from: 'u1', to: 'me', text: 'hello' }))
188
+
189
+ const events = await collect(client)
190
+ expect(events.map((e) => e.kind)).toEqual(['event', 'message'])
191
+ const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
192
+ expect(msg.message.text).toBe('hello')
193
+ })
194
+
195
+ it('keeps streaming when E2EE decryption fails (no reconnect loop)', async () => {
196
+ const ops = [
197
+ { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me', text: 'plain-on-raw' } },
198
+ { type: 'NOTIFIED_READ_MESSAGE' },
199
+ ]
200
+ const client = clientWithStream(ops, async () => {
201
+ throw new Error('E2EE decrypt failed')
202
+ })
203
+
204
+ const events = await collect(client)
205
+ expect(events.map((e) => e.kind)).toEqual(['event', 'message', 'event'])
206
+ const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
207
+ expect(msg.message.from.id).toBe('u1')
208
+ expect(msg.message.text).toBeNull()
209
+ })
210
+
211
+ it('propagates polling errors so the listener can reconnect', async () => {
212
+ const client = new LineClient()
213
+ ;(client as any).client = {
214
+ base: {
215
+ profile: { mid: 'me' },
216
+ createPolling: () => ({
217
+ async *_listenTalkEvents(opts: { onError?: (e: unknown) => void }) {
218
+ opts.onError?.(new Error('sync failed'))
219
+ yield undefined as never
220
+ },
221
+ }),
222
+ e2ee: { decryptE2EEMessage: async (m: unknown) => m },
223
+ },
224
+ }
225
+
226
+ await expect(collect(client)).rejects.toThrow('sync failed')
227
+ })
228
+
229
+ it('swallows empty long-poll errors so the connection is not torn down', async () => {
230
+ const client = new LineClient()
231
+ ;(client as any).client = {
232
+ base: {
233
+ profile: { mid: 'me' },
234
+ createPolling: () => ({
235
+ async *_listenTalkEvents(opts: { onError?: (e: unknown) => void }) {
236
+ // given: an idle long-poll returns an empty body, then a real op arrives
237
+ opts.onError?.(new Error('Request internal failed: Invalid response buffer <>'))
238
+ yield { type: 'NOTIFIED_READ_MESSAGE' }
239
+ },
240
+ }),
241
+ e2ee: { decryptE2EEMessage: async (m: unknown) => m },
242
+ },
243
+ }
244
+
245
+ const events = await collect(client)
246
+ expect(events.map((e) => e.kind)).toEqual(['event'])
247
+ })
248
+
249
+ it('propagates non-empty malformed buffer errors', async () => {
250
+ const client = new LineClient()
251
+ ;(client as any).client = {
252
+ base: {
253
+ profile: { mid: 'me' },
254
+ createPolling: () => ({
255
+ async *_listenTalkEvents(opts: { onError?: (e: unknown) => void }) {
256
+ opts.onError?.(new Error('Request internal failed: Invalid response buffer <de ad be ef>'))
257
+ yield undefined as never
258
+ },
259
+ }),
260
+ e2ee: { decryptE2EEMessage: async (m: unknown) => m },
261
+ },
262
+ }
263
+
264
+ await expect(collect(client)).rejects.toThrow('Invalid response buffer <de ad be ef>')
265
+ })
266
+ })
267
+
57
268
  describe('login() without credentials', () => {
58
269
  it('throws LineError when no saved credentials exist', async () => {
59
270
  const { LineCredentialManager } = require('./credential-manager')
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
+ import type { Operation as LineOperation } from '@jsr/evex__linejs-types'
5
+
4
6
  import { getConfigDir } from '@/shared/utils/config-dir'
5
7
  import { FileStorage } from '@/vendor/linejs/base/storage/mod.js'
6
8
  import {
@@ -23,12 +25,46 @@ import type {
23
25
  } from './types'
24
26
  import { LineError } from './types'
25
27
 
28
+ export interface LineRawMessage {
29
+ raw: { id: unknown; contentType?: unknown; createdTime?: unknown; toType?: unknown; to?: unknown; from?: unknown }
30
+ to: { id: unknown }
31
+ from: { id: unknown }
32
+ isMyMessage: boolean
33
+ text: string | null
34
+ }
35
+
36
+ export type LineRawEvent = { kind: 'message'; message: LineRawMessage } | { kind: 'event'; op: LineOperation }
37
+
38
+ const MAX_MESSAGE_ID = 9223372036854775807n
39
+
26
40
  function wrapError(error: unknown, code: string): LineError {
27
41
  if (error instanceof LineError) return error
28
42
  const message = error instanceof Error ? error.message : String(error)
29
43
  return new LineError(code, message)
30
44
  }
31
45
 
46
+ function isE2EEUnavailableError(message: string): boolean {
47
+ return /E2EE|e2ee|KeyNotFound|saveE2EE/.test(message)
48
+ }
49
+
50
+ function requiresEncryption(message: string): boolean {
51
+ return /RETRY_ENCRYPT|can not send using plain mode|cannot send using plain mode/i.test(message)
52
+ }
53
+
54
+ // An idle long-poll returning zero bytes is normal, but the vendored Thrift reader
55
+ // throws "Invalid response buffer <>" (empty brackets = empty body). Match only that
56
+ // empty case so the poll loop continues; a non-empty malformed buffer still propagates.
57
+ function isEmptyLongPollError(message: string): boolean {
58
+ return /Invalid response buffer <>/.test(message)
59
+ }
60
+
61
+ // Writes to stderr so partial-result degradation stays visible without
62
+ // corrupting the JSON the CLI prints to stdout.
63
+ function warnDegraded(action: string, error: unknown): void {
64
+ const message = error instanceof Error ? error.message : String(error)
65
+ console.warn(`[line] could not ${action}: ${message}`)
66
+ }
67
+
32
68
  function mapChatType(rawType: unknown): 'user' | 'group' | 'room' | 'square' {
33
69
  if (rawType === 'GROUP' || rawType === 0) return 'group'
34
70
  if (rawType === 'ROOM' || rawType === 1) return 'room'
@@ -209,7 +245,10 @@ export class LineClient {
209
245
  },
210
246
  syncReason: 'INTERNAL',
211
247
  }),
212
- client.fetchJoinedChats().catch(() => []),
248
+ client.fetchJoinedChats().catch((error: unknown) => {
249
+ warnDegraded('fetch joined group/room chats', error)
250
+ return []
251
+ }),
213
252
  ])
214
253
 
215
254
  for (const chat of joinedChats) {
@@ -239,7 +278,9 @@ export class LineClient {
239
278
  for (const c of contacts ?? []) {
240
279
  nameMap.set(c.mid, { name: c.displayName, type: 'user' })
241
280
  }
242
- } catch {}
281
+ } catch (error) {
282
+ warnDegraded('resolve user display names', error)
283
+ }
243
284
  }
244
285
 
245
286
  if (groupMids.length > 0) {
@@ -253,7 +294,9 @@ export class LineClient {
253
294
  memberCount: memberMids ? Object.keys(memberMids).length : undefined,
254
295
  })
255
296
  }
256
- } catch {}
297
+ } catch (error) {
298
+ warnDegraded('resolve group/room names', error)
299
+ }
257
300
  }
258
301
 
259
302
  for (const box of messageBoxes) {
@@ -279,13 +322,16 @@ export class LineClient {
279
322
  const client = this.ensureClient()
280
323
  const count = options?.count ?? 20
281
324
 
325
+ // getPreviousMessagesV2WithRequest pages backward from endMessageId. A
326
+ // messageId:0 sentinel returns nothing; the max int64 id acts as "from the
327
+ // latest message" and works for any chat regardless of message-box position.
282
328
  const serverTime = await client.base.talk.getServerTime()
283
329
  const rawMessages = await client.base.talk.getPreviousMessagesV2WithRequest({
284
330
  request: {
285
331
  messageBoxId: chatId,
286
332
  endMessageId: {
287
333
  deliveredTime: BigInt(serverTime),
288
- messageId: BigInt(0),
334
+ messageId: MAX_MESSAGE_ID,
289
335
  },
290
336
  messagesCount: count,
291
337
  },
@@ -313,10 +359,22 @@ export class LineClient {
313
359
  sent = await client.base.talk.sendMessage({ to: chatId, text, e2ee: true })
314
360
  } catch (e2eeError) {
315
361
  const msg = e2eeError instanceof Error ? e2eeError.message : String(e2eeError)
316
- if (msg.includes('E2EE') || msg.includes('e2ee') || msg.includes('KeyNotFound') || msg.includes('saveE2EE')) {
362
+ if (!isE2EEUnavailableError(msg)) throw e2eeError
363
+
364
+ try {
317
365
  sent = await client.base.talk.sendMessage({ to: chatId, text, e2ee: false })
318
- } else {
319
- throw e2eeError
366
+ } catch (plainError) {
367
+ const plainMsg = plainError instanceof Error ? plainError.message : String(plainError)
368
+ // Some chats force Letter Sealing and reject plain mode. This session
369
+ // authenticated via auth token and has no local E2EE private key, so it
370
+ // cannot encrypt. Surface a clear error and keep the original cause.
371
+ if (requiresEncryption(plainMsg)) {
372
+ throw new LineError(
373
+ 'e2ee_required',
374
+ `This chat requires end-to-end encryption (Letter Sealing), which this auth-token session cannot provide. Original error: ${msg}`,
375
+ )
376
+ }
377
+ throw plainError
320
378
  }
321
379
  }
322
380
 
@@ -331,6 +389,55 @@ export class LineClient {
331
389
  }
332
390
  }
333
391
 
392
+ // Drives the vendor polling generator (talk.sync()) instead of Client.listen().
393
+ // Client.listen() uses a LEGY HTTP/2 push connection (duplex:'half' streaming
394
+ // fetch) that yields zero bytes under Bun and dies immediately, so no events
395
+ // ever arrive (evex-dev/linejs#117). The upstream "fix" (linejs#134, v2.7.0+)
396
+ // only adds an undici+allowH2 path for Node.js and explicitly skips Bun
397
+ // ("Bun" in globalThis -> return false), falling back to Bun's native fetch,
398
+ // which still can't stream duplex:'half' HTTP/2 (oven-sh/bun#30342, #31881).
399
+ // Polling works on every runtime. Messages are normalized like Client.listen().
400
+ async *streamEvents(signal: AbortSignal): AsyncGenerator<LineRawEvent, void, unknown> {
401
+ const client = this.ensureClient()
402
+ const polling = client.base.createPolling()
403
+ const selfMid = client.base.profile?.mid
404
+
405
+ for await (const op of polling._listenTalkEvents({
406
+ signal,
407
+ onError: (error) => {
408
+ const message = error instanceof Error ? error.message : String(error)
409
+ if (isEmptyLongPollError(message)) return
410
+ throw error instanceof Error ? error : new Error(message)
411
+ },
412
+ })) {
413
+ yield { kind: 'event', op }
414
+ if (op.type === 'SEND_MESSAGE' || op.type === 'RECEIVE_MESSAGE') {
415
+ // A single undecryptable message must not kill the stream: the failing
416
+ // op stays in the sync window and would be re-fetched every poll, causing
417
+ // an endless decrypt-fail -> reconnect loop. Fall back to the raw op and
418
+ // surface the message with null text, since its text is unreadable.
419
+ let raw = op.message
420
+ let decrypted = true
421
+ try {
422
+ raw = await client.base.e2ee.decryptE2EEMessage(op.message)
423
+ } catch {
424
+ raw = op.message
425
+ decrypted = false
426
+ }
427
+ yield {
428
+ kind: 'message',
429
+ message: {
430
+ raw,
431
+ to: { id: raw.to },
432
+ from: { id: raw.from },
433
+ isMyMessage: selfMid === raw.from,
434
+ text: decrypted ? (raw.text ?? null) : null,
435
+ },
436
+ }
437
+ }
438
+ }
439
+ }
440
+
334
441
  close(): void {
335
442
  this.client = null
336
443
  }