@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.
Files changed (86) hide show
  1. package/README.md +46 -4
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +101 -0
  13. package/dist/auth/credential-store.js +47 -17
  14. package/dist/auth/keychain.js +7 -54
  15. package/dist/cli/query.js +875 -0
  16. package/dist/cli/work-item.js +24 -0
  17. package/dist/insight/relationships.js +1 -1
  18. package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
  19. package/dist/line/auth/pwless/index.js +1 -1
  20. package/dist/line/client/auth-service/client.js +7 -8
  21. package/dist/line/client/buddy-service/client.js +73 -0
  22. package/dist/line/client/index.js +28 -3
  23. package/dist/line/client/obs-media-client.js +38 -0
  24. package/dist/line/client/parsers.js +1 -0
  25. package/dist/line/client/square-service/client.js +298 -0
  26. package/dist/line/client/sticker-meta.js +32 -0
  27. package/dist/line/client/sync-service/client.js +31 -3
  28. package/dist/line/client/talk-service/command-client.js +9 -1
  29. package/dist/line/client/talk-service/contact-query.js +8 -1
  30. package/dist/line/client/talk-service/message-box-query.js +17 -0
  31. package/dist/line/client/talk-service/message-id-query.js +27 -3
  32. package/dist/line/client/talk-service/query-client.js +32 -26
  33. package/dist/line/client/talk-service/requests.js +10 -11
  34. package/dist/line/core/auth-session-runtime.js +17 -2
  35. package/dist/line/core/chat-event.js +35 -0
  36. package/dist/line/core/chat-runtime-service.js +18 -23
  37. package/dist/line/core/config.js +1 -0
  38. package/dist/line/core/directory-runtime-service.js +10 -2
  39. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
  40. package/dist/line/core/flex-message.js +453 -0
  41. package/dist/line/core/mention.js +1 -1
  42. package/dist/line/core/message-preview.js +60 -0
  43. package/dist/line/core/message-query-service.js +0 -20
  44. package/dist/line/core/service.js +1 -3
  45. package/dist/mcp/handlers/contacts.js +12 -6
  46. package/dist/mcp/handlers/conversations.js +91 -4
  47. package/dist/mcp/handlers/login.js +1 -1
  48. package/dist/mcp/handlers/message-actions.js +1 -1
  49. package/dist/mcp/handlers/search.js +1 -1
  50. package/dist/mcp/handlers/send.js +8 -11
  51. package/dist/mcp/handlers/shared.js +1 -1
  52. package/dist/mcp/integrator.js +168 -0
  53. package/dist/mcp/media.js +135 -4
  54. package/dist/mcp/names.js +78 -6
  55. package/dist/mcp/phi-guard.js +2 -2
  56. package/dist/search/bigram.js +2 -2
  57. package/dist/search/collector.js +1 -1
  58. package/dist/search/default-embedder.js +12 -3
  59. package/dist/search/embedder.js +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/work-item/adapter.js +1 -0
  62. package/dist/work-item/destination-contracts.js +1 -0
  63. package/dist/work-item/event-context.js +73 -0
  64. package/dist/work-item/gold-contract.js +101 -0
  65. package/dist/work-item/gold-coverage.js +96 -0
  66. package/dist/work-item/gold-derivation.js +67 -0
  67. package/dist/work-item/gold-split.js +143 -0
  68. package/dist/work-item/index.js +23 -0
  69. package/dist/work-item/line-adapter.js +111 -0
  70. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  71. package/dist/work-item/local-provider.js +38 -0
  72. package/dist/work-item/local-work-event-provider.js +56 -0
  73. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  74. package/dist/work-item/provider.js +21 -0
  75. package/dist/work-item/registry.js +19 -0
  76. package/dist/work-item/service.js +85 -0
  77. package/dist/work-item/store.js +97 -0
  78. package/dist/work-item/types.js +79 -0
  79. package/dist/work-item/work-event-graph-engine.js +63 -0
  80. package/dist/work-item/work-event-inference.js +267 -0
  81. package/dist/work-item/work-event-matcher.js +105 -0
  82. package/dist/work-item/work-event-model-manifest.js +39 -0
  83. package/dist/work-item/work-event-store.js +125 -0
  84. package/dist/work-item/work-event.js +123 -0
  85. package/package.json +26 -2
  86. 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
- runtime.revision =
110
- typeof nextRevision === 'bigint' ? Number(nextRevision) : nextRevision;
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
- return value || null;
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 { buildDownloadMessageContentRequest, buildGetAllChatMidsRequest, buildGetAllContactIdsRequest, buildGetMessageBoxesRequest, buildMidListRequest, buildMidLookupRequest, buildPreviousMessagesRequest, buildRecentMessagesRequest, } from './requests.js';
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
- const bootstrapKeys = await recoverBootstrapKeys(service);
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
- const recent = await service.getRecentMessages(chatId, 1);
192
- lastMessageId = recent?.[0]?.id ? String(recent[0].id) : null;
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
- await service.client.sendChatChecked(chatId, lastMessageId, 0);
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
  /**
@@ -50,6 +50,7 @@ export const LINE_APP_CONFIG = {
50
50
  host: 'ga2.line.naver.jp',
51
51
  // Service paths
52
52
  talkPath: '/S4',
53
+ squarePath: '/SQ1',
53
54
  syncPath: '/SYNC4',
54
55
  authPath: '/AS4',
55
56
  revokePath: '/RS4',
@@ -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
- return service.client.getOwnedStickerPackages(language);
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
- await persistRecoveredKeys(service, keys, bootstrapInput.mid);
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
  *