@rikaidev/yomi 0.2.6

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +81 -0
  3. package/PRIVACY.md +39 -0
  4. package/README.md +659 -0
  5. package/dist/auth/credential-store.js +395 -0
  6. package/dist/auth/keychain.js +256 -0
  7. package/dist/cli/login.js +142 -0
  8. package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
  9. package/dist/line/auth/protocol/index.js +3 -0
  10. package/dist/line/auth/protocol/login-metadata.js +159 -0
  11. package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
  12. package/dist/line/auth/pwless/index.js +163 -0
  13. package/dist/line/auth/pwless/steps.js +213 -0
  14. package/dist/line/client/auth-service/client.js +240 -0
  15. package/dist/line/client/e2ee-service/client.js +69 -0
  16. package/dist/line/client/e2ee-service/requests.js +92 -0
  17. package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
  18. package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
  19. package/dist/line/client/index.js +213 -0
  20. package/dist/line/client/obs-media-client.js +140 -0
  21. package/dist/line/client/parsers.js +69 -0
  22. package/dist/line/client/shop-service/client.js +63 -0
  23. package/dist/line/client/sticker-meta.js +46 -0
  24. package/dist/line/client/sync-service/client.js +317 -0
  25. package/dist/line/client/talk-service/chat-query.js +122 -0
  26. package/dist/line/client/talk-service/command-client.js +242 -0
  27. package/dist/line/client/talk-service/contact-query.js +77 -0
  28. package/dist/line/client/talk-service/message-box-query.js +137 -0
  29. package/dist/line/client/talk-service/message-id-query.js +241 -0
  30. package/dist/line/client/talk-service/query-client.js +219 -0
  31. package/dist/line/client/talk-service/requests.js +383 -0
  32. package/dist/line/client/transport.js +138 -0
  33. package/dist/line/core/auth-session-logout.js +55 -0
  34. package/dist/line/core/auth-session-runtime.js +231 -0
  35. package/dist/line/core/auth-session-service.js +85 -0
  36. package/dist/line/core/chat-runtime-service.js +343 -0
  37. package/dist/line/core/client-runtime.js +17 -0
  38. package/dist/line/core/config.js +67 -0
  39. package/dist/line/core/constants.js +56 -0
  40. package/dist/line/core/contact-query-service.js +20 -0
  41. package/dist/line/core/directory-runtime-service.js +83 -0
  42. package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
  43. package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
  44. package/dist/line/core/e2ee/index.js +6 -0
  45. package/dist/line/core/e2ee/keys/decrypt.js +110 -0
  46. package/dist/line/core/e2ee/keys/encrypt.js +117 -0
  47. package/dist/line/core/e2ee/keys/group-key.js +260 -0
  48. package/dist/line/core/e2ee/keys/key-manager.js +231 -0
  49. package/dist/line/core/e2ee/keys/key-payload.js +130 -0
  50. package/dist/line/core/e2ee/keys/key-types.js +1 -0
  51. package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
  52. package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
  53. package/dist/line/core/e2ee/keys/peer-key.js +191 -0
  54. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
  55. package/dist/line/core/e2ee/types.js +4 -0
  56. package/dist/line/core/group-query-service.js +20 -0
  57. package/dist/line/core/media-decrypt.js +78 -0
  58. package/dist/line/core/media-encrypt.js +97 -0
  59. package/dist/line/core/mention.js +89 -0
  60. package/dist/line/core/message-command-service.js +454 -0
  61. package/dist/line/core/message-query-service.js +161 -0
  62. package/dist/line/core/service.js +134 -0
  63. package/dist/line/core/session-state/index.js +243 -0
  64. package/dist/line/core/session-state/persist.js +192 -0
  65. package/dist/line/core/store.js +8 -0
  66. package/dist/line/core/thrift/binary/binary-reader.js +151 -0
  67. package/dist/line/core/thrift/binary/binary-writer.js +157 -0
  68. package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
  69. package/dist/line/core/thrift/compact/compact-reader.js +180 -0
  70. package/dist/line/core/thrift/compact/compact-writer.js +161 -0
  71. package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
  72. package/dist/line/core/thrift/fields/builders.js +124 -0
  73. package/dist/line/core/thrift/index.js +12 -0
  74. package/dist/line/core/thrift/message/decoder.js +55 -0
  75. package/dist/line/core/thrift/message/encoder.js +25 -0
  76. package/dist/line/core/thrift/primitives/varint.js +64 -0
  77. package/dist/line/core/thrift/struct/reader.js +172 -0
  78. package/dist/line/core/thrift/struct/writer.js +189 -0
  79. package/dist/line/core/thrift/types.js +23 -0
  80. package/dist/line/core/video-thumbnail.js +104 -0
  81. package/dist/mcp/handlers/contacts.js +166 -0
  82. package/dist/mcp/handlers/conversations.js +193 -0
  83. package/dist/mcp/handlers/groups.js +123 -0
  84. package/dist/mcp/handlers/index.js +15 -0
  85. package/dist/mcp/handlers/login.js +473 -0
  86. package/dist/mcp/handlers/media.js +69 -0
  87. package/dist/mcp/handlers/message-actions.js +62 -0
  88. package/dist/mcp/handlers/scope.js +120 -0
  89. package/dist/mcp/handlers/search.js +196 -0
  90. package/dist/mcp/handlers/send.js +523 -0
  91. package/dist/mcp/handlers/shared.js +84 -0
  92. package/dist/mcp/handlers/stickers.js +87 -0
  93. package/dist/mcp/media.js +228 -0
  94. package/dist/mcp/names.js +133 -0
  95. package/dist/mcp/phi-guard.js +105 -0
  96. package/dist/mcp/policy.js +34 -0
  97. package/dist/mcp/server.js +301 -0
  98. package/dist/mcp/tools.js +761 -0
  99. package/dist/mcp/ui/capability.js +26 -0
  100. package/dist/mcp/ui/login-app-protocol.js +118 -0
  101. package/dist/mcp/ui/login-app-styles.js +142 -0
  102. package/dist/mcp/ui/login-app-view.js +361 -0
  103. package/dist/mcp/ui/login-app.js +72 -0
  104. package/dist/mcp/ui/login-structured-content.js +1 -0
  105. package/dist/mcp/ui/resource.js +35 -0
  106. package/dist/mcp/ui/tools-with-ui.js +40 -0
  107. package/dist/search/bigram.js +86 -0
  108. package/dist/search/capture.js +140 -0
  109. package/dist/search/collector.js +176 -0
  110. package/dist/search/default-embedder.js +101 -0
  111. package/dist/search/embedder.js +13 -0
  112. package/dist/search/scope.js +142 -0
  113. package/dist/search/sqlite.js +141 -0
  114. package/dist/search/store.js +469 -0
  115. package/dist/util/data-dir.js +55 -0
  116. package/dist/util/log.js +86 -0
  117. package/dist/version.js +7 -0
  118. package/package.json +63 -0
  119. package/run.mjs +71 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * LINE auth/session restore and token refresh runtime.
