@rikaidev/yomi 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/dist/ambient/ambient-event-graph.js +172 -0
- package/dist/ambient/ambient-observer.js +71 -0
- package/dist/ambient/event-graph-reducer.js +300 -0
- package/dist/ambient/ingestion-adapter.js +203 -0
- package/dist/ambient/journal/crypto.js +58 -0
- package/dist/ambient/journal/journal-store.js +284 -0
- package/dist/ambient/journal/types.js +7 -0
- package/dist/ambient/summaries/retriever.js +55 -0
- package/dist/ambient/summaries/summary-engine.js +128 -0
- package/dist/ambient/summaries/types.js +4 -0
- package/dist/attention/policy.js +101 -0
- package/dist/auth/credential-store.js +47 -17
- package/dist/auth/keychain.js +7 -54
- package/dist/cli/query.js +875 -0
- package/dist/cli/work-item.js +24 -0
- package/dist/insight/relationships.js +1 -1
- package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
- package/dist/line/auth/pwless/index.js +1 -1
- package/dist/line/client/auth-service/client.js +7 -8
- package/dist/line/client/buddy-service/client.js +73 -0
- package/dist/line/client/index.js +28 -3
- package/dist/line/client/obs-media-client.js +38 -0
- package/dist/line/client/parsers.js +1 -0
- package/dist/line/client/square-service/client.js +298 -0
- package/dist/line/client/sticker-meta.js +32 -0
- package/dist/line/client/sync-service/client.js +31 -3
- package/dist/line/client/talk-service/command-client.js +9 -1
- package/dist/line/client/talk-service/contact-query.js +8 -1
- package/dist/line/client/talk-service/message-box-query.js +17 -0
- package/dist/line/client/talk-service/message-id-query.js +27 -3
- package/dist/line/client/talk-service/query-client.js +32 -26
- package/dist/line/client/talk-service/requests.js +10 -11
- package/dist/line/core/auth-session-runtime.js +17 -2
- package/dist/line/core/chat-event.js +35 -0
- package/dist/line/core/chat-runtime-service.js +18 -23
- package/dist/line/core/config.js +1 -0
- package/dist/line/core/directory-runtime-service.js +10 -2
- package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
- package/dist/line/core/flex-message.js +453 -0
- package/dist/line/core/mention.js +1 -1
- package/dist/line/core/message-preview.js +60 -0
- package/dist/line/core/message-query-service.js +0 -20
- package/dist/line/core/service.js +1 -3
- package/dist/mcp/handlers/contacts.js +12 -6
- package/dist/mcp/handlers/conversations.js +91 -4
- package/dist/mcp/handlers/login.js +1 -1
- package/dist/mcp/handlers/message-actions.js +1 -1
- package/dist/mcp/handlers/search.js +1 -1
- package/dist/mcp/handlers/send.js +8 -11
- package/dist/mcp/handlers/shared.js +1 -1
- package/dist/mcp/integrator.js +168 -0
- package/dist/mcp/media.js +135 -4
- package/dist/mcp/names.js +78 -6
- package/dist/mcp/phi-guard.js +2 -2
- package/dist/search/bigram.js +2 -2
- package/dist/search/collector.js +1 -1
- package/dist/search/default-embedder.js +12 -3
- package/dist/search/embedder.js +1 -1
- package/dist/version.js +1 -1
- package/dist/work-item/adapter.js +1 -0
- package/dist/work-item/destination-contracts.js +1 -0
- package/dist/work-item/event-context.js +73 -0
- package/dist/work-item/gold-contract.js +101 -0
- package/dist/work-item/gold-coverage.js +96 -0
- package/dist/work-item/gold-derivation.js +67 -0
- package/dist/work-item/gold-split.js +143 -0
- package/dist/work-item/index.js +23 -0
- package/dist/work-item/line-adapter.js +111 -0
- package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
- package/dist/work-item/local-provider.js +38 -0
- package/dist/work-item/local-work-event-provider.js +56 -0
- package/dist/work-item/onnx-work-event-adapter.js +223 -0
- package/dist/work-item/provider.js +21 -0
- package/dist/work-item/registry.js +19 -0
- package/dist/work-item/service.js +85 -0
- package/dist/work-item/store.js +97 -0
- package/dist/work-item/types.js +79 -0
- package/dist/work-item/work-event-graph-engine.js +63 -0
- package/dist/work-item/work-event-inference.js +267 -0
- package/dist/work-item/work-event-matcher.js +105 -0
- package/dist/work-item/work-event-model-manifest.js +39 -0
- package/dist/work-item/work-event-store.js +125 -0
- package/dist/work-item/work-event.js +123 -0
- package/package.json +26 -2
- package/run.mjs +371 -2
|
@@ -52,6 +52,29 @@ function buildSyncPayload(runtime, count) {
|
|
|
52
52
|
];
|
|
53
53
|
return encodeCallMessage('sync', runtime.seq++, fields);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* A negative revision is uninitialised, never a valid LINE checkpoint.
|
|
57
|
+
* Short-lived callers (for example the desktop `chats` bridge) call
|
|
58
|
+
* syncLongPoll directly, so initialise the cursor at the protocol boundary.
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureSyncRevision(runtime, lineLog) {
|
|
61
|
+
const current = Number(runtime.revision);
|
|
62
|
+
if (Number.isFinite(current) && current >= 0)
|
|
63
|
+
return;
|
|
64
|
+
const result = await runtime.sendTalk('getLastOpRevision', []);
|
|
65
|
+
if (result.fields?.[1]) {
|
|
66
|
+
const exc = result.fields[1];
|
|
67
|
+
lineLog.error('revision.fetch.failed', { error: JSON.stringify(exc) });
|
|
68
|
+
throw new Error(`getLastOpRevision: ${exc[2] || exc[1] || 'unknown'}`);
|
|
69
|
+
}
|
|
70
|
+
const rawRevision = result.fields?.[0];
|
|
71
|
+
const revision = typeof rawRevision === 'bigint' ? Number(rawRevision) : Number(rawRevision);
|
|
72
|
+
if (!Number.isFinite(revision) || revision < 0) {
|
|
73
|
+
throw new Error(`getLastOpRevision returned invalid revision: ${String(rawRevision)}`);
|
|
74
|
+
}
|
|
75
|
+
runtime.revision = revision;
|
|
76
|
+
lineLog.info('revision.fetch', { type: typeof rawRevision, value: revision });
|
|
77
|
+
}
|
|
55
78
|
/**
|
|
56
79
|
* Parse raw LINE operations into normalized operation objects.
|
|
57
80
|
*
|
|
@@ -99,15 +122,18 @@ function throwSyncException(result, lineLog) {
|
|
|
99
122
|
* @param response - Top-level sync response payload.
|
|
100
123
|
* @returns Raw operations array.
|
|
101
124
|
*/
|
|
102
|
-
function updateSyncRevisions(runtime, response) {
|
|
125
|
+
export function updateSyncRevisions(runtime, response) {
|
|
103
126
|
const operationResponse = response?.[1];
|
|
104
127
|
const ops = operationResponse?.[1];
|
|
105
128
|
const nextRevision = response?.[2];
|
|
106
129
|
const lastGlobalRevision = operationResponse?.[2];
|
|
107
130
|
const lastIndividualRevision = operationResponse?.[3];
|
|
108
131
|
if (typeof nextRevision === 'number' || typeof nextRevision === 'bigint') {
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const candidate = typeof nextRevision === 'bigint' ? Number(nextRevision) : nextRevision;
|
|
133
|
+
if (Number.isFinite(candidate) &&
|
|
134
|
+
candidate >= Number(runtime.revision || 0)) {
|
|
135
|
+
runtime.revision = candidate;
|
|
136
|
+
}
|
|
111
137
|
}
|
|
112
138
|
if (typeof lastGlobalRevision === 'number' ||
|
|
113
139
|
typeof lastGlobalRevision === 'bigint') {
|
|
@@ -218,6 +244,7 @@ export function createSyncClient(runtime) {
|
|
|
218
244
|
* @returns Parsed operations.
|
|
219
245
|
*/
|
|
220
246
|
async sync(count = 50) {
|
|
247
|
+
await ensureSyncRevision(runtime, getLineLog(runtime));
|
|
221
248
|
const data = buildSyncPayload(runtime, count);
|
|
222
249
|
const result = await sendRequest(runtime.host, LINE_APP_CONFIG.syncPath, data, { 'X-Line-Access': runtime.authToken }, 10000);
|
|
223
250
|
if (result.error === 'timeout') {
|
|
@@ -236,6 +263,7 @@ export function createSyncClient(runtime) {
|
|
|
236
263
|
async syncLongPoll(count = 50, timeoutMs = 60000) {
|
|
237
264
|
const lineLog = getLineLog(runtime);
|
|
238
265
|
const startedAt = Date.now();
|
|
266
|
+
await ensureSyncRevision(runtime, lineLog);
|
|
239
267
|
const data = buildSyncPayload(runtime, count);
|
|
240
268
|
const result = await sendRequest(runtime.host, LINE_APP_CONFIG.syncPath, data, { 'X-Line-Access': runtime.authToken }, timeoutMs + 5000);
|
|
241
269
|
if (result.error === 'timeout') {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Buffer } from 'node:buffer';
|
|
7
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';
|
|
8
|
+
import { buildAcceptChatInvitationRequest, buildCancelReactionRequest, buildContactMidActionRequest, buildCreateChatRequest, buildDeleteOtherFromChatRequest, buildDeleteSelfFromChatRequest, buildFindAndAddContactsByMidRequest, buildInviteIntoChatRequest, buildReactRequest, buildSendChatCheckedRequest, buildSendMessageRequest, buildUnsendMessageRequest, buildUpdateChatNameRequest, buildUpdateContactNotificationRequest, normalizeSendMessageOptions, } from './requests.js';
|
|
9
9
|
/**
|
|
10
10
|
* Create the TalkService command capability bound to one LINE client runtime.
|
|
11
11
|
*
|
|
@@ -60,6 +60,14 @@ export function createTalkCommandClient(runtime) {
|
|
|
60
60
|
}
|
|
61
61
|
return true;
|
|
62
62
|
},
|
|
63
|
+
/** Mute or unmute notifications for a 1:1/official-account contact. */
|
|
64
|
+
async updateContactNotificationDisabled(mid, disabled) {
|
|
65
|
+
const result = await runtime.sendTalk('updateContactSetting', buildUpdateContactNotificationRequest(mid, disabled));
|
|
66
|
+
if (result.error) {
|
|
67
|
+
throw new Error(`updateContactSetting failed: ${result.error}`);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
63
71
|
/**
|
|
64
72
|
* Rename a group/chat via TalkService updateChat. Side effect visible to
|
|
65
73
|
* every member. Throws on LINE error.
|
|
@@ -8,7 +8,10 @@ function normalizeNumericValue(value) {
|
|
|
8
8
|
if (typeof value === 'bigint') {
|
|
9
9
|
return Number(value);
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
if (value === null || value === undefined || value === '')
|
|
12
|
+
return null;
|
|
13
|
+
const number = Number(value);
|
|
14
|
+
return Number.isFinite(number) ? number : null;
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Extract profile fields from one raw contact struct.
|
|
@@ -27,6 +30,10 @@ function mapContactProfileFields(contact) {
|
|
|
27
30
|
picturePath: contact[37] || null,
|
|
28
31
|
statusMessageContentMetadata: contact[43] || {},
|
|
29
32
|
profileId: contact[49] || null,
|
|
33
|
+
attributes: normalizeNumericValue(contact[35]),
|
|
34
|
+
settings: normalizeNumericValue(contact[36]),
|
|
35
|
+
notificationDisabled: Boolean((normalizeNumericValue(contact[36]) ?? 0) & 1),
|
|
36
|
+
isOfficial: Boolean((normalizeNumericValue(contact[35]) ?? 0) & 32),
|
|
30
37
|
};
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
@@ -106,6 +106,23 @@ export function mapMessageBoxList(boxes) {
|
|
|
106
106
|
.filter(Boolean)
|
|
107
107
|
: [];
|
|
108
108
|
}
|
|
109
|
+
/** Merge paged message boxes without duplicate rows and stop on a stalled cursor. */
|
|
110
|
+
export function mergeMessageBoxPages(pages) {
|
|
111
|
+
const merged = new Map();
|
|
112
|
+
for (const page of pages) {
|
|
113
|
+
for (const box of page.messageBoxes ?? []) {
|
|
114
|
+
if (box?.id)
|
|
115
|
+
merged.set(String(box.id), box);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...merged.values()];
|
|
119
|
+
}
|
|
120
|
+
export function nextMessageBoxCursor(boxes, previous) {
|
|
121
|
+
const next = String(boxes.at(-1)?.id ?? '');
|
|
122
|
+
if (!next || next === previous)
|
|
123
|
+
return undefined;
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
109
126
|
/**
|
|
110
127
|
* Emit one debug log for previous-message responses.
|
|
111
128
|
*
|
|
@@ -32,6 +32,12 @@ const writeStruct = (fields) => {
|
|
|
32
32
|
]);
|
|
33
33
|
};
|
|
34
34
|
writeValue = (type, value) => {
|
|
35
|
+
if (type === 2) {
|
|
36
|
+
return Buffer.from([value ? 1 : 0]);
|
|
37
|
+
}
|
|
38
|
+
if (type === 8) {
|
|
39
|
+
return writeI32(Number(value));
|
|
40
|
+
}
|
|
35
41
|
if (type === 10) {
|
|
36
42
|
return writeI64(value);
|
|
37
43
|
}
|
|
@@ -49,9 +55,17 @@ writeValue = (type, value) => {
|
|
|
49
55
|
...items.map((item) => writeValue(elementType, item)),
|
|
50
56
|
]);
|
|
51
57
|
}
|
|
58
|
+
if (type === 14) {
|
|
59
|
+
const [elementType, items] = value;
|
|
60
|
+
return Buffer.concat([
|
|
61
|
+
Buffer.from([elementType]),
|
|
62
|
+
writeI32(items.length),
|
|
63
|
+
...items.map((item) => writeValue(elementType, item)),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
52
66
|
throw new Error(`Unsupported TBinary write type: ${type}`);
|
|
53
67
|
};
|
|
54
|
-
const encodeBinaryCallMessage = (method, seqId, fields) => {
|
|
68
|
+
export const encodeBinaryCallMessage = (method, seqId, fields) => {
|
|
55
69
|
return Buffer.concat([
|
|
56
70
|
writeI32(0x80010001),
|
|
57
71
|
writeString(method),
|
|
@@ -145,9 +159,19 @@ readValue = (buffer, state, type) => {
|
|
|
145
159
|
}
|
|
146
160
|
return items;
|
|
147
161
|
}
|
|
162
|
+
if (type === 14) {
|
|
163
|
+
const elementType = buffer[state.position];
|
|
164
|
+
state.position += 1;
|
|
165
|
+
const length = readI32(buffer, state);
|
|
166
|
+
const items = [];
|
|
167
|
+
for (let index = 0; index < length; index += 1) {
|
|
168
|
+
items.push(readValue(buffer, state, elementType));
|
|
169
|
+
}
|
|
170
|
+
return items;
|
|
171
|
+
}
|
|
148
172
|
throw new Error(`Unsupported TBinary read type: ${type}`);
|
|
149
173
|
};
|
|
150
|
-
const decodeBinaryResponseMessage = (buffer) => {
|
|
174
|
+
export const decodeBinaryResponseMessage = (buffer) => {
|
|
151
175
|
const state = { position: 0 };
|
|
152
176
|
readU32(buffer, state);
|
|
153
177
|
const method = readString(buffer, state);
|
|
@@ -155,7 +179,7 @@ const decodeBinaryResponseMessage = (buffer) => {
|
|
|
155
179
|
const fields = readStruct(buffer, state);
|
|
156
180
|
return { fields, method, seqId };
|
|
157
181
|
};
|
|
158
|
-
const postBinary = (host, path, body, token) => {
|
|
182
|
+
export const postBinary = (host, path, body, token) => {
|
|
159
183
|
return new Promise((resolve, reject) => {
|
|
160
184
|
const request = https.request({
|
|
161
185
|
headers: {
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { parseMessages } from '../parsers.js';
|
|
7
7
|
import { fetchChats } from './chat-query.js';
|
|
8
8
|
import { mapContactList } from './contact-query.js';
|
|
9
|
-
import { buildEndMessageIdFields, buildMessageBoxRequest, logPreviousMessagesResponse, logRecentMessagesResponse, mapMessageBoxList, } from './message-box-query.js';
|
|
9
|
+
import { buildEndMessageIdFields, buildMessageBoxRequest, logPreviousMessagesResponse, logRecentMessagesResponse, mapMessageBoxList, mergeMessageBoxPages, nextMessageBoxCursor, } from './message-box-query.js';
|
|
10
10
|
import { fetchMessagesByIds } from './message-id-query.js';
|
|
11
|
-
import {
|
|
11
|
+
import { buildGetAllChatMidsRequest, buildGetAllContactIdsRequest, buildGetMessageBoxesRequest, buildMidListRequest, buildMidLookupRequest, buildPreviousMessagesRequest, buildRecentMessagesRequest, } from './requests.js';
|
|
12
12
|
/** Sync reason constant matching CHRLINE reference. */
|
|
13
13
|
const SYNC_REASON = 7;
|
|
14
14
|
/** Sync reason used by getPreviousMessagesV2WithRequest for secondary-device history sync. */
|
|
@@ -122,6 +122,36 @@ export function createTalkQueryClient(runtime) {
|
|
|
122
122
|
hasNext: Boolean(root?.[2]),
|
|
123
123
|
};
|
|
124
124
|
},
|
|
125
|
+
/** Retrieve every message box using the server's min/max chat cursor. */
|
|
126
|
+
async getAllMessageBoxes(options = {}) {
|
|
127
|
+
const pageSize = Math.max(1, options.messageBoxCountLimit ?? 100);
|
|
128
|
+
const pages = [];
|
|
129
|
+
// getMessageBoxes paginates toward older boxes with minChatId. Using
|
|
130
|
+
// maxChatId here makes LINE repeat almost the whole first page (and can
|
|
131
|
+
// make a full inbox look capped at 100 rows).
|
|
132
|
+
let minChatId = options.minChatId;
|
|
133
|
+
const seenCursors = new Set();
|
|
134
|
+
for (;;) {
|
|
135
|
+
const page = await this.getMessageBoxes({
|
|
136
|
+
...options,
|
|
137
|
+
messageBoxCountLimit: pageSize,
|
|
138
|
+
...(minChatId ? { minChatId } : {}),
|
|
139
|
+
});
|
|
140
|
+
pages.push(page);
|
|
141
|
+
const boxes = page.messageBoxes;
|
|
142
|
+
if (!page.hasNext || boxes.length === 0)
|
|
143
|
+
break;
|
|
144
|
+
const next = nextMessageBoxCursor(boxes, minChatId);
|
|
145
|
+
if (!next || seenCursors.has(next))
|
|
146
|
+
break;
|
|
147
|
+
seenCursors.add(next);
|
|
148
|
+
minChatId = next;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
messageBoxes: mergeMessageBoxPages(pages),
|
|
152
|
+
hasNext: false,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
125
155
|
/**
|
|
126
156
|
* Retrieve previous messages through one message-box cursor.
|
|
127
157
|
*
|
|
@@ -191,29 +221,5 @@ export function createTalkQueryClient(runtime) {
|
|
|
191
221
|
logRecentMessagesResponse(chatId, count, raw, messages);
|
|
192
222
|
return messages;
|
|
193
223
|
},
|
|
194
|
-
/**
|
|
195
|
-
* Retrieve binary media content for one LINE message.
|
|
196
|
-
*
|
|
197
|
-
* @param messageId - LINE message identifier.
|
|
198
|
-
* @param requestId - Client request identifier.
|
|
199
|
-
* @returns Message content bytes.
|
|
200
|
-
*/
|
|
201
|
-
async downloadMessageContent(messageId, requestId = `yomi-${Date.now()}`) {
|
|
202
|
-
const result = await runtime.sendTalk('downloadMessageContent', buildDownloadMessageContentRequest(requestId, messageId));
|
|
203
|
-
const content = result.fields?.[0];
|
|
204
|
-
return Buffer.isBuffer(content) ? content : Buffer.from(content || '');
|
|
205
|
-
},
|
|
206
|
-
/**
|
|
207
|
-
* Retrieve binary preview media content for one LINE message.
|
|
208
|
-
*
|
|
209
|
-
* @param messageId - LINE message identifier.
|
|
210
|
-
* @param requestId - Client request identifier.
|
|
211
|
-
* @returns Message preview bytes.
|
|
212
|
-
*/
|
|
213
|
-
async downloadMessageContentPreview(messageId, requestId = `yomi-${Date.now()}`) {
|
|
214
|
-
const result = await runtime.sendTalk('downloadMessageContentPreview', buildDownloadMessageContentRequest(requestId, messageId));
|
|
215
|
-
const content = result.fields?.[0];
|
|
216
|
-
return Buffer.isBuffer(content) ? content : Buffer.from(content || '');
|
|
217
|
-
},
|
|
218
224
|
};
|
|
219
225
|
}
|
|
@@ -148,16 +148,6 @@ export function buildPreviousMessagesRequest(request, endMessageIdFields, syncRe
|
|
|
148
148
|
export function buildRecentMessagesRequest(chatId, count) {
|
|
149
149
|
return [stringField(2, chatId), i32Field(3, count)];
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Build a message-content download request.
|
|
153
|
-
*
|
|
154
|
-
* @param requestId - Client request identifier.
|
|
155
|
-
* @param messageId - LINE message identifier.
|
|
156
|
-
* @returns Thrift request fields.
|
|
157
|
-
*/
|
|
158
|
-
export function buildDownloadMessageContentRequest(requestId, messageId) {
|
|
159
|
-
return [stringField(1, requestId), stringField(2, messageId)];
|
|
160
|
-
}
|
|
161
151
|
/**
|
|
162
152
|
* Build a single-MID lookup request.
|
|
163
153
|
*
|
|
@@ -200,6 +190,15 @@ export function buildSendChatCheckedRequest(chatMid, lastMessageId, sessionId =
|
|
|
200
190
|
byteField(4, sessionId),
|
|
201
191
|
];
|
|
202
192
|
}
|
|
193
|
+
/** Build updateContactSetting for LINE's per-contact notification mute. */
|
|
194
|
+
export function buildUpdateContactNotificationRequest(mid, disabled) {
|
|
195
|
+
return [
|
|
196
|
+
i32Field(1, 0),
|
|
197
|
+
stringField(2, mid),
|
|
198
|
+
i32Field(3, 1),
|
|
199
|
+
stringField(4, disabled ? 'true' : 'false'),
|
|
200
|
+
];
|
|
201
|
+
}
|
|
203
202
|
/**
|
|
204
203
|
* Build the updateChat request fields to rename a group/chat.
|
|
205
204
|
*
|
|
@@ -300,7 +299,7 @@ export function buildCreateChatRequest(name, mids, chatType = 1) {
|
|
|
300
299
|
* Single struct arg at field 1: `{ reqSeq, messageId, reactionType{ predefined } }`.
|
|
301
300
|
* The messageId is a thrift i64 (LINE message ids are 64-bit), so it is passed
|
|
302
301
|
* as a BigInt. `reactionType` is LINE's predefinedReactionType enum:
|
|
303
|
-
* 2 = LIKE 👍, 3 = LOVE ❤️, 4 = LAUGH 😆, 5 = SURPRISE 😮, 6 = SAD 😢, 7 = ANGRY 😡.
|
|
302
|
+
* Protocol emoji mapping: 2 = LIKE 👍, 3 = LOVE ❤️, 4 = LAUGH 😆, 5 = SURPRISE 😮, 6 = SAD 😢, 7 = ANGRY 😡.
|
|
304
303
|
*
|
|
305
304
|
* @param messageId - Target message id (numeric string).
|
|
306
305
|
* @param reactionType - Predefined reaction type (default 2 = LIKE).
|
|
@@ -5,7 +5,7 @@ import { createCliLogger } from '../../util/log.js';
|
|
|
5
5
|
import { recoverBootstrapKeys } from './e2ee/recovery/bootstrap-keys.js';
|
|
6
6
|
import { STATE } from './service.js';
|
|
7
7
|
const TERMINAL_ERROR_REGEX = /LOGGED_OUT|DIVESTED/i;
|
|
8
|
-
const AUTH_ERROR_REGEX = /expired|unauthorized|authentication|invalid.*token/i;
|
|
8
|
+
const AUTH_ERROR_REGEX = /expired|unauthorized|authentication|invalid.*token|access token refresh required/i;
|
|
9
9
|
/**
|
|
10
10
|
* Resolve the active LINE logger for one runtime.
|
|
11
11
|
*
|
|
@@ -58,7 +58,9 @@ async function restoreE2EEState(service, lineLog) {
|
|
|
58
58
|
* @param lineLog - Logger instance.
|
|
59
59
|
*/
|
|
60
60
|
async function restoreBootstrapKeys(service, lineLog) {
|
|
61
|
-
|
|
61
|
+
// Resume is intentionally read-only. Recovered keys are imported into
|
|
62
|
+
// this process but are not written back to the durable Keychain blob.
|
|
63
|
+
const bootstrapKeys = await recoverBootstrapKeys(service, { persist: false });
|
|
62
64
|
if (bootstrapKeys?.length) {
|
|
63
65
|
service.e2eeWarning = false;
|
|
64
66
|
service.emit('e2eeWarning', {
|
|
@@ -89,6 +91,11 @@ async function validateRestoredSession(service, lineLog) {
|
|
|
89
91
|
const profile = await service.client.getProfile();
|
|
90
92
|
if (profile) {
|
|
91
93
|
service.profile = profile;
|
|
94
|
+
// Session resume is a read-only/background path. Persisting profile
|
|
95
|
+
// metadata here rewrites the entire macOS Keychain blob on every
|
|
96
|
+
// desktop polling process and triggers a security permission prompt.
|
|
97
|
+
// Login and explicit account changes own durable profile writes; a
|
|
98
|
+
// resume may use this in-memory profile for the current request only.
|
|
92
99
|
lineLog.info('session.restore.validated', {
|
|
93
100
|
profile: profile.displayName || profile.mid,
|
|
94
101
|
});
|
|
@@ -130,6 +137,14 @@ async function handleSessionValidationError(service, lineLog, validationErr) {
|
|
|
130
137
|
return false;
|
|
131
138
|
}
|
|
132
139
|
if (AUTH_ERROR_REGEX.test(msg)) {
|
|
140
|
+
if (process.env.YOMI_READ_ONLY_SESSION === '1') {
|
|
141
|
+
lineLog.info('auth.refresh.skip', { reason: 'read_only_session' });
|
|
142
|
+
service.loginRequired = true;
|
|
143
|
+
service.loginReason = 'expired';
|
|
144
|
+
service.setState(STATE.DISCONNECTED);
|
|
145
|
+
service.emit('line:loginRequired');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
133
148
|
lineLog.info('session.restore.refresh_attempt');
|
|
134
149
|
const refreshed = await tryRefreshToken(service);
|
|
135
150
|
if (refreshed) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function chatEventParticipantMids(message) {
|
|
2
|
+
if (Number(message?.contentType) !== 18)
|
|
3
|
+
return [];
|
|
4
|
+
const raw = message?.contentMetadata?.LOC_ARGS;
|
|
5
|
+
if (typeof raw !== 'string')
|
|
6
|
+
return [];
|
|
7
|
+
return raw
|
|
8
|
+
.split('\u001e')
|
|
9
|
+
.map((value) => value.trim())
|
|
10
|
+
.filter((value) => value.startsWith('u'));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Interpret LINE's private ChatEvent localization keys observed in real group
|
|
14
|
+
* traffic. Unknown keys remain explicit so new events never masquerade as a
|
|
15
|
+
* decoded action.
|
|
16
|
+
*/
|
|
17
|
+
export function interpretChatEvent(message, names) {
|
|
18
|
+
const key = typeof message?.contentMetadata?.LOC_KEY === 'string'
|
|
19
|
+
? message.contentMetadata.LOC_KEY
|
|
20
|
+
: '';
|
|
21
|
+
const mids = chatEventParticipantMids(message);
|
|
22
|
+
const name = (index) => names.get(mids[index]) || '一位成員';
|
|
23
|
+
switch (key) {
|
|
24
|
+
case 'C_GI':
|
|
25
|
+
return mids.length >= 2
|
|
26
|
+
? `${name(0)} 邀請 ${name(1)} 加入群組`
|
|
27
|
+
: '有人邀請成員加入群組';
|
|
28
|
+
case 'C_MJ':
|
|
29
|
+
return mids.length >= 1 ? `${name(0)} 加入群組` : '有成員加入群組';
|
|
30
|
+
case 'C_ML':
|
|
31
|
+
return mids.length >= 1 ? `${name(0)} 離開群組` : '有成員離開群組';
|
|
32
|
+
default:
|
|
33
|
+
return key ? `[聊天室活動:${key}]` : '[聊天室活動]';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* on a backfill-adapter module that was not extracted). See
|
|
11
11
|
* yomi/README.md for the full extraction rationale.
|
|
12
12
|
*/
|
|
13
|
+
import { isSquareChatMid } from '../client/square-service/client.js';
|
|
13
14
|
import { createMessageCommandService } from './message-command-service.js';
|
|
14
15
|
import { createMessageQueryService } from './message-query-service.js';
|
|
15
16
|
/**
|
|
@@ -38,6 +39,9 @@ export function createChatRuntimeService(service) {
|
|
|
38
39
|
* @returns LINE sendMessage result (includes the sent message id).
|
|
39
40
|
*/
|
|
40
41
|
async sendMessage(to, text, contentMetadata, reply) {
|
|
42
|
+
if (isSquareChatMid(to)) {
|
|
43
|
+
return service.client.sendSquareMessage(to, text, contentMetadata);
|
|
44
|
+
}
|
|
41
45
|
return createMessageCommandService(() => service.client, service.e2eeManager).sendMessage(to, {
|
|
42
46
|
e2ee: true,
|
|
43
47
|
text,
|
|
@@ -154,26 +158,6 @@ export function createChatRuntimeService(service) {
|
|
|
154
158
|
async getPreviousMessages(chatId, count = 50, before = {}) {
|
|
155
159
|
return createMessageQueryService(() => service.client, service.e2eeManager).getPreviousMessages(chatId, count, before);
|
|
156
160
|
},
|
|
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
161
|
/**
|
|
178
162
|
* Mark a LINE conversation read up to a message via the explicit
|
|
179
163
|
* sendChatChecked boundary. When messageId is omitted, resolves the most
|
|
@@ -188,13 +172,24 @@ export function createChatRuntimeService(service) {
|
|
|
188
172
|
async markChatRead(chatId, messageId) {
|
|
189
173
|
let lastMessageId = messageId ?? null;
|
|
190
174
|
if (!lastMessageId) {
|
|
191
|
-
|
|
192
|
-
|
|
175
|
+
if (isSquareChatMid(chatId)) {
|
|
176
|
+
const recent = await service.client.getSquareChatMessages(chatId, 100);
|
|
177
|
+
lastMessageId = recent.at(-1)?.id ? String(recent.at(-1).id) : null;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const recent = await service.getRecentMessages(chatId, 1);
|
|
181
|
+
lastMessageId = recent?.[0]?.id ? String(recent[0].id) : null;
|
|
182
|
+
}
|
|
193
183
|
}
|
|
194
184
|
if (!lastMessageId) {
|
|
195
185
|
return { marked: false, reason: 'no message to mark read' };
|
|
196
186
|
}
|
|
197
|
-
|
|
187
|
+
if (isSquareChatMid(chatId)) {
|
|
188
|
+
await service.client.markSquareChatRead(chatId, lastMessageId);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
await service.client.sendChatChecked(chatId, lastMessageId, 0);
|
|
192
|
+
}
|
|
198
193
|
return { marked: true, chatId, lastMessageId };
|
|
199
194
|
},
|
|
200
195
|
/**
|
package/dist/line/core/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LINE core directory/runtime capability.
|
|
3
3
|
*/
|
|
4
|
-
import { fetchStickerPackageMeta } from '../client/sticker-meta.js';
|
|
4
|
+
import { fetchStickerPackageMeta, localizedStickerTitle, } from '../client/sticker-meta.js';
|
|
5
5
|
import { createContactQueryService } from './contact-query-service.js';
|
|
6
6
|
import { createGroupQueryService } from './group-query-service.js';
|
|
7
7
|
/**
|
|
@@ -46,7 +46,15 @@ export function createDirectoryRuntimeService(service) {
|
|
|
46
46
|
* @returns Owned sticker packages: { packageId, title, version }.
|
|
47
47
|
*/
|
|
48
48
|
async listStickerPackages(language = 'en') {
|
|
49
|
-
|
|
49
|
+
const owned = await service.client.getOwnedStickerPackages(language);
|
|
50
|
+
return Promise.all(owned.map(async (pkg) => {
|
|
51
|
+
const meta = await fetchStickerPackageMeta(String(pkg.packageId)).catch(() => null);
|
|
52
|
+
return {
|
|
53
|
+
...pkg,
|
|
54
|
+
title: localizedStickerTitle(meta?.title, language, pkg.title),
|
|
55
|
+
titles: meta?.title ?? {},
|
|
56
|
+
};
|
|
57
|
+
}));
|
|
50
58
|
},
|
|
51
59
|
/**
|
|
52
60
|
* Search the account's OWNED sticker packages by title substring, and
|
|
@@ -12,7 +12,7 @@ import { decryptKeyChain } from '../index.js';
|
|
|
12
12
|
* @param service - LineProtocolService-like object
|
|
13
13
|
* @returns Recovered key list, or null when bootstrap material is unavailable
|
|
14
14
|
*/
|
|
15
|
-
export async function recoverBootstrapKeys(service) {
|
|
15
|
+
export async function recoverBootstrapKeys(service, options = {}) {
|
|
16
16
|
const bootstrapInput = await readBootstrapRecoveryInput(service);
|
|
17
17
|
if (!bootstrapInput) {
|
|
18
18
|
return null;
|
|
@@ -21,7 +21,13 @@ export async function recoverBootstrapKeys(service) {
|
|
|
21
21
|
if (!keys) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
service.e2eeManager.importKeys(keys);
|
|
25
|
+
if (bootstrapInput.mid) {
|
|
26
|
+
service.e2eeManager.bindSelfKeysToMid(bootstrapInput.mid);
|
|
27
|
+
}
|
|
28
|
+
if (options.persist !== false) {
|
|
29
|
+
await service.sessionState.saveE2EEKeys(keys);
|
|
30
|
+
}
|
|
25
31
|
return keys;
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
@@ -122,20 +128,6 @@ function rebuildBootstrapKeys(input) {
|
|
|
122
128
|
}));
|
|
123
129
|
return Array.isArray(keys) && keys.length > 0 ? keys : null;
|
|
124
130
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Persist recovered keys into the in-memory manager and session state.
|
|
127
|
-
*
|
|
128
|
-
* @param service - LineProtocolService-like object.
|
|
129
|
-
* @param keys - Recovered key list.
|
|
130
|
-
* @param mid - Optional self MID.
|
|
131
|
-
*/
|
|
132
|
-
async function persistRecoveredKeys(service, keys, mid) {
|
|
133
|
-
service.e2eeManager.importKeys(keys);
|
|
134
|
-
if (mid) {
|
|
135
|
-
service.e2eeManager.bindSelfKeysToMid(mid);
|
|
136
|
-
}
|
|
137
|
-
await service.sessionState.saveE2EEKeys(keys);
|
|
138
|
-
}
|
|
139
131
|
/**
|
|
140
132
|
* Decode a base64-encoded string into a Buffer when possible.
|
|
141
133
|
*
|