agent-messenger 2.19.4 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/line/client.d.ts +10 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +156 -11
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/e2ee-storage.d.ts +16 -0
- package/dist/src/platforms/line/e2ee-storage.d.ts.map +1 -0
- package/dist/src/platforms/line/e2ee-storage.js +93 -0
- package/dist/src/platforms/line/e2ee-storage.js.map +1 -0
- package/dist/src/platforms/line/index.d.ts +1 -1
- package/dist/src/platforms/line/index.d.ts.map +1 -1
- package/dist/src/platforms/line/index.js.map +1 -1
- package/dist/src/platforms/line/listener.d.ts.map +1 -1
- package/dist/src/platforms/line/listener.js +3 -2
- package/dist/src/platforms/line/listener.js.map +1 -1
- package/dist/src/platforms/line/types.d.ts +13 -0
- package/dist/src/platforms/line/types.d.ts.map +1 -1
- package/dist/src/platforms/line/types.js +6 -0
- package/dist/src/platforms/line/types.js.map +1 -1
- package/dist/src/platforms/teams/cli.d.ts.map +1 -1
- package/dist/src/platforms/teams/cli.js +2 -1
- package/dist/src/platforms/teams/cli.js.map +1 -1
- package/dist/src/platforms/teams/client.d.ts +4 -1
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +84 -0
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/chat.d.ts +13 -0
- package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/chat.js +111 -0
- package/dist/src/platforms/teams/commands/chat.js.map +1 -0
- package/dist/src/platforms/teams/commands/index.d.ts +1 -0
- package/dist/src/platforms/teams/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/index.js +1 -0
- package/dist/src/platforms/teams/commands/index.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +24 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +8 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/tui/adapters/line-adapter.js +1 -1
- package/dist/src/tui/adapters/line-adapter.js.map +1 -1
- package/dist/src/vendor/linejs/_dist/client/login.d.ts +2 -1
- package/dist/src/vendor/linejs/client/login.js +3 -2
- package/dist/src/vendor/linejs/client/login.test.ts +11 -0
- package/docs/content/docs/cli/line.mdx +13 -11
- 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 +7 -5
- package/skills/agent-line/references/common-patterns.md +12 -3
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +20 -2
- package/skills/agent-teams/references/common-patterns.md +28 -0
- 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 +190 -2
- package/src/platforms/line/client.ts +183 -13
- package/src/platforms/line/e2ee-storage.test.ts +154 -0
- package/src/platforms/line/e2ee-storage.ts +119 -0
- package/src/platforms/line/index.test.ts +10 -0
- package/src/platforms/line/index.ts +1 -0
- package/src/platforms/line/listener.test.ts +32 -0
- package/src/platforms/line/listener.ts +5 -4
- package/src/platforms/line/types.test.ts +17 -0
- package/src/platforms/line/types.ts +13 -0
- package/src/platforms/slack/commands/auth.test.ts +16 -6
- package/src/platforms/slack/token-extractor.test.ts +34 -7
- package/src/platforms/teams/cli.ts +2 -0
- package/src/platforms/teams/client.test.ts +96 -0
- package/src/platforms/teams/client.ts +133 -0
- package/src/platforms/teams/commands/chat.test.ts +100 -0
- package/src/platforms/teams/commands/chat.ts +131 -0
- package/src/platforms/teams/commands/index.ts +1 -0
- package/src/platforms/teams/types.ts +20 -0
- package/src/tui/adapters/line-adapter.ts +1 -1
- package/src/vendor/linejs/_dist/client/login.d.ts +2 -1
- package/src/vendor/linejs/client/login.js +3 -2
- package/src/vendor/linejs/client/login.test.ts +11 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'bun:test'
|
|
4
|
+
|
|
5
|
+
describe('linejs login wrappers', () => {
|
|
6
|
+
it('passes E2EE option through password login', async () => {
|
|
7
|
+
const source = await readFile(new URL('./login.js', import.meta.url), 'utf8')
|
|
8
|
+
|
|
9
|
+
expect(source).toContain('e2ee: opts.e2ee')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -9,14 +9,14 @@ description: Complete reference for the agent-line CLI.
|
|
|
9
9
|
|
|
10
10
|
Before diving in, a few things about LINE's architecture:
|
|
11
11
|
|
|
12
|
-
| Term | Description
|
|
13
|
-
| ------------------------- |
|
|
14
|
-
| **MID** | LINE's unique identifier for users, chats, and rooms. Prefixed with `u` (user), `c` (group chat), or `r` (room).
|
|
15
|
-
| **QR code login** | The primary authentication method. Scan a QR code with the LINE mobile app to authorize the CLI.
|
|
16
|
-
| **ANDROIDSECONDARY** | The default device type. Registers as a secondary Android device so your phone and desktop sessions stay active.
|
|
17
|
-
| **E2EE (Letter Sealing)** | LINE's end-to-end encryption. The CLI attempts E2EE first and falls back to plaintext
|
|
18
|
-
| **Device types** | Controls which device slot the CLI occupies. Secondary devices coexist with your other sessions.
|
|
19
|
-
| **PIN verification** | During login, LINE may display a PIN that you confirm on your phone to authorize the new device.
|
|
12
|
+
| Term | Description |
|
|
13
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| **MID** | LINE's unique identifier for users, chats, and rooms. Prefixed with `u` (user), `c` (group chat), or `r` (room). |
|
|
15
|
+
| **QR code login** | The primary authentication method. Scan a QR code with the LINE mobile app to authorize the CLI. |
|
|
16
|
+
| **ANDROIDSECONDARY** | The default device type. Registers as a secondary Android device so your phone and desktop sessions stay active. |
|
|
17
|
+
| **E2EE (Letter Sealing)** | LINE's end-to-end encryption. The CLI encrypts and decrypts E2EE messages when key material is available, restoring keys saved by a prior QR/email login. It attempts E2EE first and falls back to plaintext otherwise. Chats that require E2EE reject the fallback and fail with `e2ee_required` when no key material is present. |
|
|
18
|
+
| **Device types** | Controls which device slot the CLI occupies. Secondary devices coexist with your other sessions. |
|
|
19
|
+
| **PIN verification** | During login, LINE may display a PIN that you confirm on your phone to authorize the new device. |
|
|
20
20
|
|
|
21
21
|
## Quick Start
|
|
22
22
|
|
|
@@ -192,7 +192,9 @@ Each message includes:
|
|
|
192
192
|
- `message_id` — unique message identifier
|
|
193
193
|
- `chat_id` — which chat the message belongs to
|
|
194
194
|
- `author_id` — sender's MID
|
|
195
|
-
- `
|
|
195
|
+
- `author_name` — sender's display name, resolved from contacts (omitted when it can't be resolved)
|
|
196
|
+
- `text` — message text content (null for non-text or undecryptable messages). Letter Sealing (E2EE) messages are decrypted automatically when key material is available
|
|
197
|
+
- `decryption_error` — present only when an E2EE message couldn't be decrypted; `{ code, message }` where `code` is `missing_e2ee_key` or `decrypt_failed`
|
|
196
198
|
- `content_type` — message type (NONE = text, IMAGE, VIDEO, etc.)
|
|
197
199
|
- `sent_at` — ISO 8601 timestamp
|
|
198
200
|
|
|
@@ -230,7 +232,7 @@ agent-line message list c7a8b9c0d1e2f3a4b5c6d7e8 -n 200
|
|
|
230
232
|
- No message editing or deletion
|
|
231
233
|
- No search across chats
|
|
232
234
|
- Plain text messages only (no photos, videos, or rich content)
|
|
233
|
-
- Sending to chats that **require** E2EE (Letter Sealing)
|
|
235
|
+
- Sending to chats that **require** E2EE (Letter Sealing) needs E2EE key material from a prior QR/email login; without it such sends fail with `e2ee_required`
|
|
234
236
|
- Chat IDs are MID strings, not human-readable — use `chat list` to discover them
|
|
235
237
|
|
|
236
238
|
## Troubleshooting
|
|
@@ -267,7 +269,7 @@ If the QR code doesn't open in your browser:
|
|
|
267
269
|
|
|
268
270
|
### E2EE errors
|
|
269
271
|
|
|
270
|
-
The CLI tries E2EE (Letter Sealing) first and falls back to plaintext when possible, so most messages still send. Chats that **require** E2EE reject the plaintext fallback,
|
|
272
|
+
The CLI tries E2EE (Letter Sealing) first and falls back to plaintext when possible, so most messages still send. When E2EE key material is available — restored from a prior QR/email login — the CLI encrypts and decrypts those messages. Chats that **require** E2EE reject the plaintext fallback, so if no key material is present the send fails with `e2ee_required`. Re-run `agent-line auth login` (QR) to provision E2EE keys.
|
|
271
273
|
|
|
272
274
|
### PIN verification timeout
|
|
273
275
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-messenger",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "Multi-platform messaging CLI for AI agents (Slack, Discord, Teams, Webex, Telegram, Telegram Bot, WhatsApp, LINE, Instagram, KakaoTalk, Channel Talk)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-channeltalk
|
|
3
3
|
description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.20.0
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
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.
|
|
4
|
+
version: 2.20.0
|
|
5
5
|
allowed-tools: Bash(agent-line:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -298,7 +298,9 @@ Each message includes:
|
|
|
298
298
|
- `message_id` - unique message identifier
|
|
299
299
|
- `type` - message type (text, image, sticker, etc.)
|
|
300
300
|
- `author_id` - sender's MID
|
|
301
|
-
- `
|
|
301
|
+
- `author_name` - sender's display name, resolved from contacts (omitted when unresolved)
|
|
302
|
+
- `text` - message text content (E2EE messages are decrypted automatically when key material is available; null when undecryptable)
|
|
303
|
+
- `decryption_error` - present only for E2EE messages that couldn't be decrypted (`code`: `missing_e2ee_key` or `decrypt_failed`)
|
|
302
304
|
- `sent_at` - Unix timestamp (milliseconds)
|
|
303
305
|
|
|
304
306
|
## Output Format
|
|
@@ -428,8 +430,8 @@ See the [LINE SDK documentation](https://agent-messenger.dev/docs/sdk/line) for
|
|
|
428
430
|
## Limitations
|
|
429
431
|
|
|
430
432
|
- No auto-extraction of credentials (requires interactive login via QR code or email/password)
|
|
431
|
-
- E2EE (Letter Sealing)
|
|
432
|
-
- Sending to chats that **require** E2EE (Letter Sealing)
|
|
433
|
+
- E2EE (Letter Sealing) messages are decrypted automatically when key material is available (restored from a prior QR/email login); without keys, content may be unreadable
|
|
434
|
+
- Sending to chats that **require** E2EE (Letter Sealing) needs E2EE key material from a prior QR/email login; without it such sends fail with `e2ee_required`
|
|
433
435
|
- No file upload support yet
|
|
434
436
|
- No sticker or rich message sending (text only)
|
|
435
437
|
- No group creation or management
|
|
@@ -479,7 +481,7 @@ If commands start failing with `not_authenticated` or `invalid_token`:
|
|
|
479
481
|
|
|
480
482
|
### E2EE messages unreadable
|
|
481
483
|
|
|
482
|
-
|
|
484
|
+
The CLI decrypts Letter Sealing (E2EE) messages when E2EE key material is available. Keys are provisioned by a QR or email/password login and reused on later sessions. If a message comes back with `text: null` and a `decryption_error` of `missing_e2ee_key`, the session has no usable key — re-run `agent-line auth login` (QR) to provision E2EE keys.
|
|
483
485
|
|
|
484
486
|
## References
|
|
485
487
|
|
|
@@ -71,12 +71,15 @@ MESSAGES=$(agent-line message list "$CHAT_ID" -n 50)
|
|
|
71
71
|
MSG_COUNT=$(echo "$MESSAGES" | jq 'length')
|
|
72
72
|
echo "Found $MSG_COUNT messages"
|
|
73
73
|
|
|
74
|
-
# Show messages
|
|
75
|
-
|
|
74
|
+
# Show messages by display name; Letter Sealing messages are decrypted when E2EE
|
|
75
|
+
# key material is available, otherwise decryption_error explains why text is null.
|
|
76
|
+
echo "$MESSAGES" | jq -r '.[] | "\(.author_name // .author_id): \(.text // .decryption_error.message // "[non-text]")"'
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
**When to use**: Context gathering, summarizing conversations, catching up on missed messages.
|
|
79
80
|
|
|
81
|
+
**E2EE note**: `message list` decrypts Letter Sealing (E2EE) messages when key material is available — restored from a prior QR/email login. When keys are missing, `text` is `null` and `decryption_error.code` is `missing_e2ee_key`; re-run `agent-line auth login` (QR) to provision keys.
|
|
82
|
+
|
|
80
83
|
## Pattern 4: Monitor for New Messages
|
|
81
84
|
|
|
82
85
|
**Use case**: Watch a chat room and respond to new messages using the SDK
|
|
@@ -130,7 +133,9 @@ listener.on('connected', (info) => {
|
|
|
130
133
|
})
|
|
131
134
|
|
|
132
135
|
listener.on('message', (event) => {
|
|
133
|
-
|
|
136
|
+
// event.decryption_error?: { code: 'missing_e2ee_key' | 'decrypt_failed'; message: string }
|
|
137
|
+
const content = event.text ?? event.decryption_error?.message ?? '[non-text]'
|
|
138
|
+
console.log(`[${event.chat_id}] ${event.author_id}: ${content}`)
|
|
134
139
|
})
|
|
135
140
|
|
|
136
141
|
listener.on('error', (error) => {
|
|
@@ -152,6 +157,10 @@ await listener.start()
|
|
|
152
157
|
|
|
153
158
|
**Features**: Auto-reconnects with exponential backoff, typed events, AbortController-based clean shutdown.
|
|
154
159
|
|
|
160
|
+
**E2EE note**: For LINE Letter Sealing messages that cannot be decrypted in the current session, `text` stays `null` and `decryption_error` explains whether E2EE key material is missing or decryption failed.
|
|
161
|
+
|
|
162
|
+
Message listener payloads include `decryption_error` when encrypted content is present but unavailable. Check `event.decryption_error.code` for `missing_e2ee_key` or `decrypt_failed` before treating `text: null` as a non-text message.
|
|
163
|
+
|
|
155
164
|
## Pattern 5: Get User Profile
|
|
156
165
|
|
|
157
166
|
**Use case**: Retrieve your own LINE profile information
|
|
@@ -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.
|
|
4
|
+
version: 2.20.0
|
|
5
5
|
allowed-tools: Bash(agent-teams:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -207,6 +207,23 @@ agent-teams channel info <team-id> 19:abc123@thread.tacv2
|
|
|
207
207
|
agent-teams channel history <team-id> <channel-id> --limit 100
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
### Chat Commands
|
|
211
|
+
|
|
212
|
+
For personal Microsoft accounts (`@outlook.com` / `@live.com`) that have no teams or channels — only 1:1, group, and self chat threads. Work accounts can use these too for their 1:1 and group chats.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# List 1:1, group, and self chats
|
|
216
|
+
agent-teams chat list
|
|
217
|
+
|
|
218
|
+
# Get chat message history
|
|
219
|
+
agent-teams chat history <chat-id> --limit 100
|
|
220
|
+
|
|
221
|
+
# Send a message to a chat
|
|
222
|
+
agent-teams chat send <chat-id> "Hello"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Chat IDs look like `19:guid1_guid2@unq.gbl.spaces` (1:1) or `19:guid@thread.tacv2` (group). Get them from `chat list`. The `48:notes` chat (`type: self`) is your personal "to me" notes thread.
|
|
226
|
+
|
|
210
227
|
### Team Commands
|
|
211
228
|
|
|
212
229
|
```bash
|
|
@@ -357,7 +374,7 @@ Common errors:
|
|
|
357
374
|
|
|
358
375
|
- `Not authenticated`: No valid token (auto-extraction failed — see Troubleshooting)
|
|
359
376
|
- `Token expired`: Token has expired and auto-refresh failed — see Troubleshooting
|
|
360
|
-
- `No current team set`: Run `team switch <id>` first
|
|
377
|
+
- `No current team set`: Run `team switch <id>` first. Personal accounts have no teams — use `chat list` / `chat history` / `chat send` instead
|
|
361
378
|
- `Message not found`: Invalid message ID
|
|
362
379
|
- `Channel not found`: Invalid channel ID
|
|
363
380
|
- `401 Unauthorized`: Token expired and auto-refresh failed — see Troubleshooting
|
|
@@ -419,6 +436,7 @@ See the [Teams SDK documentation](https://agent-messenger.dev/docs/sdk/teams) fo
|
|
|
419
436
|
- No real-time events / WebSocket connection
|
|
420
437
|
- No voice/video channel support
|
|
421
438
|
- No team management (create/delete channels, roles)
|
|
439
|
+
- Personal accounts: chats only (no teams/channels); use the `chat` commands
|
|
422
440
|
- No meeting support
|
|
423
441
|
- No webhook support
|
|
424
442
|
- Plain text messages only (no adaptive cards in v1)
|
|
@@ -438,6 +438,34 @@ SNAPSHOT=$(teams_cmd agent-teams snapshot)
|
|
|
438
438
|
|
|
439
439
|
**When to use**: Any script that runs for more than a few minutes.
|
|
440
440
|
|
|
441
|
+
## Pattern 12: Personal Account Chats (No Teams)
|
|
442
|
+
|
|
443
|
+
**Use case**: Message from a personal Microsoft account (`@outlook.com` / `@live.com`), which has no teams or channels — only 1:1, group, and self ("to me") chats
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
#!/bin/bash
|
|
447
|
+
|
|
448
|
+
# Personal accounts have no teams, so `team list` is empty and team/channel
|
|
449
|
+
# commands fail with "No current team set". Use the `chat` commands instead.
|
|
450
|
+
|
|
451
|
+
agent-teams auth extract 2>/dev/null || true
|
|
452
|
+
|
|
453
|
+
# List chats (type is oneOnOne, group, or self)
|
|
454
|
+
CHATS=$(agent-teams chat list)
|
|
455
|
+
echo "$CHATS" | jq -r '.[] | " \(.type): \(.id) — \(.topic // .last_message // "")"'
|
|
456
|
+
|
|
457
|
+
# Pick a chat ID (here: the self "to me" notes thread)
|
|
458
|
+
CHAT_ID=$(echo "$CHATS" | jq -r '.[] | select(.type=="self") | .id')
|
|
459
|
+
|
|
460
|
+
# Read history and send a message
|
|
461
|
+
agent-teams chat history "$CHAT_ID" --limit 20
|
|
462
|
+
agent-teams chat send "$CHAT_ID" "Reminder: stand-up at 10am"
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**When to use**: Any workflow on a personal/consumer Teams account. Get chat IDs from `chat list` — they look like `19:guid1_guid2@unq.gbl.spaces` (1:1), `19:guid@thread.tacv2` (group), or `48:notes` (self).
|
|
466
|
+
|
|
467
|
+
**Note**: Work accounts also have 1:1 and group chats and can use these same `chat` commands.
|
|
468
|
+
|
|
441
469
|
## Best Practices
|
|
442
470
|
|
|
443
471
|
### 1. Always Handle Token Expiry
|
|
@@ -56,9 +56,10 @@ describe('LineClient', () => {
|
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
describe('getMessages()', () => {
|
|
59
|
-
function clientWithTalk(talk: Record<string, unknown>): LineClient {
|
|
59
|
+
function clientWithTalk(talk: Record<string, unknown>, e2ee?: Record<string, unknown>): LineClient {
|
|
60
60
|
const client = new LineClient()
|
|
61
|
-
|
|
61
|
+
const { profile, ...talkMethods } = talk
|
|
62
|
+
;(client as any).client = { base: { talk: talkMethods, profile, e2ee: e2ee ?? {} } }
|
|
62
63
|
return client
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -105,6 +106,181 @@ describe('LineClient', () => {
|
|
|
105
106
|
expect(result.map((m) => m.message_id)).toEqual(['30', '20'])
|
|
106
107
|
expect(result.map((m) => m.text)).toEqual(['c', 'b'])
|
|
107
108
|
})
|
|
109
|
+
|
|
110
|
+
it('resolves author MIDs to display names via getContacts', async () => {
|
|
111
|
+
const client = clientWithTalk({
|
|
112
|
+
profile: { mid: 'me' },
|
|
113
|
+
getServerTime: async () => 1700000000000,
|
|
114
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
115
|
+
{ id: '10', from: 'u1', text: 'hi', contentType: 'NONE', createdTime: 1700000001000 },
|
|
116
|
+
{ id: '11', from: 'u2', text: 'yo', contentType: 'NONE', createdTime: 1700000002000 },
|
|
117
|
+
],
|
|
118
|
+
getContacts: async ({ mids }: { mids: string[] }) =>
|
|
119
|
+
mids.map((mid) => ({ mid, displayName: mid === 'u1' ? 'Alice' : 'Bob' })),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const result = await client.getMessages('chat1', { count: 2 })
|
|
123
|
+
expect(result.map((m) => m.author_name)).toEqual(['Alice', 'Bob'])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('batch-resolves unique authors in a single getContacts call', async () => {
|
|
127
|
+
let contactCalls = 0
|
|
128
|
+
const client = clientWithTalk({
|
|
129
|
+
profile: { mid: 'me' },
|
|
130
|
+
getServerTime: async () => 1700000000000,
|
|
131
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
132
|
+
{ id: '10', from: 'u1', text: 'a', contentType: 'NONE', createdTime: 1700000001000 },
|
|
133
|
+
{ id: '11', from: 'u1', text: 'b', contentType: 'NONE', createdTime: 1700000002000 },
|
|
134
|
+
{ id: '12', from: 'u2', text: 'c', contentType: 'NONE', createdTime: 1700000003000 },
|
|
135
|
+
],
|
|
136
|
+
getContacts: async ({ mids }: { mids: string[] }) => {
|
|
137
|
+
contactCalls++
|
|
138
|
+
expect([...mids].sort()).toEqual(['u1', 'u2'])
|
|
139
|
+
return mids.map((mid) => ({ mid, displayName: mid.toUpperCase() }))
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const result = await client.getMessages('chat1', { count: 3 })
|
|
144
|
+
expect(contactCalls).toBe(1)
|
|
145
|
+
expect(result.map((m) => m.author_name)).toEqual(['U1', 'U1', 'U2'])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('resolves the current user via getProfile since getContacts omits self', async () => {
|
|
149
|
+
const client = clientWithTalk({
|
|
150
|
+
profile: { mid: 'me' },
|
|
151
|
+
getServerTime: async () => 1700000000000,
|
|
152
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
153
|
+
{ id: '10', from: 'me', text: 'mine', contentType: 'NONE', createdTime: 1700000001000 },
|
|
154
|
+
],
|
|
155
|
+
getContacts: async () => [],
|
|
156
|
+
getProfile: async () => ({ mid: 'me', displayName: 'My Name' }),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
160
|
+
expect(result[0].author_name).toBe('My Name')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('falls back to bare author_id when name resolution fails', async () => {
|
|
164
|
+
const client = clientWithTalk({
|
|
165
|
+
profile: { mid: 'me' },
|
|
166
|
+
getServerTime: async () => 1700000000000,
|
|
167
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
168
|
+
{ id: '10', from: 'u1', text: 'hi', contentType: 'NONE', createdTime: 1700000001000 },
|
|
169
|
+
],
|
|
170
|
+
getContacts: async () => {
|
|
171
|
+
throw new Error('network down')
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
176
|
+
expect(result[0].author_name).toBeUndefined()
|
|
177
|
+
expect(result[0].author_id).toBe('u1')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('decrypts Letter-Sealing chunk messages via decryptE2EEMessage', async () => {
|
|
181
|
+
const encrypted = {
|
|
182
|
+
id: '40',
|
|
183
|
+
from: 'u1',
|
|
184
|
+
text: null,
|
|
185
|
+
contentType: 'NONE',
|
|
186
|
+
createdTime: 1700000004000,
|
|
187
|
+
chunks: ['a', 'b'],
|
|
188
|
+
metadata: { e2eeMark: '2', e2eeVersion: '2' },
|
|
189
|
+
}
|
|
190
|
+
const client = clientWithTalk(
|
|
191
|
+
{
|
|
192
|
+
getServerTime: async () => 1700000000000,
|
|
193
|
+
getPreviousMessagesV2WithRequest: async () => [encrypted],
|
|
194
|
+
},
|
|
195
|
+
{ decryptE2EEMessage: async (m: { id: string }) => ({ ...m, text: 'decrypted secret' }) },
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
199
|
+
expect(result[0].text).toBe('decrypted secret')
|
|
200
|
+
expect(result[0].decryption_error).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('normalizes metadata-shaped history messages to contentMetadata before decrypting', async () => {
|
|
204
|
+
let received: { contentMetadata?: unknown } | undefined
|
|
205
|
+
const client = clientWithTalk(
|
|
206
|
+
{
|
|
207
|
+
getServerTime: async () => 1700000000000,
|
|
208
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
209
|
+
{
|
|
210
|
+
id: '40',
|
|
211
|
+
from: 'u1',
|
|
212
|
+
text: null,
|
|
213
|
+
contentType: 'NONE',
|
|
214
|
+
createdTime: 1700000004000,
|
|
215
|
+
chunks: ['a', 'b'],
|
|
216
|
+
metadata: { e2eeMark: '2', e2eeVersion: '2' },
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
decryptE2EEMessage: async (m: { contentMetadata?: unknown }) => {
|
|
222
|
+
received = m
|
|
223
|
+
// mirror the vendor decryptor: it reads contentMetadata.e2eeVersion
|
|
224
|
+
const meta = m.contentMetadata as { e2eeVersion?: string }
|
|
225
|
+
return { text: `v${meta.e2eeVersion}` }
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
231
|
+
expect((received?.contentMetadata as { e2eeVersion?: string })?.e2eeVersion).toBe('2')
|
|
232
|
+
expect(result[0].text).toBe('v2')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('surfaces missing_e2ee_key when decryption fails for lack of keys', async () => {
|
|
236
|
+
const client = clientWithTalk(
|
|
237
|
+
{
|
|
238
|
+
getServerTime: async () => 1700000000000,
|
|
239
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
240
|
+
{
|
|
241
|
+
id: '40',
|
|
242
|
+
from: 'u1',
|
|
243
|
+
text: null,
|
|
244
|
+
contentType: 'NONE',
|
|
245
|
+
createdTime: 1700000004000,
|
|
246
|
+
chunks: ['a', 'b'],
|
|
247
|
+
metadata: { e2eeMark: '2', e2eeVersion: '2' },
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
decryptE2EEMessage: async () => {
|
|
253
|
+
throw new Error('NoE2EEKey: E2EE Key has not been saved')
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
259
|
+
expect(result[0].text).toBeNull()
|
|
260
|
+
expect(result[0].decryption_error?.code).toBe('missing_e2ee_key')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('does not decrypt plain messages that already carry text', async () => {
|
|
264
|
+
let decryptCalls = 0
|
|
265
|
+
const client = clientWithTalk(
|
|
266
|
+
{
|
|
267
|
+
getServerTime: async () => 1700000000000,
|
|
268
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
269
|
+
{ id: '50', from: 'u1', text: 'plain hello', contentType: 'NONE', createdTime: 1700000005000 },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
decryptE2EEMessage: async () => {
|
|
274
|
+
decryptCalls++
|
|
275
|
+
return { text: 'should-not-run' }
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
281
|
+
expect(result[0].text).toBe('plain hello')
|
|
282
|
+
expect(decryptCalls).toBe(0)
|
|
283
|
+
})
|
|
108
284
|
})
|
|
109
285
|
|
|
110
286
|
describe('sendMessage()', () => {
|
|
@@ -206,6 +382,18 @@ describe('LineClient', () => {
|
|
|
206
382
|
const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
|
|
207
383
|
expect(msg.message.from.id).toBe('u1')
|
|
208
384
|
expect(msg.message.text).toBeNull()
|
|
385
|
+
expect(msg.message.decryption_error).toEqual({ code: 'decrypt_failed', message: 'E2EE decrypt failed' })
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('marks missing E2EE key failures explicitly', async () => {
|
|
389
|
+
const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me' } }
|
|
390
|
+
const client = clientWithStream([op], async () => {
|
|
391
|
+
throw new Error('NoE2EEKey: E2EE Key has not been saved')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const events = await collect(client)
|
|
395
|
+
const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
|
|
396
|
+
expect(msg.message.decryption_error?.code).toBe('missing_e2ee_key')
|
|
209
397
|
})
|
|
210
398
|
|
|
211
399
|
it('propagates polling errors so the listener can reconnect', async () => {
|