@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,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE Client sync capability.
|
|
3
|
+
*/
|
|
4
|
+
import { createCliLogger } from '../../../util/log.js';
|
|
5
|
+
import { LINE_APP_CONFIG } from '../../core/config.js';
|
|
6
|
+
import { OP_TYPE } from '../../core/constants.js';
|
|
7
|
+
import { encodeCallMessage } from '../../core/thrift/index.js';
|
|
8
|
+
import { sendRequest } from '../transport.js';
|
|
9
|
+
/**
|
|
10
|
+
* Check whether a logger exposes the CLI logger contract used by LINE sync.
|
|
11
|
+
*
|
|
12
|
+
* @param logger - Candidate logger.
|
|
13
|
+
* @returns True when the logger supports info/warn/error.
|
|
14
|
+
*/
|
|
15
|
+
function hasCliLogger(logger) {
|
|
16
|
+
return Boolean(logger?.info && logger?.warn && logger?.error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the active logger for one LINE sync runtime.
|
|
20
|
+
*
|
|
21
|
+
* @param runtime - LINE client runtime.
|
|
22
|
+
* @returns Logger for sync diagnostics.
|
|
23
|
+
*/
|
|
24
|
+
function getLineLog(runtime) {
|
|
25
|
+
if (hasCliLogger(runtime?.startupFlowLogger)) {
|
|
26
|
+
return runtime.startupFlowLogger;
|
|
27
|
+
}
|
|
28
|
+
if (hasCliLogger(runtime?.logger)) {
|
|
29
|
+
return runtime.logger;
|
|
30
|
+
}
|
|
31
|
+
return createCliLogger('LINE');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the thrift payload for one sync request.
|
|
35
|
+
*
|
|
36
|
+
* @param runtime - LINE client runtime.
|
|
37
|
+
* @param count - Maximum operations requested.
|
|
38
|
+
* @returns Encoded sync request payload.
|
|
39
|
+
*/
|
|
40
|
+
function buildSyncPayload(runtime, count) {
|
|
41
|
+
const fields = [
|
|
42
|
+
[
|
|
43
|
+
12,
|
|
44
|
+
1,
|
|
45
|
+
[
|
|
46
|
+
[10, 1, Math.max(0, runtime.revision)],
|
|
47
|
+
[8, 2, count],
|
|
48
|
+
[10, 3, runtime.globalRevision ?? 0],
|
|
49
|
+
[10, 4, runtime.individualRevision ?? 0],
|
|
50
|
+
],
|
|
51
|
+
],
|
|
52
|
+
];
|
|
53
|
+
return encodeCallMessage('sync', runtime.seq++, fields);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse raw LINE operations into normalized operation objects.
|
|
57
|
+
*
|
|
58
|
+
* @param runtime - LINE client runtime.
|
|
59
|
+
* @param ops - Raw operation array.
|
|
60
|
+
* @returns Parsed operations.
|
|
61
|
+
*/
|
|
62
|
+
function parseOperations(runtime, ops) {
|
|
63
|
+
return Array.isArray(ops)
|
|
64
|
+
? ops.map((op) => runtime.parseOperation(op)).filter(Boolean)
|
|
65
|
+
: [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Apply token rotation returned from a sync response.
|
|
69
|
+
*
|
|
70
|
+
* @param runtime - LINE client runtime.
|
|
71
|
+
* @param result - Decoded sync response.
|
|
72
|
+
*/
|
|
73
|
+
function applyTokenRotation(runtime, result) {
|
|
74
|
+
if (!result.nextToken) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
runtime.authToken = result.nextToken;
|
|
78
|
+
runtime.emit('tokenRotated', result.nextToken);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Throw when the sync response contains a thrift exception payload.
|
|
82
|
+
*
|
|
83
|
+
* @param result - Decoded sync response.
|
|
84
|
+
* @param lineLog - Active LINE logger.
|
|
85
|
+
*/
|
|
86
|
+
function throwSyncException(result, lineLog) {
|
|
87
|
+
const exc = result.fields?.[1];
|
|
88
|
+
if (!exc || typeof exc !== 'object') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const excMsg = typeof exc[2] === 'string' ? exc[2] : JSON.stringify(exc);
|
|
92
|
+
lineLog.error('sync.exception', { error: excMsg });
|
|
93
|
+
throw new Error(`sync exception: ${excMsg}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply revision fields from one sync response and return raw operations.
|
|
97
|
+
*
|
|
98
|
+
* @param runtime - LINE client runtime.
|
|
99
|
+
* @param response - Top-level sync response payload.
|
|
100
|
+
* @returns Raw operations array.
|
|
101
|
+
*/
|
|
102
|
+
function updateSyncRevisions(runtime, response) {
|
|
103
|
+
const operationResponse = response?.[1];
|
|
104
|
+
const ops = operationResponse?.[1];
|
|
105
|
+
const nextRevision = response?.[2];
|
|
106
|
+
const lastGlobalRevision = operationResponse?.[2];
|
|
107
|
+
const lastIndividualRevision = operationResponse?.[3];
|
|
108
|
+
if (typeof nextRevision === 'number' || typeof nextRevision === 'bigint') {
|
|
109
|
+
runtime.revision =
|
|
110
|
+
typeof nextRevision === 'bigint' ? Number(nextRevision) : nextRevision;
|
|
111
|
+
}
|
|
112
|
+
if (typeof lastGlobalRevision === 'number' ||
|
|
113
|
+
typeof lastGlobalRevision === 'bigint') {
|
|
114
|
+
runtime.globalRevision =
|
|
115
|
+
typeof lastGlobalRevision === 'bigint'
|
|
116
|
+
? Number(lastGlobalRevision)
|
|
117
|
+
: lastGlobalRevision;
|
|
118
|
+
}
|
|
119
|
+
if (typeof lastIndividualRevision === 'number' ||
|
|
120
|
+
typeof lastIndividualRevision === 'bigint') {
|
|
121
|
+
runtime.individualRevision =
|
|
122
|
+
typeof lastIndividualRevision === 'bigint'
|
|
123
|
+
? Number(lastIndividualRevision)
|
|
124
|
+
: lastIndividualRevision;
|
|
125
|
+
}
|
|
126
|
+
return ops;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Emit one sync poll summary and maintain idle counters.
|
|
130
|
+
*
|
|
131
|
+
* @param lineLog - Active LINE logger.
|
|
132
|
+
* @param runtime - LINE client runtime.
|
|
133
|
+
* @param opCount - Number of operations returned.
|
|
134
|
+
* @param elapsedMs - Poll duration in milliseconds.
|
|
135
|
+
*/
|
|
136
|
+
function recordSyncPoll(lineLog, runtime, opCount, elapsedMs) {
|
|
137
|
+
if (opCount > 0) {
|
|
138
|
+
lineLog.info('sync.event', {
|
|
139
|
+
elapsed_ms: elapsedMs,
|
|
140
|
+
ops: opCount,
|
|
141
|
+
revision: runtime.revision,
|
|
142
|
+
});
|
|
143
|
+
runtime._syncEmptyLogCount = 0;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
runtime._syncEmptyLogCount = (runtime._syncEmptyLogCount || 0) + 1;
|
|
147
|
+
if (runtime._syncEmptyLogCount === 1 ||
|
|
148
|
+
runtime._syncEmptyLogCount % 10 === 0) {
|
|
149
|
+
lineLog.info('sync.idle', {
|
|
150
|
+
elapsed_ms: elapsedMs,
|
|
151
|
+
empty_polls: runtime._syncEmptyLogCount,
|
|
152
|
+
revision: runtime.revision,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Emit parsed operations to downstream listeners and advance revisions.
|
|
158
|
+
*
|
|
159
|
+
* @param runtime - LINE client runtime.
|
|
160
|
+
* @param ops - Parsed operations.
|
|
161
|
+
*/
|
|
162
|
+
function emitOperations(runtime, ops) {
|
|
163
|
+
for (const op of ops) {
|
|
164
|
+
runtime.emit('operation', op);
|
|
165
|
+
if (op.type === OP_TYPE.RECEIVE_MESSAGE && op.message) {
|
|
166
|
+
runtime.emit('message', op.message);
|
|
167
|
+
}
|
|
168
|
+
if (op.revision && op.revision > runtime.revision) {
|
|
169
|
+
runtime.revision = op.revision;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Compute the next poll delay from the current activity level.
|
|
175
|
+
*
|
|
176
|
+
* @param opCount - Number of operations returned.
|
|
177
|
+
* @param consecutiveEmptyPolls - Consecutive idle poll count.
|
|
178
|
+
* @returns Delay in milliseconds.
|
|
179
|
+
*/
|
|
180
|
+
function getNextPollDelay(opCount, consecutiveEmptyPolls) {
|
|
181
|
+
if (opCount > 0) {
|
|
182
|
+
return 250;
|
|
183
|
+
}
|
|
184
|
+
return Math.min(1000 * 2 ** Math.min(consecutiveEmptyPolls, 4), 15000);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Sleep helper used by the polling loop.
|
|
188
|
+
*
|
|
189
|
+
* @param ms - Delay in milliseconds.
|
|
190
|
+
* @returns Promise resolving after the delay.
|
|
191
|
+
*/
|
|
192
|
+
async function sleep(ms) {
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Run one long-poll iteration and emit any returned operations.
|
|
197
|
+
*
|
|
198
|
+
* @param runtime - LINE client runtime.
|
|
199
|
+
* @returns Parsed operations from this poll.
|
|
200
|
+
*/
|
|
201
|
+
async function pollOnce(runtime) {
|
|
202
|
+
const ops = await runtime.syncLongPoll(50, 60000);
|
|
203
|
+
emitOperations(runtime, ops);
|
|
204
|
+
return ops;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Create the sync capability bound to one LINE client runtime.
|
|
208
|
+
*
|
|
209
|
+
* @param runtime - Mutable LINE client runtime.
|
|
210
|
+
* @returns Sync and polling methods bound to the runtime.
|
|
211
|
+
*/
|
|
212
|
+
export function createSyncClient(runtime) {
|
|
213
|
+
return {
|
|
214
|
+
/**
|
|
215
|
+
* Sync operations since the current revision.
|
|
216
|
+
*
|
|
217
|
+
* @param count - Maximum number of operations to retrieve.
|
|
218
|
+
* @returns Parsed operations.
|
|
219
|
+
*/
|
|
220
|
+
async sync(count = 50) {
|
|
221
|
+
const data = buildSyncPayload(runtime, count);
|
|
222
|
+
const result = await sendRequest(runtime.host, LINE_APP_CONFIG.syncPath, data, { 'X-Line-Access': runtime.authToken }, 10000);
|
|
223
|
+
if (result.error === 'timeout') {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
const ops = result.fields?.[0]?.[1]?.[1];
|
|
227
|
+
return parseOperations(runtime, ops);
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* Long-poll sync until new operations arrive or the timeout elapses.
|
|
231
|
+
*
|
|
232
|
+
* @param count - Maximum number of operations to retrieve.
|
|
233
|
+
* @param timeoutMs - Long-poll timeout in milliseconds.
|
|
234
|
+
* @returns Parsed operations.
|
|
235
|
+
*/
|
|
236
|
+
async syncLongPoll(count = 50, timeoutMs = 60000) {
|
|
237
|
+
const lineLog = getLineLog(runtime);
|
|
238
|
+
const startedAt = Date.now();
|
|
239
|
+
const data = buildSyncPayload(runtime, count);
|
|
240
|
+
const result = await sendRequest(runtime.host, LINE_APP_CONFIG.syncPath, data, { 'X-Line-Access': runtime.authToken }, timeoutMs + 5000);
|
|
241
|
+
if (result.error === 'timeout') {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
applyTokenRotation(runtime, result);
|
|
245
|
+
throwSyncException(result, lineLog);
|
|
246
|
+
if (result.statusCode && result.statusCode !== 200) {
|
|
247
|
+
lineLog.warn('sync.http', { status_code: result.statusCode });
|
|
248
|
+
}
|
|
249
|
+
const response = result.fields?.[0];
|
|
250
|
+
const ops = updateSyncRevisions(runtime, response);
|
|
251
|
+
const opCount = Array.isArray(ops) ? ops.length : 0;
|
|
252
|
+
recordSyncPoll(lineLog, runtime, opCount, Date.now() - startedAt);
|
|
253
|
+
return parseOperations(runtime, ops);
|
|
254
|
+
},
|
|
255
|
+
/**
|
|
256
|
+
* Fetch the latest operation revision from the server.
|
|
257
|
+
*
|
|
258
|
+
* @returns The latest operation revision.
|
|
259
|
+
*/
|
|
260
|
+
async getLastOpRevision() {
|
|
261
|
+
const lineLog = getLineLog(runtime);
|
|
262
|
+
const result = await runtime.sendTalk('getLastOpRevision', []);
|
|
263
|
+
if (result.fields?.[1]) {
|
|
264
|
+
const exc = result.fields[1];
|
|
265
|
+
lineLog.error('revision.fetch.failed', { error: JSON.stringify(exc) });
|
|
266
|
+
throw new Error(`getLastOpRevision: ${exc[2] || exc[1] || 'unknown'}`);
|
|
267
|
+
}
|
|
268
|
+
const rev = result.fields?.[0];
|
|
269
|
+
lineLog.info('revision.fetch', { type: typeof rev, value: rev });
|
|
270
|
+
return rev !== undefined
|
|
271
|
+
? typeof rev === 'bigint'
|
|
272
|
+
? Number(rev)
|
|
273
|
+
: rev
|
|
274
|
+
: 0;
|
|
275
|
+
},
|
|
276
|
+
/**
|
|
277
|
+
* Start continuous polling for new operations and messages.
|
|
278
|
+
*
|
|
279
|
+
* @returns Promise resolving when polling stops.
|
|
280
|
+
*/
|
|
281
|
+
async startPolling() {
|
|
282
|
+
const lineLog = getLineLog(runtime);
|
|
283
|
+
if (runtime.polling) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
runtime.polling = true;
|
|
287
|
+
runtime.aborted = false;
|
|
288
|
+
if (runtime.revision < 0) {
|
|
289
|
+
runtime.revision = await runtime.getLastOpRevision();
|
|
290
|
+
lineLog.info('polling.start', { revision: runtime.revision });
|
|
291
|
+
}
|
|
292
|
+
let consecutiveEmptyPolls = 0;
|
|
293
|
+
while (runtime.polling && !runtime.aborted) {
|
|
294
|
+
try {
|
|
295
|
+
const ops = await pollOnce(runtime);
|
|
296
|
+
consecutiveEmptyPolls =
|
|
297
|
+
ops.length === 0 ? consecutiveEmptyPolls + 1 : 0;
|
|
298
|
+
await sleep(getNextPollDelay(ops.length, consecutiveEmptyPolls));
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
runtime.emit('error', err);
|
|
302
|
+
consecutiveEmptyPolls = 0;
|
|
303
|
+
if (runtime.polling && !runtime.aborted) {
|
|
304
|
+
await sleep(3000);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
/**
|
|
310
|
+
* Stop the polling loop.
|
|
311
|
+
*/
|
|
312
|
+
stopPolling() {
|
|
313
|
+
runtime.polling = false;
|
|
314
|
+
runtime.aborted = true;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createCliLogger } from '../../../util/log.js';
|
|
2
|
+
import { buildGetChatsRequest } from './requests.js';
|
|
3
|
+
const GET_CHATS_BATCH_SIZE = 100;
|
|
4
|
+
const lineLog = createCliLogger('LINE');
|
|
5
|
+
/**
|
|
6
|
+
* Normalize bigint-like thrift scalars into numbers when possible.
|
|
7
|
+
*
|
|
8
|
+
* @param value - Raw thrift scalar.
|
|
9
|
+
* @returns Parsed number or original fallback.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeNumericValue(value) {
|
|
12
|
+
if (typeof value === 'bigint') {
|
|
13
|
+
return Number(value);
|
|
14
|
+
}
|
|
15
|
+
return value || null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the actual chat-array field from a getChats response.
|
|
19
|
+
*
|
|
20
|
+
* @param responseRoot - Root response payload.
|
|
21
|
+
* @returns Raw chat array or null.
|
|
22
|
+
*/
|
|
23
|
+
export function findChatArray(responseRoot) {
|
|
24
|
+
if (!responseRoot) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const keys = Object.keys(responseRoot)
|
|
28
|
+
.map(Number)
|
|
29
|
+
.sort((a, b) => a - b);
|
|
30
|
+
lineLog.debug('talk.get_chats.response', { keys: keys.join(',') });
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
const candidate = responseRoot[key];
|
|
33
|
+
if (!Array.isArray(candidate)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (candidate.length === 0 || typeof candidate[0] !== 'object') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (key !== 1) {
|
|
40
|
+
lineLog.warn('talk.get_chats.field_mismatch', {
|
|
41
|
+
actual_field: key,
|
|
42
|
+
expected_field: 1,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Map one raw chat thrift struct into the app shape.
|
|
51
|
+
*
|
|
52
|
+
* @param chat - Raw chat thrift struct.
|
|
53
|
+
* @returns Normalized chat or null.
|
|
54
|
+
*/
|
|
55
|
+
export function mapChatStruct(chat) {
|
|
56
|
+
if (!chat || typeof chat !== 'object') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: chat[1] ?? null,
|
|
61
|
+
chatMid: chat[2] || null,
|
|
62
|
+
createdTime: normalizeNumericValue(chat[3]),
|
|
63
|
+
notificationDisabled: Boolean(chat[4]),
|
|
64
|
+
favoriteTimestamp: normalizeNumericValue(chat[5]),
|
|
65
|
+
chatName: chat[6] || null,
|
|
66
|
+
picturePath: chat[7] || null,
|
|
67
|
+
extra: chat[8] || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Map a chat list, dropping invalid entries.
|
|
72
|
+
*
|
|
73
|
+
* @param chats - Raw chat list.
|
|
74
|
+
* @returns Normalized chat list.
|
|
75
|
+
*/
|
|
76
|
+
export function mapChatList(chats) {
|
|
77
|
+
return Array.isArray(chats)
|
|
78
|
+
? chats
|
|
79
|
+
.map((chat) => mapChatStruct(chat))
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
: [];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch and normalize chats from TalkService.
|
|
85
|
+
*
|
|
86
|
+
* @param runtime - LINE client runtime.
|
|
87
|
+
* @param syncReason - Talk sync reason constant.
|
|
88
|
+
* @param chatMids - Requested chat MIDs.
|
|
89
|
+
* @param withMembers - Whether to include members.
|
|
90
|
+
* @returns Normalized chat list.
|
|
91
|
+
*/
|
|
92
|
+
export async function fetchChats(runtime, syncReason, chatMids, withMembers = true) {
|
|
93
|
+
if (!Array.isArray(chatMids) || chatMids.length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
if (chatMids.length > GET_CHATS_BATCH_SIZE) {
|
|
97
|
+
const allChats = [];
|
|
98
|
+
for (let index = 0; index < chatMids.length; index += GET_CHATS_BATCH_SIZE) {
|
|
99
|
+
const batch = chatMids.slice(index, index + GET_CHATS_BATCH_SIZE);
|
|
100
|
+
const chats = await fetchChats(runtime, syncReason, batch, withMembers);
|
|
101
|
+
allChats.push(...chats);
|
|
102
|
+
}
|
|
103
|
+
return allChats;
|
|
104
|
+
}
|
|
105
|
+
let result;
|
|
106
|
+
try {
|
|
107
|
+
result = await runtime.sendTalk('getChats', buildGetChatsRequest(chatMids, withMembers, syncReason));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
lineLog.error('talk.get_chats.failed', { error: error?.message || error });
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const response = result.fields?.[0];
|
|
114
|
+
const chatStructs = findChatArray(response);
|
|
115
|
+
if (chatStructs) {
|
|
116
|
+
return mapChatList(chatStructs);
|
|
117
|
+
}
|
|
118
|
+
lineLog.warn('talk.get_chats.empty', {
|
|
119
|
+
keys: response ? Object.keys(response).join(',') : 'null',
|
|
120
|
+
});
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE TalkService command capability.
|
|
3
|
+
*
|
|
4
|
+
* Send messages and other TalkService write operations with side effects.
|
|
5
|
+
*/
|
|
6
|
+
import { Buffer } from 'node:buffer';
|
|
7
|
+
import { parseMessage } from '../parsers.js';
|
|
8
|
+
import { buildAcceptChatInvitationRequest, buildCancelReactionRequest, buildContactMidActionRequest, buildCreateChatRequest, buildDeleteOtherFromChatRequest, buildDeleteSelfFromChatRequest, buildFindAndAddContactsByMidRequest, buildInviteIntoChatRequest, buildReactRequest, buildSendChatCheckedRequest, buildSendMessageRequest, buildUnsendMessageRequest, buildUpdateChatNameRequest, normalizeSendMessageOptions, } from './requests.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create the TalkService command capability bound to one LINE client runtime.
|
|
11
|
+
*
|
|
12
|
+
* @param runtime - Mutable LINE client runtime.
|
|
13
|
+
* @returns Talk command methods bound to the runtime.
|
|
14
|
+
*/
|
|
15
|
+
export function createTalkCommandClient(runtime) {
|
|
16
|
+
return {
|
|
17
|
+
/**
|
|
18
|
+
* Send a LINE message via TalkService.
|
|
19
|
+
*
|
|
20
|
+
* This method keeps backward compatibility with the existing
|
|
21
|
+
* `sendMessage(to, text)` signature while also accepting a richer options
|
|
22
|
+
* object closer to linejs TalkService.sendMessage(). The extended form is
|
|
23
|
+
* required for E2EE outbound payloads because LINE expects caller-supplied
|
|
24
|
+
* `chunks` and matching `contentMetadata`.
|
|
25
|
+
*
|
|
26
|
+
* @param to - The recipient chat ID, or a full message options object.
|
|
27
|
+
* @param text - The message text when using the legacy two-argument signature.
|
|
28
|
+
* @returns The sent message object or null on error.
|
|
29
|
+
*/
|
|
30
|
+
async sendMessage(to, text) {
|
|
31
|
+
const options = normalizeSendMessageOptions(to, text);
|
|
32
|
+
if (Array.isArray(options.chunks)) {
|
|
33
|
+
options.chunks = options.chunks.map((chunk) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
34
|
+
}
|
|
35
|
+
const result = await runtime.sendTalk('sendMessage', buildSendMessageRequest(options));
|
|
36
|
+
if (result.error) {
|
|
37
|
+
throw new Error(`sendMessage failed: ${result.error}`);
|
|
38
|
+
}
|
|
39
|
+
// fields[0] is the raw Thrift Message struct; normalize it so callers
|
|
40
|
+
// get { id, from, ... } consistent with the read path (raw struct
|
|
41
|
+
// carries the id at index [4], not `.id`).
|
|
42
|
+
const sent = result.fields?.[0];
|
|
43
|
+
return sent ? parseMessage(sent) : null;
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Mark a LINE conversation read up to a given message via TalkService
|
|
47
|
+
* sendChatChecked. This has a side effect visible to the other party (a
|
|
48
|
+
* read receipt); it is only ever called from explicit user-intent paths,
|
|
49
|
+
* never from background sync. Returns true on success; throws on LINE error.
|
|
50
|
+
*
|
|
51
|
+
* @param chatMid - Chat MID to mark read.
|
|
52
|
+
* @param lastMessageId - Message id to mark read up to.
|
|
53
|
+
* @param sessionId - Client session id (default 0).
|
|
54
|
+
* @returns True when the read receipt was accepted.
|
|
55
|
+
*/
|
|
56
|
+
async sendChatChecked(chatMid, lastMessageId, sessionId = 0) {
|
|
57
|
+
const result = await runtime.sendTalk('sendChatChecked', buildSendChatCheckedRequest(chatMid, lastMessageId, sessionId));
|
|
58
|
+
if (result.error) {
|
|
59
|
+
throw new Error(`sendChatChecked failed: ${result.error}`);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Rename a group/chat via TalkService updateChat. Side effect visible to
|
|
65
|
+
* every member. Throws on LINE error.
|
|
66
|
+
*
|
|
67
|
+
* @param chatMid - Group/chat MID to rename.
|
|
68
|
+
* @param name - New name.
|
|
69
|
+
* @returns True when the rename was accepted.
|
|
70
|
+
*/
|
|
71
|
+
async updateChatName(chatMid, name) {
|
|
72
|
+
const result = await runtime.sendTalk('updateChat', buildUpdateChatNameRequest(chatMid, name));
|
|
73
|
+
if (result.error) {
|
|
74
|
+
throw new Error(`updateChat failed: ${result.error}`);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Invite members into a group/chat via TalkService inviteIntoChat. The
|
|
80
|
+
* invitees must accept before joining a group. Throws on LINE error.
|
|
81
|
+
*
|
|
82
|
+
* @param chatMid - Group/chat MID to invite into.
|
|
83
|
+
* @param mids - MIDs to invite.
|
|
84
|
+
* @returns True when the invitation was accepted.
|
|
85
|
+
*/
|
|
86
|
+
async inviteIntoChat(chatMid, mids) {
|
|
87
|
+
const result = await runtime.sendTalk('inviteIntoChat', buildInviteIntoChatRequest(chatMid, mids));
|
|
88
|
+
if (result.error) {
|
|
89
|
+
throw new Error(`inviteIntoChat failed: ${result.error}`);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* Remove (kick) members from a group/chat via TalkService
|
|
95
|
+
* deleteOtherFromChat. Side effect visible to every member; the removed
|
|
96
|
+
* member loses access. Throws on LINE error.
|
|
97
|
+
*
|
|
98
|
+
* @param chatMid - Group/chat MID to remove members from.
|
|
99
|
+
* @param mids - MIDs to remove.
|
|
100
|
+
* @returns True when the removal was accepted.
|
|
101
|
+
*/
|
|
102
|
+
async deleteOtherFromChat(chatMid, mids) {
|
|
103
|
+
const result = await runtime.sendTalk('deleteOtherFromChat', buildDeleteOtherFromChatRequest(chatMid, mids));
|
|
104
|
+
if (result.error) {
|
|
105
|
+
throw new Error(`deleteOtherFromChat failed: ${result.error}`);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* Leave a group/chat via TalkService deleteSelfFromChat — the authenticated
|
|
111
|
+
* account itself exits. Throws on LINE error.
|
|
112
|
+
*
|
|
113
|
+
* @param chatMid - Group/chat MID to leave.
|
|
114
|
+
* @returns True when the departure was accepted.
|
|
115
|
+
*/
|
|
116
|
+
async deleteSelfFromChat(chatMid) {
|
|
117
|
+
const result = await runtime.sendTalk('deleteSelfFromChat', buildDeleteSelfFromChatRequest(chatMid));
|
|
118
|
+
if (result.error) {
|
|
119
|
+
throw new Error(`deleteSelfFromChat failed: ${result.error}`);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Create a new group/room via TalkService createChat with an initial member
|
|
125
|
+
* set. Does NOT mint or register any E2EE group key (key material is
|
|
126
|
+
* established lazily on first E2EE send/receive) — see the never-mint fix.
|
|
127
|
+
* Throws on LINE error.
|
|
128
|
+
*
|
|
129
|
+
* @param name - New chat name.
|
|
130
|
+
* @param mids - Initial member MIDs.
|
|
131
|
+
* @param chatType - LINE chat type (0 = group, 1 = room). Default 1.
|
|
132
|
+
* @returns The created Chat struct.
|
|
133
|
+
*/
|
|
134
|
+
async createChat(name, mids, chatType = 1) {
|
|
135
|
+
const result = await runtime.sendTalk('createChat', buildCreateChatRequest(name, mids, chatType));
|
|
136
|
+
if (result.error) {
|
|
137
|
+
throw new Error(`createChat failed: ${result.error}`);
|
|
138
|
+
}
|
|
139
|
+
return result.fields?.[0] ?? null;
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Add a predefined reaction to a message via TalkService react. Visible to
|
|
143
|
+
* the conversation. Throws on LINE error.
|
|
144
|
+
*
|
|
145
|
+
* @param messageId - Target message id (numeric string).
|
|
146
|
+
* @param reactionType - Predefined reaction type (2=LIKE, 3=LOVE, 4=LAUGH,
|
|
147
|
+
* 5=SURPRISE, 6=SAD, 7=ANGRY). Default 2.
|
|
148
|
+
* @returns True when the reaction was accepted.
|
|
149
|
+
*/
|
|
150
|
+
async react(messageId, reactionType = 2) {
|
|
151
|
+
const result = await runtime.sendTalk('react', buildReactRequest(messageId, reactionType));
|
|
152
|
+
if (result.error) {
|
|
153
|
+
throw new Error(`react failed: ${result.error}`);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Remove this account's reaction from a message via TalkService
|
|
159
|
+
* cancelReaction. Throws on LINE error.
|
|
160
|
+
*
|
|
161
|
+
* @param messageId - Target message id (numeric string).
|
|
162
|
+
* @returns True when the cancellation was accepted.
|
|
163
|
+
*/
|
|
164
|
+
async cancelReaction(messageId) {
|
|
165
|
+
const result = await runtime.sendTalk('cancelReaction', buildCancelReactionRequest(messageId));
|
|
166
|
+
if (result.error) {
|
|
167
|
+
throw new Error(`cancelReaction failed: ${result.error}`);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Unsend (retract) one of this account's own messages via TalkService
|
|
173
|
+
* unsendMessage — it is removed for everyone in the conversation. LINE only
|
|
174
|
+
* permits unsending the caller's own messages. Throws on LINE error.
|
|
175
|
+
*
|
|
176
|
+
* @param messageId - Message id to unsend (numeric string).
|
|
177
|
+
* @returns True when the unsend was accepted.
|
|
178
|
+
*/
|
|
179
|
+
async unsendMessage(messageId) {
|
|
180
|
+
const result = await runtime.sendTalk('unsendMessage', buildUnsendMessageRequest(messageId));
|
|
181
|
+
if (result.error) {
|
|
182
|
+
throw new Error(`unsendMessage failed: ${result.error}`);
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
/**
|
|
187
|
+
* Add a friend by MID via TalkService findAndAddContactsByMid. Throws on
|
|
188
|
+
* LINE error.
|
|
189
|
+
*
|
|
190
|
+
* @param mid - MID of the person to add.
|
|
191
|
+
* @param reference - JSON reference breadcrumb (optional).
|
|
192
|
+
* @returns The added contact result (raw response fields).
|
|
193
|
+
*/
|
|
194
|
+
async findAndAddContactByMid(mid, reference) {
|
|
195
|
+
const result = await runtime.sendTalk('findAndAddContactsByMid', buildFindAndAddContactsByMidRequest(mid, reference));
|
|
196
|
+
if (result.error) {
|
|
197
|
+
throw new Error(`findAndAddContactsByMid failed: ${result.error}`);
|
|
198
|
+
}
|
|
199
|
+
return result.fields?.[0] ?? null;
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Block a contact via TalkService blockContact. Throws on LINE error.
|
|
203
|
+
*
|
|
204
|
+
* @param mid - Contact MID to block.
|
|
205
|
+
* @returns True when the block was accepted.
|
|
206
|
+
*/
|
|
207
|
+
async blockContact(mid) {
|
|
208
|
+
const result = await runtime.sendTalk('blockContact', buildContactMidActionRequest(mid));
|
|
209
|
+
if (result.error) {
|
|
210
|
+
throw new Error(`blockContact failed: ${result.error}`);
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Unblock a contact via TalkService unblockContact. Throws on LINE error.
|
|
216
|
+
*
|
|
217
|
+
* @param mid - Contact MID to unblock.
|
|
218
|
+
* @returns True when the unblock was accepted.
|
|
219
|
+
*/
|
|
220
|
+
async unblockContact(mid) {
|
|
221
|
+
const result = await runtime.sendTalk('unblockContact', buildContactMidActionRequest(mid));
|
|
222
|
+
if (result.error) {
|
|
223
|
+
throw new Error(`unblockContact failed: ${result.error}`);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Accept a group/chat invitation via TalkService acceptChatInvitation —
|
|
229
|
+
* this account joins the chat. Throws on LINE error.
|
|
230
|
+
*
|
|
231
|
+
* @param chatMid - Group/chat MID to accept the invitation for.
|
|
232
|
+
* @returns True when the acceptance was accepted.
|
|
233
|
+
*/
|
|
234
|
+
async acceptChatInvitation(chatMid) {
|
|
235
|
+
const result = await runtime.sendTalk('acceptChatInvitation', buildAcceptChatInvitationRequest(chatMid));
|
|
236
|
+
if (result.error) {
|
|
237
|
+
throw new Error(`acceptChatInvitation failed: ${result.error}`);
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|