@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.
- package/LICENSE +21 -0
- package/NOTICE +81 -0
- package/PRIVACY.md +39 -0
- package/README.md +659 -0
- package/dist/auth/credential-store.js +395 -0
- package/dist/auth/keychain.js +256 -0
- package/dist/cli/login.js +142 -0
- package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
- package/dist/line/auth/protocol/index.js +3 -0
- package/dist/line/auth/protocol/login-metadata.js +159 -0
- package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
- package/dist/line/auth/pwless/index.js +163 -0
- package/dist/line/auth/pwless/steps.js +213 -0
- package/dist/line/client/auth-service/client.js +240 -0
- package/dist/line/client/e2ee-service/client.js +69 -0
- package/dist/line/client/e2ee-service/requests.js +92 -0
- package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
- package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
- package/dist/line/client/index.js +213 -0
- package/dist/line/client/obs-media-client.js +140 -0
- package/dist/line/client/parsers.js +69 -0
- package/dist/line/client/shop-service/client.js +63 -0
- package/dist/line/client/sticker-meta.js +46 -0
- package/dist/line/client/sync-service/client.js +317 -0
- package/dist/line/client/talk-service/chat-query.js +122 -0
- package/dist/line/client/talk-service/command-client.js +242 -0
- package/dist/line/client/talk-service/contact-query.js +77 -0
- package/dist/line/client/talk-service/message-box-query.js +137 -0
- package/dist/line/client/talk-service/message-id-query.js +241 -0
- package/dist/line/client/talk-service/query-client.js +219 -0
- package/dist/line/client/talk-service/requests.js +383 -0
- package/dist/line/client/transport.js +138 -0
- package/dist/line/core/auth-session-logout.js +55 -0
- package/dist/line/core/auth-session-runtime.js +231 -0
- package/dist/line/core/auth-session-service.js +85 -0
- package/dist/line/core/chat-runtime-service.js +343 -0
- package/dist/line/core/client-runtime.js +17 -0
- package/dist/line/core/config.js +67 -0
- package/dist/line/core/constants.js +56 -0
- package/dist/line/core/contact-query-service.js +20 -0
- package/dist/line/core/directory-runtime-service.js +83 -0
- package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
- package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
- package/dist/line/core/e2ee/index.js +6 -0
- package/dist/line/core/e2ee/keys/decrypt.js +110 -0
- package/dist/line/core/e2ee/keys/encrypt.js +117 -0
- package/dist/line/core/e2ee/keys/group-key.js +260 -0
- package/dist/line/core/e2ee/keys/key-manager.js +231 -0
- package/dist/line/core/e2ee/keys/key-payload.js +130 -0
- package/dist/line/core/e2ee/keys/key-types.js +1 -0
- package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
- package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
- package/dist/line/core/e2ee/keys/peer-key.js +191 -0
- package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
- package/dist/line/core/e2ee/types.js +4 -0
- package/dist/line/core/group-query-service.js +20 -0
- package/dist/line/core/media-decrypt.js +78 -0
- package/dist/line/core/media-encrypt.js +97 -0
- package/dist/line/core/mention.js +89 -0
- package/dist/line/core/message-command-service.js +454 -0
- package/dist/line/core/message-query-service.js +161 -0
- package/dist/line/core/service.js +134 -0
- package/dist/line/core/session-state/index.js +243 -0
- package/dist/line/core/session-state/persist.js +192 -0
- package/dist/line/core/store.js +8 -0
- package/dist/line/core/thrift/binary/binary-reader.js +151 -0
- package/dist/line/core/thrift/binary/binary-writer.js +157 -0
- package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
- package/dist/line/core/thrift/compact/compact-reader.js +180 -0
- package/dist/line/core/thrift/compact/compact-writer.js +161 -0
- package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
- package/dist/line/core/thrift/fields/builders.js +124 -0
- package/dist/line/core/thrift/index.js +12 -0
- package/dist/line/core/thrift/message/decoder.js +55 -0
- package/dist/line/core/thrift/message/encoder.js +25 -0
- package/dist/line/core/thrift/primitives/varint.js +64 -0
- package/dist/line/core/thrift/struct/reader.js +172 -0
- package/dist/line/core/thrift/struct/writer.js +189 -0
- package/dist/line/core/thrift/types.js +23 -0
- package/dist/line/core/video-thumbnail.js +104 -0
- package/dist/mcp/handlers/contacts.js +166 -0
- package/dist/mcp/handlers/conversations.js +193 -0
- package/dist/mcp/handlers/groups.js +123 -0
- package/dist/mcp/handlers/index.js +15 -0
- package/dist/mcp/handlers/login.js +473 -0
- package/dist/mcp/handlers/media.js +69 -0
- package/dist/mcp/handlers/message-actions.js +62 -0
- package/dist/mcp/handlers/scope.js +120 -0
- package/dist/mcp/handlers/search.js +196 -0
- package/dist/mcp/handlers/send.js +523 -0
- package/dist/mcp/handlers/shared.js +84 -0
- package/dist/mcp/handlers/stickers.js +87 -0
- package/dist/mcp/media.js +228 -0
- package/dist/mcp/names.js +133 -0
- package/dist/mcp/phi-guard.js +105 -0
- package/dist/mcp/policy.js +34 -0
- package/dist/mcp/server.js +301 -0
- package/dist/mcp/tools.js +761 -0
- package/dist/mcp/ui/capability.js +26 -0
- package/dist/mcp/ui/login-app-protocol.js +118 -0
- package/dist/mcp/ui/login-app-styles.js +142 -0
- package/dist/mcp/ui/login-app-view.js +361 -0
- package/dist/mcp/ui/login-app.js +72 -0
- package/dist/mcp/ui/login-structured-content.js +1 -0
- package/dist/mcp/ui/resource.js +35 -0
- package/dist/mcp/ui/tools-with-ui.js +40 -0
- package/dist/search/bigram.js +86 -0
- package/dist/search/capture.js +140 -0
- package/dist/search/collector.js +176 -0
- package/dist/search/default-embedder.js +101 -0
- package/dist/search/embedder.js +13 -0
- package/dist/search/scope.js +142 -0
- package/dist/search/sqlite.js +141 -0
- package/dist/search/store.js +469 -0
- package/dist/util/data-dir.js +55 -0
- package/dist/util/log.js +86 -0
- package/dist/version.js +7 -0
- package/package.json +63 -0
- 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
|
+
}
|