3
+ */
4
+ import { createCliLogger } from '../../util/log.js';
5
+ import { recoverBootstrapKeys } from './e2ee/recovery/bootstrap-keys.js';
6
+ import { STATE } from './service.js';
7
+ const TERMINAL_ERROR_REGEX = /LOGGED_OUT|DIVESTED/i;
8
+ const AUTH_ERROR_REGEX = /expired|unauthorized|authentication|invalid.*token/i;
9
+ /**
10
+ * Resolve the active LINE logger for one runtime.
11
+ *
12
+ * @param service - LineProtocolService instance
13
+ * @returns Scope-bound CLI logger
14
+ */
15
+ function getLineLog(service) {
16
+ if (service?.startupFlowLogger) {
17
+ return service.startupFlowLogger;
18
+ }
19
+ if (service?.logger?.info) {
20
+ return service.logger;
21
+ }
22
+ return createCliLogger('LINE');
23
+ }
24
+ /**
25
+ * Restore persisted E2EE self keys or recover them from bootstrap material.
26
+ *
27
+ * @param service - LineProtocolService instance.
28
+ * @param lineLog - Logger instance.
29
+ */
30
+ async function restoreE2EEState(service, lineLog) {
31
+ const savedE2EEKeys = await service.credentialStore.get('line_e2ee_keys');
32
+ if (!savedE2EEKeys) {
33
+ await restoreBootstrapKeys(service, lineLog);
34
+ return;
35
+ }
36
+ try {
37
+ const parsedKeys = JSON.parse(savedE2EEKeys);
38
+ if (!Array.isArray(parsedKeys) || parsedKeys.length === 0) {
39
+ return;
40
+ }
41
+ service.e2eeManager.importKeys(parsedKeys);
42
+ const savedMid = await service.credentialStore.get('line_mid');
43
+ if (savedMid) {
44
+ service.e2eeManager.bindSelfKeysToMid(savedMid);
45
+ }
46
+ service.e2eeWarning = false;
47
+ service.emit('e2eeWarning', { active: false, reason: null });
48
+ lineLog.info('e2ee.restore.complete', { key_count: parsedKeys.length });
49
+ }
50
+ catch (error) {
51
+ lineLog.warn('e2ee.restore.failed', { error: error?.message });
52
+ }
53
+ }
54
+ /**
55
+ * Attempt E2EE restore from bootstrap material.
56
+ *
57
+ * @param service - LineProtocolService instance.
58
+ * @param lineLog - Logger instance.
59
+ */
60
+ async function restoreBootstrapKeys(service, lineLog) {
61
+ const bootstrapKeys = await recoverBootstrapKeys(service);
62
+ if (bootstrapKeys?.length) {
63
+ service.e2eeWarning = false;
64
+ service.emit('e2eeWarning', {
65
+ active: false,
66
+ reason: null,
67
+ recoveredFromBootstrap: true,
68
+ });
69
+ lineLog.info('e2ee.restore.bootstrap_complete', {
70
+ key_count: bootstrapKeys.length,
71
+ });
72
+ return;
73
+ }
74
+ service.e2eeWarning = true;
75
+ service.emit('e2eeWarning', { active: true, reason: 'missing_keys' });
76
+ lineLog.warn('e2ee.restore.missing_keys', {
77
+ action: 'skip_undecrypted_messages_until_relogin',
78
+ });
79
+ }
80
+ /**
81
+ * Validate the restored session profile and decide next auth behavior.
82
+ *
83
+ * @param service - LineProtocolService instance.
84
+ * @param lineLog - Logger instance.
85
+ * @returns True when the session is considered restored.
86
+ */
87
+ async function validateRestoredSession(service, lineLog) {
88
+ try {
89
+ const profile = await service.client.getProfile();
90
+ if (profile) {
91
+ service.profile = profile;
92
+ lineLog.info('session.restore.validated', {
93
+ profile: profile.displayName || profile.mid,
94
+ });
95
+ }
96
+ service.loginRequired = false;
97
+ service.loginReason = null;
98
+ service.setState(STATE.CONNECTED);
99
+ return true;
100
+ }
101
+ catch (validationErr) {
102
+ return handleSessionValidationError(service, lineLog, validationErr);
103
+ }
104
+ }
105
+ /**
106
+ * Handle auth/network errors encountered during restore validation.
107
+ *
108
+ * @param service - LineProtocolService instance.
109
+ * @param lineLog - Logger instance.
110
+ * @param validationErr - Validation error.
111
+ * @returns True when the session remains usable.
112
+ */
113
+ async function handleSessionValidationError(service, lineLog, validationErr) {
114
+ const msg = validationErr?.message || '';
115
+ lineLog.warn('session.restore.validation_failed', { error: msg });
116
+ if (TERMINAL_ERROR_REGEX.test(msg)) {
117
+ // Never destroy credentials on a transient/error-path signal — a
118
+ // regex misfire or one bad response would otherwise cost the operator
119
+ // a phone-PIN re-login for nothing. The stale token stays on disk;
120
+ // a later successful login overwrites it via persistLoginCredentials.
121
+ // clearAuth() remains reserved for the explicit, user-initiated
122
+ // invalidateSession() path (see auth-session-service.ts).
123
+ lineLog.warn('session.restore.revoked', {
124
+ action: 'require_relogin_without_clearing_credentials',
125
+ });
126
+ service.loginRequired = true;
127
+ service.loginReason = 'revoked';
128
+ service.setState(STATE.DISCONNECTED);
129
+ service.emit('line:loginRequired');
130
+ return false;
131
+ }
132
+ if (AUTH_ERROR_REGEX.test(msg)) {
133
+ lineLog.info('session.restore.refresh_attempt');
134
+ const refreshed = await tryRefreshToken(service);
135
+ if (refreshed) {
136
+ service.loginRequired = false;
137
+ service.loginReason = null;
138
+ service.setState(STATE.CONNECTED);
139
+ return true;
140
+ }
141
+ // AUTH_ERROR_REGEX matched (expired/unauthorized/invalid token) and the
142
+ // silent refresh could not save it. Nothing revoked this device — the
143
+ // token just aged out, so don't tell the user someone logged in elsewhere.
144
+ service.loginRequired = true;
145
+ service.loginReason = 'expired';
146
+ service.setState(STATE.DISCONNECTED);
147
+ service.emit('line:loginRequired');
148
+ return false;
149
+ }
150
+ lineLog.warn('session.restore.trusted_optimistically');
151
+ service.loginRequired = false;
152
+ service.loginReason = null;
153
+ service.setState(STATE.CONNECTED);
154
+ return true;
155
+ }
156
+ /**
157
+ * Attempt to refresh the LINE auth token using the stored refresh token.
158
+ * Updates credentials in the credential store on success.
159
+ *
160
+ * @param service - LineProtocolService instance
161
+ * @returns True if the token was refreshed successfully, false otherwise.
162
+ */
163
+ export async function tryRefreshToken(service) {
164
+ const lineLog = getLineLog(service);
165
+ try {
166
+ const refreshToken = service.sessionState.refreshToken ||
167
+ (await service.credentialStore.get('line_refresh_token'));
168
+ if (!refreshToken || !service.client) {
169
+ lineLog.info('auth.refresh.skip', {
170
+ reason: 'missing_refresh_token_or_client',
171
+ });
172
+ return false;
173
+ }
174
+ lineLog.info('auth.refresh.start');
175
+ const result = await service.client.refreshAuthToken(refreshToken);
176
+ await service.sessionState.applyRefreshResult(result);
177
+ try {
178
+ await service.client.reportRefreshedAccessToken?.(result.authToken);
179
+ }
180
+ catch (reportErr) {
181
+ lineLog.warn('auth.refresh.report_failed', { error: reportErr?.message });
182
+ }
183
+ lineLog.info('auth.refresh.complete', {
184
+ next_refresh_hours: Math.round((result.durationUntilRefreshInSec || 0) / 3600),
185
+ });
186
+ service.emit('tokenRotated', result.authToken);
187
+ return true;
188
+ }
189
+ catch (err) {
190
+ lineLog.error('auth.refresh.failed', { error: err?.message });
191
+ return false;
192
+ }
193
+ }
194
+ /**
195
+ * Restore a LINE session from saved credentials.
196
+ * Validates the token with getProfile(), falls back to token refresh on auth error,
197
+ * and emits 'line:loginRequired' if both fail.
198
+ *
199
+ * @param service - LineProtocolService instance
200
+ * @returns True if session was restored successfully, false if re-login is needed.
201
+ */
202
+ export async function resumeSession(service) {
203
+ const lineLog = getLineLog(service);
204
+ try {
205
+ await service.sessionState.loadFromStore();
206
+ service.recentFetchState = await service.sessionState.loadRecentFetchState();
207
+ const token = service.sessionState.authToken;
208
+ if (!token) {
209
+ lineLog.info('session.restore.skip', { reason: 'no_saved_auth_token' });
210
+ return false;
211
+ }
212
+ lineLog.info('session.restore.start');
213
+ const { LineClient } = await import('../client/index.js');
214
+ service.client = new LineClient(token, {
215
+ logger: service.logger,
216
+ startupFlowLogger: service.startupFlowLogger,
217
+ });
218
+ // `service.client` is its own EventEmitter (see sync-service/client.ts's
219
+ // `runtime.emit('error', ...)` in the poll loop) — a zero-listener 'error'
220
+ // emit there throws uncaught and kills the process just like on `service`
221
+ // itself. Attach here, at construction, for the client's lifetime.
222
+ service.client.on('error', (error) => lineLog.warn('client.error', { error: error?.message ?? String(error) }));
223
+ service.sessionState.bindClient(service.client);
224
+ await restoreE2EEState(service, lineLog);
225
+ return validateRestoredSession(service, lineLog);
226
+ }
227
+ catch (outerErr) {
228
+ lineLog.error('session.restore.failed', { error: outerErr?.message });
229
+ return false;
230
+ }
231
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * LINE core auth/session capability.
3
+ */
4
+ import { performPwlessLogin } from '../auth/protocol/index.js';
5
+ import { performLogout } from './auth-session-logout.js';
6
+ import { resumeSession as resumeSessionImpl, tryRefreshToken as tryRefreshTokenImpl, } from './auth-session-runtime.js';
7
+ /**
8
+ * Create the auth/session capability bound to one LINE protocol service.
9
+ *
10
+ * @param service - Mutable LINE protocol service runtime.
11
+ * @param states - Service state constants.
12
+ * @returns Auth/session methods.
13
+ */
14
+ export function createAuthSessionService(service, states) {
15
+ return {
16
+ /**
17
+ * Start the passwordless login process.
18
+ *
19
+ * @param phone - E.164 phone number.
20
+ * @param region - Region code.
21
+ * @returns Promise resolving to the login result.
22
+ */
23
+ async startPwlessLogin(phone, region) {
24
+ return performPwlessLogin(service, phone, region, states.LOGGING_IN, states.CONNECTED, states.ERROR);
25
+ },
26
+ /**
27
+ * Log out from LINE, clearing the local session regardless of remote result.
28
+ *
29
+ * @returns Promise resolving when logout completes.
30
+ */
31
+ async logout() {
32
+ return performLogout(service, states.DISCONNECTED);
33
+ },
34
+ /**
35
+ * Mark the active session as invalid after LINE rejects a previously saved token.
36
+ *
37
+ * @param reason - Operator/debug reason for invalidation.
38
+ */
39
+ async invalidateSession(reason = 'line_auth_invalidated') {
40
+ try {
41
+ service.client?.stopPolling?.();
42
+ }
43
+ catch {
44
+ // Polling may already be stopped.
45
+ }
46
+ const savedPhone = await service.credentialStore?.get?.('line_phone');
47
+ const savedRegion = await service.credentialStore?.get?.('line_region');
48
+ await service.sessionState.clearAuth();
49
+ if (savedPhone) {
50
+ await service.credentialStore?.set?.('line_phone', savedPhone);
51
+ }
52
+ if (savedRegion) {
53
+ await service.credentialStore?.set?.('line_region', savedRegion);
54
+ }
55
+ service.client = null;
56
+ service.profile = null;
57
+ service.e2eeWarning = false;
58
+ service.loginRequired = true;
59
+ // Only reached when LINE rejects a token it previously accepted, which
60
+ // is a revocation regardless of the operator-supplied debug reason.
61
+ service.loginReason = 'revoked';
62
+ service.nameCache.clear();
63
+ service.chatCache.clear();
64
+ service.setState(states.DISCONNECTED);
65
+ service.emit('line:loginRequired', { reason });
66
+ service.emit('error', new Error(reason));
67
+ },
68
+ /**
69
+ * Attempt token refresh via stored refresh token.
70
+ *
71
+ * @returns True when refresh succeeded.
72
+ */
73
+ async tryRefreshToken() {
74
+ return tryRefreshTokenImpl(service);
75
+ },
76
+ /**
77
+ * Restore a previously persisted LINE session.
78
+ *
79
+ * @returns True when the session was restored.
80
+ */
81
+ async resumeSession() {
82
+ return resumeSessionImpl(service);
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * LINE core chat/runtime capability — Yomi read-only + explicit-send subset.
3
+ *
4
+ * Yomi is primarily a pure query server: it exposes message read/download
5
+ * methods, plus one explicit, always-E2EE `sendMessage` write path used only
6
+ * by the `send_message` MCP tool. Capabilities this module intentionally
7
+ * does NOT carry over are: startPolling (continuous read-state mutation),
8
+ * scanChats / fetchRecentMessages (bulk backfill scanning),
9
+ * recoverE2EEContext, and createBackfillAdapter (pipeline-facing, depends
10
+ * on a backfill-adapter module that was not extracted). See
11
+ * yomi/README.md for the full extraction rationale.
12
+ */
13
+ import { createMessageCommandService } from './message-command-service.js';
14
+ import { createMessageQueryService } from './message-query-service.js';
15
+ /**
16
+ * Create the chat/runtime capability bound to one LINE protocol service.
17
+ *
18
+ * @param service - Mutable LINE protocol service runtime.
19
+ * @returns Chat/runtime methods.
20
+ */
21
+ export function createChatRuntimeService(service) {
22
+ return {
23
+ /**
24
+ * Send one plain-text LINE message, always E2EE-encrypted through the
25
+ * explicit message-command boundary. There is no plaintext-send path:
26
+ * the caller-facing `send_message` tool only ever reaches this method,
27
+ * and this method only ever asks for `{ e2ee: true }`. If the E2EE key
28
+ * material cannot be resolved (peer negotiation or group key fetch
29
+ * fails), the underlying encrypt call throws and no request reaches
30
+ * LINE — never a silent plaintext fallback.
31
+ *
32
+ * @param to - Recipient chat MID (1:1 `u...` or group/room `c.../r...`).
33
+ * @param text - Plain-text message body.
34
+ * @param contentMetadata - Optional extra contentMetadata to merge in
35
+ * alongside the E2EE markers — e.g. an outbound `MENTION` payload built
36
+ * via `../mention.ts`. Never part of the encrypted chunks; see the
37
+ * JSDoc on message-command-service.ts's `sendMessage` E2EE branch.
38
+ * @returns LINE sendMessage result (includes the sent message id).
39
+ */
40
+ async sendMessage(to, text, contentMetadata, reply) {
41
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendMessage(to, {
42
+ e2ee: true,
43
+ text,
44
+ contentMetadata,
45
+ relatedMessageId: reply?.relatedMessageId ?? null,
46
+ messageRelationType: reply?.messageRelationType ?? null,
47
+ relatedMessageServiceCode: reply?.relatedMessageServiceCode ?? null,
48
+ });
49
+ },
50
+ /**
51
+ * Send one 1:1 E2EE image, always through the explicit
52
+ * upload-then-send message-command boundary. There is no plaintext or
53
+ * unencrypted-upload path: if the E2EE key material cannot be
54
+ * resolved, or the OBS upload is rejected, the underlying call throws
55
+ * and no message reaches LINE.
56
+ *
57
+ * @param to - Recipient chat MID (1:1 `u...`).
58
+ * @param imageBytes - Raw (plaintext) image bytes to send.
59
+ * @param fileName - Original filename, used to derive the extension.
60
+ * @returns `{ sent, messageId, oid }` describing the delivered image.
61
+ */
62
+ async sendImage(to, imageBytes, fileName) {
63
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendImage(to, imageBytes, fileName);
64
+ },
65
+ /**
66
+ * Send one E2EE file attachment through the shared media-send pipeline.
67
+ *
68
+ * @param to - Recipient chat MID.
69
+ * @param fileBytes - Raw (plaintext) file bytes to send.
70
+ * @param fileName - Original filename (sealed E2EE for the recipient).
71
+ * @returns `{ sent, messageId, oid }` describing the delivered file.
72
+ */
73
+ async sendFile(to, fileBytes, fileName) {
74
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendFile(to, fileBytes, fileName);
75
+ },
76
+ /**
77
+ * Send one E2EE audio message through the shared media-send pipeline.
78
+ *
79
+ * @param to - Recipient chat MID.
80
+ * @param audioBytes - Raw (plaintext) audio bytes.
81
+ * @param fileName - Original filename (for the OBS upload name).
82
+ * @param durationMs - Audio duration in ms, when known.
83
+ * @returns `{ sent, messageId, oid }` describing the delivered audio.
84
+ */
85
+ async sendAudio(to, audioBytes, fileName, durationMs) {
86
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendAudio(to, audioBytes, fileName, durationMs);
87
+ },
88
+ /**
89
+ * Send one E2EE video message through the shared media-send pipeline.
90
+ *
91
+ * @param to - Recipient chat MID.
92
+ * @param videoBytes - Raw (plaintext) video bytes.
93
+ * @param fileName - Original filename (for the OBS upload name).
94
+ * @param durationMs - Video duration in ms, when known.
95
+ * @returns `{ sent, messageId, oid }` describing the delivered video.
96
+ */
97
+ async sendVideo(to, videoBytes, fileName, durationMs) {
98
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendVideo(to, videoBytes, fileName, durationMs);
99
+ },
100
+ /**
101
+ * Share a LINE contact card (contentType CONTACT) — not media.
102
+ *
103
+ * @param to - Recipient chat MID.
104
+ * @param contactMid - MID of the person whose card is shared.
105
+ * @param displayName - Display name shown on the card.
106
+ * @returns `{ sent, messageId }` describing the delivered card.
107
+ */
108
+ async sendContact(to, contactMid, displayName) {
109
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendContact(to, contactMid, displayName);
110
+ },
111
+ /**
112
+ * Send a LINE sticker (contentType STICKER) — not media.
113
+ *
114
+ * @param to - Recipient chat MID.
115
+ * @param stickerId - Sticker id (STKID).
116
+ * @param packageId - Sticker package id (STKPKGID).
117
+ * @param version - Sticker version (STKVER).
118
+ * @returns `{ sent, messageId }` describing the delivered sticker.
119
+ */
120
+ async sendSticker(to, stickerId, packageId, version) {
121
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendSticker(to, stickerId, packageId, version);
122
+ },
123
+ /**
124
+ * Send a location message (contentType LOCATION) — not media, not E2EE.
125
+ *
126
+ * @param to - Recipient chat MID.
127
+ * @param latitude - Latitude.
128
+ * @param longitude - Longitude.
129
+ * @param title - Optional place name.
130
+ * @param address - Optional address.
131
+ * @returns `{ sent, messageId }` describing the delivered location.
132
+ */
133
+ async sendLocation(to, latitude, longitude, title, address) {
134
+ return createMessageCommandService(() => service.client, service.e2eeManager).sendLocation(to, latitude, longitude, title, address);
135
+ },
136
+ /**
137
+ * Fetch recent LINE messages through the explicit message-query boundary.
138
+ *
139
+ * @param chatId - LINE chat MID.
140
+ * @param count - Maximum number of messages.
141
+ * @returns Recent LINE messages.
142
+ */
143
+ async getRecentMessages(chatId, count = 50) {
144
+ return createMessageQueryService(() => service.client, service.e2eeManager).getRecentMessages(chatId, count);
145
+ },
146
+ /**
147
+ * Fetch one page of LINE messages older than a cursor.
148
+ *
149
+ * @param chatId - LINE chat MID.
150
+ * @param count - Maximum number of messages.
151
+ * @param before - Cursor identifying the oldest already-seen message.
152
+ * @returns Previous LINE messages, older than the cursor.
153
+ */
154
+ async getPreviousMessages(chatId, count = 50, before = {}) {
155
+ return createMessageQueryService(() => service.client, service.e2eeManager).getPreviousMessages(chatId, count, before);
156
+ },
157
+ /**
158
+ * Download the original LINE content bytes for one message.
159
+ *
160
+ * @param messageId - LINE message identifier.
161
+ * @param requestId - Optional request identifier.
162
+ * @returns Original message content bytes.
163
+ */
164
+ async downloadMessageContent(messageId, requestId) {
165
+ return createMessageQueryService(() => service.client, service.e2eeManager).downloadMessageContent(messageId, requestId);
166
+ },
167
+ /**
168
+ * Download the LINE preview bytes for one message.
169
+ *
170
+ * @param messageId - LINE message identifier.
171
+ * @param requestId - Optional request identifier.
172
+ * @returns Preview content bytes.
173
+ */
174
+ async downloadMessageContentPreview(messageId, requestId) {
175
+ return createMessageQueryService(() => service.client, service.e2eeManager).downloadMessageContentPreview(messageId, requestId);
176
+ },
177
+ /**
178
+ * Mark a LINE conversation read up to a message via the explicit
179
+ * sendChatChecked boundary. When messageId is omitted, resolves the most
180
+ * recent message id in the chat. NEVER called by background capture/sync —
181
+ * only by the send_message/send_image auto-read and the explicit mark_read
182
+ * tool. Honest no-op (marked:false) when there is no message to mark.
183
+ *
184
+ * @param chatId - LINE chat MID.
185
+ * @param messageId - Optional message id to mark read up to.
186
+ * @returns Result describing whether a read receipt was sent.
187
+ */
188
+ async markChatRead(chatId, messageId) {
189
+ let lastMessageId = messageId ?? null;
190
+ if (!lastMessageId) {
191
+ const recent = await service.getRecentMessages(chatId, 1);
192
+ lastMessageId = recent?.[0]?.id ? String(recent[0].id) : null;
193
+ }
194
+ if (!lastMessageId) {
195
+ return { marked: false, reason: 'no message to mark read' };
196
+ }
197
+ await service.client.sendChatChecked(chatId, lastMessageId, 0);
198
+ return { marked: true, chatId, lastMessageId };
199
+ },
200
+ /**
201
+ * Rename a group/chat (TalkService updateChat). Side effect visible to all
202
+ * members.
203
+ *
204
+ * @param chatId - Group/chat MID.
205
+ * @param name - New name.
206
+ * @returns `{ renamed, chatId, name }`.
207
+ */
208
+ async renameGroup(chatId, name) {
209
+ await service.client.updateChatName(chatId, name);
210
+ return { renamed: true, chatId, name };
211
+ },
212
+ /**
213
+ * Invite members into a group/chat (TalkService inviteIntoChat).
214
+ *
215
+ * @param chatId - Group/chat MID.
216
+ * @param mids - Member MIDs to invite.
217
+ * @returns `{ invited, chatId, mids }`.
218
+ */
219
+ async inviteToGroup(chatId, mids) {
220
+ await service.client.inviteIntoChat(chatId, mids);
221
+ return { invited: true, chatId, mids };
222
+ },
223
+ /**
224
+ * Remove (kick) members from a group/chat (TalkService
225
+ * deleteOtherFromChat).
226
+ *
227
+ * @param chatId - Group/chat MID.
228
+ * @param mids - Member MIDs to remove.
229
+ * @returns `{ removed, chatId, mids }`.
230
+ */
231
+ async kickFromGroup(chatId, mids) {
232
+ await service.client.deleteOtherFromChat(chatId, mids);
233
+ return { removed: true, chatId, mids };
234
+ },
235
+ /**
236
+ * Leave a group/chat — the authenticated account itself exits (TalkService
237
+ * deleteSelfFromChat).
238
+ *
239
+ * @param chatId - Group/chat MID to leave.
240
+ * @returns `{ left, chatId }`.
241
+ */
242
+ async leaveGroup(chatId) {
243
+ await service.client.deleteSelfFromChat(chatId);
244
+ return { left: true, chatId };
245
+ },
246
+ /**
247
+ * Create a new group/room with an initial member set (TalkService
248
+ * createChat). Does not mint any E2EE group key.
249
+ *
250
+ * @param name - New chat name.
251
+ * @param mids - Initial member MIDs.
252
+ * @param chatType - LINE chat type (0 = group, 1 = room). Default 1.
253
+ * @returns `{ created, chatId, name, chat }`.
254
+ */
255
+ async createGroup(name, mids, chatType = 1) {
256
+ const chat = await service.client.createChat(name, mids, chatType);
257
+ // The created Chat struct carries its MID at thrift field 2 (see
258
+ // mapChatStruct in chat-query.ts); field 6 is the name.
259
+ return {
260
+ created: true,
261
+ chatId: chat?.[2] ?? chat?.chatMid ?? null,
262
+ name,
263
+ chat,
264
+ };
265
+ },
266
+ /**
267
+ * Add a predefined reaction to a message (TalkService react).
268
+ *
269
+ * @param messageId - Target message id.
270
+ * @param reactionType - Predefined reaction type (2=LIKE, 3=LOVE, 4=LAUGH,
271
+ * 5=SURPRISE, 6=SAD, 7=ANGRY). Default 2.
272
+ * @returns `{ reacted, messageId, reactionType }`.
273
+ */
274
+ async reactToMessage(messageId, reactionType = 2) {
275
+ await service.client.react(messageId, reactionType);
276
+ return { reacted: true, messageId, reactionType };
277
+ },
278
+ /**
279
+ * Remove this account's reaction from a message (TalkService
280
+ * cancelReaction).
281
+ *
282
+ * @param messageId - Target message id.
283
+ * @returns `{ cancelled, messageId }`.
284
+ */
285
+ async cancelReaction(messageId) {
286
+ await service.client.cancelReaction(messageId);
287
+ return { cancelled: true, messageId };
288
+ },
289
+ /**
290
+ * Unsend (retract) one of this account's own messages for everyone
291
+ * (TalkService unsendMessage). LINE only permits unsending own messages.
292
+ *
293
+ * @param messageId - Message id to unsend.
294
+ * @returns `{ unsent, messageId }`.
295
+ */
296
+ async unsendMessage(messageId) {
297
+ await service.client.unsendMessage(messageId);
298
+ return { unsent: true, messageId };
299
+ },
300
+ /**
301
+ * Add a friend by MID (TalkService findAndAddContactsByMid).
302
+ *
303
+ * @param mid - MID of the person to add.
304
+ * @param reference - Optional JSON reference breadcrumb.
305
+ * @returns `{ added, mid, contact }`.
306
+ */
307
+ async addFriend(mid, reference) {
308
+ const contact = await service.client.findAndAddContactByMid(mid, reference);
309
+ return { added: true, mid, contact };
310
+ },
311
+ /**
312
+ * Block a contact (TalkService blockContact).
313
+ *
314
+ * @param mid - Contact MID to block.
315
+ * @returns `{ blocked, mid }`.
316
+ */
317
+ async blockContact(mid) {
318
+ await service.client.blockContact(mid);
319
+ return { blocked: true, mid };
320
+ },
321
+ /**
322
+ * Unblock a contact (TalkService unblockContact).
323
+ *
324
+ * @param mid - Contact MID to unblock.
325
+ * @returns `{ unblocked, mid }`.
326
+ */
327
+ async unblockContact(mid) {
328
+ await service.client.unblockContact(mid);
329
+ return { unblocked: true, mid };
330
+ },
331
+ /**
332
+ * Accept a group/chat invitation — this account joins the chat (TalkService
333
+ * acceptChatInvitation).
334
+ *
335
+ * @param chatMid - Group/chat MID to accept.
336
+ * @returns `{ accepted, chatId }`.
337
+ */
338
+ async acceptInvitation(chatMid) {
339
+ await service.client.acceptChatInvitation(chatMid);
340
+ return { accepted: true, chatId: chatMid };
341
+ },
342
+ };
343
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * LINE core runtime accessors.
3
+ */
4
+ /**
5
+ * Resolve the active LINE client or fail fast when the protocol service has
6
+ * not established a session yet.
7
+ *
8
+ * @param getClient - Deferred LINE client accessor
9
+ * @returns Connected LINE client instance
10
+ */
11
+ export function requireLineClient(getClient) {
12
+ const client = getClient();
13
+ if (!client) {
14
+ throw new Error('Not connected');
15
+ }
16
+ return client;
17
+ }