@rikaidev/yomi 0.5.0 → 0.6.1
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 +28 -0
- 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 +22 -16
- package/dist/auth/credential-store.js +25 -12
- package/dist/cli/query.js +276 -69
- package/dist/cli/work-item.js +24 -0
- 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 -1
- package/dist/line/client/obs-media-client.js +2 -1
- package/dist/line/client/square-service/client.js +298 -0
- package/dist/line/client/talk-service/command-client.js +9 -1
- package/dist/line/client/talk-service/message-id-query.js +27 -3
- package/dist/line/client/talk-service/requests.js +9 -0
- package/dist/line/core/auth-session-runtime.js +1 -1
- package/dist/line/core/chat-event.js +2 -0
- package/dist/line/core/chat-runtime-service.js +18 -3
- package/dist/line/core/config.js +1 -0
- package/dist/line/core/flex-message.js +323 -0
- package/dist/line/core/service.js +0 -1
- package/dist/mcp/handlers/contacts.js +12 -6
- package/dist/mcp/handlers/conversations.js +54 -0
- package/dist/mcp/handlers/send.js +8 -11
- package/dist/mcp/integrator.js +21 -7
- package/dist/mcp/media.js +101 -2
- package/dist/mcp/names.js +78 -6
- package/dist/search/default-embedder.js +12 -3
- 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 +25 -4
- package/run.mjs +186 -9
|
@@ -20,11 +20,14 @@ import { createCliLogger } from '../../util/log.js';
|
|
|
20
20
|
import { LINE_APP_CONFIG } from '../core/config.js';
|
|
21
21
|
import { encodeCallMessage } from '../core/thrift/index.js';
|
|
22
22
|
import { createAuthClient } from './auth-service/client.js';
|
|
23
|
+
import { createBuddyClient } from './buddy-service/client.js';
|
|
23
24
|
import { createE2EEClient } from './e2ee-service/client.js';
|
|
24
25
|
import { parseOperation } from './parsers.js';
|
|
25
26
|
import { createShopClient } from './shop-service/client.js';
|
|
27
|
+
import { createSquareClient } from './square-service/client.js';
|
|
26
28
|
import { createSyncClient } from './sync-service/client.js';
|
|
27
29
|
import { createTalkCommandClient } from './talk-service/command-client.js';
|
|
30
|
+
import { decodeBinaryResponseMessage, encodeBinaryCallMessage, postBinary, } from './talk-service/message-id-query.js';
|
|
28
31
|
import { createTalkQueryClient } from './talk-service/query-client.js';
|
|
29
32
|
import { sendRequest } from './transport.js';
|
|
30
33
|
const TALK_EXCEPTION_CODE_NAMES = {
|
|
@@ -130,7 +133,15 @@ export class LineClient extends EventEmitter {
|
|
|
130
133
|
restoreE2EEKeyBackup;
|
|
131
134
|
logoutZ;
|
|
132
135
|
getOwnedStickerPackages;
|
|
136
|
+
getJoinedSquareChats;
|
|
137
|
+
getSquareChatMessages;
|
|
138
|
+
getSquareChatStatus;
|
|
139
|
+
getSquareChatLatestMessage;
|
|
140
|
+
markSquareChatRead;
|
|
141
|
+
sendSquareMessage;
|
|
142
|
+
getBuddyRichMenu;
|
|
133
143
|
updateChatName;
|
|
144
|
+
updateContactNotificationDisabled;
|
|
134
145
|
inviteIntoChat;
|
|
135
146
|
deleteOtherFromChat;
|
|
136
147
|
deleteSelfFromChat;
|
|
@@ -155,7 +166,7 @@ export class LineClient extends EventEmitter {
|
|
|
155
166
|
this.seq = 1;
|
|
156
167
|
this.logger = config.logger || createCliLogger('LINE');
|
|
157
168
|
this.startupFlowLogger = config.startupFlowLogger || null;
|
|
158
|
-
Object.assign(this, createAuthClient(this), createTalkQueryClient(this), createTalkCommandClient(this), createSyncClient(this), createE2EEClient(this), createShopClient(this));
|
|
169
|
+
Object.assign(this, createAuthClient(this), createBuddyClient(this), createTalkQueryClient(this), createTalkCommandClient(this), createSyncClient(this), createE2EEClient(this), createShopClient(this), createSquareClient(this));
|
|
159
170
|
}
|
|
160
171
|
/**
|
|
161
172
|
* Send a TCompact Thrift call to an arbitrary LINE service endpoint path.
|
|
@@ -199,6 +210,22 @@ export class LineClient extends EventEmitter {
|
|
|
199
210
|
async sendTalk(method, args) {
|
|
200
211
|
return this.sendCompact(LINE_APP_CONFIG.talkPath, method, args);
|
|
201
212
|
}
|
|
213
|
+
async sendBinary(path, method, args) {
|
|
214
|
+
const body = encodeBinaryCallMessage(method, this.seq++, args);
|
|
215
|
+
const response = await postBinary(this.host, path, body, this.authToken);
|
|
216
|
+
if (response.nextToken) {
|
|
217
|
+
this.authToken = response.nextToken;
|
|
218
|
+
this.emit('tokenRotated', response.nextToken);
|
|
219
|
+
}
|
|
220
|
+
if (!response.body.length) {
|
|
221
|
+
throw new Error(`${method}(${path}) returned an empty response`);
|
|
222
|
+
}
|
|
223
|
+
const decoded = decodeBinaryResponseMessage(response.body);
|
|
224
|
+
if (decoded.fields?.[1]) {
|
|
225
|
+
throw new Error(`${method}(${path}) failed: ${decoded.fields[1]?.[2] || JSON.stringify(decoded.fields[1])}`);
|
|
226
|
+
}
|
|
227
|
+
return decoded;
|
|
228
|
+
}
|
|
202
229
|
/**
|
|
203
230
|
* Parse a raw Thrift operation struct into a domain object.
|
|
204
231
|
*
|
|
@@ -27,7 +27,8 @@ export async function downloadLineMessageData(client, options) {
|
|
|
27
27
|
throw new Error('LINE legacy media download requires a message id');
|
|
28
28
|
}
|
|
29
29
|
const suffix = options.preview ? '/preview' : '';
|
|
30
|
-
const
|
|
30
|
+
const namespace = options.square ? 'g2' : 'talk';
|
|
31
|
+
const requestUrl = `https://${OBS_HOST}/r/${namespace}/m/${encodeURIComponent(messageId)}${suffix}`;
|
|
31
32
|
const response = await fetch(requestUrl, {
|
|
32
33
|
method: 'GET',
|
|
33
34
|
headers: {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only LINE OpenChat (SquareService) capability.
|
|
3
|
+
*
|
|
4
|
+
* OpenChat is a separate service from TalkService: joined chats use `t...`
|
|
5
|
+
* MIDs and never appear in TalkService message boxes. Keep the protocol
|
|
6
|
+
* boundary explicit so callers cannot accidentally treat Square messages as
|
|
7
|
+
* ordinary group messages.
|
|
8
|
+
*/
|
|
9
|
+
import { LINE_APP_CONFIG } from '../../core/config.js';
|
|
10
|
+
import { i32Field, i64Field, mapField, setField, stringField, structField, } from '../../core/thrift/index.js';
|
|
11
|
+
function mapEntries(value) {
|
|
12
|
+
if (value instanceof Map)
|
|
13
|
+
return Array.from(value.entries());
|
|
14
|
+
if (value && typeof value === 'object')
|
|
15
|
+
return Object.entries(value);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
/** LINE currently emits `m...` SquareChat MIDs; older schemas used `t...`. */
|
|
19
|
+
export function isSquareChatMid(value) {
|
|
20
|
+
return value.startsWith('m') || value.startsWith('t');
|
|
21
|
+
}
|
|
22
|
+
function normalizeJoinedChat(chat, status, member) {
|
|
23
|
+
const id = String(chat?.[1] || '');
|
|
24
|
+
if (!isSquareChatMid(id))
|
|
25
|
+
return null;
|
|
26
|
+
const lastSquareMessage = status?.[3];
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
squareMid: chat?.[2] ? String(chat[2]) : null,
|
|
30
|
+
memberMid: member?.[1] ? String(member[1]) : null,
|
|
31
|
+
name: String(chat?.[4] || 'LINE 社群'),
|
|
32
|
+
pictureHash: chat?.[5] ? String(chat[5]) : null,
|
|
33
|
+
unreadCount: Number(status?.[5]?.[2] || 0),
|
|
34
|
+
lastMessage: lastSquareMessage?.[1] || null,
|
|
35
|
+
lastSenderName: status?.[4] ? String(status[4]) : null,
|
|
36
|
+
isMuted: member?.[5] === false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function eventMessage(event) {
|
|
40
|
+
const payload = event?.[4];
|
|
41
|
+
const received = payload?.[1];
|
|
42
|
+
const sent = payload?.[2];
|
|
43
|
+
const mutated = payload?.[41];
|
|
44
|
+
const notification = payload?.[30];
|
|
45
|
+
const envelope = received || sent || mutated || notification;
|
|
46
|
+
const squareMessage = envelope?.[2];
|
|
47
|
+
const message = squareMessage?.[1];
|
|
48
|
+
if (!message)
|
|
49
|
+
return null;
|
|
50
|
+
const chatId = String(envelope?.[1] || message?.[2] || '');
|
|
51
|
+
const id = String(message?.[4] || '');
|
|
52
|
+
if (!chatId || !id)
|
|
53
|
+
return null;
|
|
54
|
+
const fromMid = String(message?.[1] || '');
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
chatId,
|
|
58
|
+
fromMid,
|
|
59
|
+
fromName: fromMid ? `成員 ${fromMid.slice(-4)}` : '匿名成員',
|
|
60
|
+
fromPictureUrl: null,
|
|
61
|
+
isSelf: Boolean(sent),
|
|
62
|
+
text: typeof message?.[10] === 'string' ? message[10] : '',
|
|
63
|
+
createdTime: Number(message?.[6] || message?.[5] || event?.[2] || 0),
|
|
64
|
+
contentType: Number(message?.[15] || 0),
|
|
65
|
+
contentMetadata: message?.[18] && typeof message[18] === 'object' ? message[18] : {},
|
|
66
|
+
threadMid: squareMessage?.[6]?.[1]
|
|
67
|
+
? String(squareMessage[6][1])
|
|
68
|
+
: envelope?.[7]
|
|
69
|
+
? String(envelope[7])
|
|
70
|
+
: null,
|
|
71
|
+
isThreadRoot: Boolean(squareMessage?.[6]?.[2]),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function createSquareClient(runtime) {
|
|
75
|
+
const memberProfiles = new Map();
|
|
76
|
+
async function resolveMember(squareMemberMid) {
|
|
77
|
+
const cached = memberProfiles.get(squareMemberMid);
|
|
78
|
+
if (cached)
|
|
79
|
+
return cached;
|
|
80
|
+
try {
|
|
81
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareMember', [structField(1, [stringField(1, squareMemberMid)])]);
|
|
82
|
+
const member = result.fields?.[0]?.[1];
|
|
83
|
+
const displayName = String(member?.[3] || '').trim();
|
|
84
|
+
const pictureHash = String(member?.[4] || '').trim();
|
|
85
|
+
if (displayName) {
|
|
86
|
+
const profile = {
|
|
87
|
+
displayName,
|
|
88
|
+
pictureUrl: pictureHash
|
|
89
|
+
? `https://obs.line-scdn.net/${pictureHash.replace(/^\/+/, '')}`
|
|
90
|
+
: null,
|
|
91
|
+
};
|
|
92
|
+
memberProfiles.set(squareMemberMid, profile);
|
|
93
|
+
return profile;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (process.env.YOMI_DEBUG_SQUARE_MEMBER === '1') {
|
|
98
|
+
console.error('[square] getSquareMember failed', error instanceof Error ? error.message : String(error));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async function resolveMembers(squareMemberMids) {
|
|
104
|
+
const unresolved = squareMemberMids.filter((mid) => !memberProfiles.has(mid));
|
|
105
|
+
if (unresolved.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareMembers', [structField(1, [setField(2, 11, unresolved)])]);
|
|
109
|
+
const raw = result.fields?.[0]?.[1];
|
|
110
|
+
const candidates = raw && typeof raw?.[1] === 'string'
|
|
111
|
+
? [raw]
|
|
112
|
+
: mapEntries(raw).map(([, member]) => member);
|
|
113
|
+
for (const member of candidates) {
|
|
114
|
+
const mid = String(member?.[1] || '');
|
|
115
|
+
const displayName = String(member?.[3] || '').trim();
|
|
116
|
+
const pictureHash = String(member?.[4] || '').trim();
|
|
117
|
+
if (!mid || !displayName)
|
|
118
|
+
continue;
|
|
119
|
+
memberProfiles.set(mid, {
|
|
120
|
+
displayName,
|
|
121
|
+
pictureUrl: pictureHash
|
|
122
|
+
? `https://obs.line-scdn.net/${pictureHash.replace(/^\/+/, '')}`
|
|
123
|
+
: null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (process.env.YOMI_DEBUG_SQUARE_MEMBER === '1') {
|
|
129
|
+
console.error('[square] getSquareMembers failed', error instanceof Error ? error.message : String(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
async getJoinedSquareChats(limit = 100) {
|
|
135
|
+
const chats = [];
|
|
136
|
+
let continuationToken;
|
|
137
|
+
const seenTokens = [];
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const request = [i32Field(3, Math.max(1, Math.min(limit, 100)))];
|
|
141
|
+
if (continuationToken)
|
|
142
|
+
request.unshift(stringField(2, continuationToken));
|
|
143
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getJoinedSquareChats', [structField(1, request)]);
|
|
144
|
+
const response = result.fields?.[0];
|
|
145
|
+
const statuses = mapEntries(response?.[3]);
|
|
146
|
+
const members = mapEntries(response?.[2]);
|
|
147
|
+
for (const raw of Array.isArray(response?.[1]) ? response[1] : []) {
|
|
148
|
+
const id = String(raw?.[1] || '');
|
|
149
|
+
const chat = normalizeJoinedChat(raw, statuses.find(([key]) => key === id)?.[1], members.find(([key]) => key === id)?.[1]);
|
|
150
|
+
if (chat)
|
|
151
|
+
chats.push(chat);
|
|
152
|
+
}
|
|
153
|
+
const next = response?.[4] ? String(response[4]) : '';
|
|
154
|
+
if (!next || seenTokens.includes(next) || chats.length >= limit)
|
|
155
|
+
break;
|
|
156
|
+
seenTokens.push(next);
|
|
157
|
+
continuationToken = next;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// LINE's Desktop identity currently answers getJoinedSquareChats with
|
|
162
|
+
// `501 NO XLT`. A bootstrap fetchMyEvents(subscriptionId=0) returns
|
|
163
|
+
// the authoritative CREATE_SQUARE_CHAT_MEMBER snapshots instead.
|
|
164
|
+
chats.length = 0;
|
|
165
|
+
let eventContinuation;
|
|
166
|
+
const seenEventTokens = [];
|
|
167
|
+
for (;;) {
|
|
168
|
+
const request = [i64Field(1, 0), i32Field(3, 100)];
|
|
169
|
+
if (eventContinuation)
|
|
170
|
+
request.push(stringField(4, eventContinuation));
|
|
171
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'fetchMyEvents', [structField(1, request)]);
|
|
172
|
+
const response = result.fields?.[0];
|
|
173
|
+
for (const event of Array.isArray(response?.[2]) ? response[2] : []) {
|
|
174
|
+
const createdMembership = event?.[4]?.[17];
|
|
175
|
+
const chat = normalizeJoinedChat(createdMembership?.[1], createdMembership?.[2], createdMembership?.[3]);
|
|
176
|
+
if (chat)
|
|
177
|
+
chats.push(chat);
|
|
178
|
+
}
|
|
179
|
+
const next = response?.[4] ? String(response[4]) : '';
|
|
180
|
+
if (!next || seenEventTokens.includes(next) || chats.length >= limit)
|
|
181
|
+
break;
|
|
182
|
+
seenEventTokens.push(next);
|
|
183
|
+
eventContinuation = next;
|
|
184
|
+
}
|
|
185
|
+
if (chats.length === 0)
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
return chats
|
|
189
|
+
.filter((chat, index) => chats.findIndex((item) => item.id === chat.id) === index)
|
|
190
|
+
.slice(0, limit);
|
|
191
|
+
},
|
|
192
|
+
async getSquareChatMessages(chatId, limit = 40, threadMid) {
|
|
193
|
+
if (!isSquareChatMid(chatId))
|
|
194
|
+
throw new Error('Invalid OpenChat MID');
|
|
195
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'fetchSquareChatEvents', [
|
|
196
|
+
structField(1, [
|
|
197
|
+
i64Field(1, 0),
|
|
198
|
+
stringField(2, chatId),
|
|
199
|
+
i32Field(4, Math.max(1, Math.min(limit, 100))),
|
|
200
|
+
i32Field(5, 1),
|
|
201
|
+
i32Field(6, 1),
|
|
202
|
+
i32Field(8, 1),
|
|
203
|
+
...(threadMid ? [stringField(9, threadMid)] : []),
|
|
204
|
+
]),
|
|
205
|
+
]);
|
|
206
|
+
const events = result.fields?.[0]?.[2];
|
|
207
|
+
const messages = [];
|
|
208
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
209
|
+
const message = eventMessage(event);
|
|
210
|
+
if (!message || message.chatId !== chatId)
|
|
211
|
+
continue;
|
|
212
|
+
const existingIndex = messages.findIndex((item) => item.id === message.id);
|
|
213
|
+
if (existingIndex >= 0)
|
|
214
|
+
messages[existingIndex] = message;
|
|
215
|
+
else
|
|
216
|
+
messages.push(message);
|
|
217
|
+
}
|
|
218
|
+
const senderMids = [
|
|
219
|
+
...new Set(messages.map((message) => message.fromMid).filter(Boolean)),
|
|
220
|
+
];
|
|
221
|
+
await resolveMembers(senderMids);
|
|
222
|
+
await Promise.all(senderMids.map(async (mid) => {
|
|
223
|
+
const profile = await resolveMember(mid);
|
|
224
|
+
if (!profile)
|
|
225
|
+
return;
|
|
226
|
+
for (const message of messages) {
|
|
227
|
+
if (message.fromMid !== mid)
|
|
228
|
+
continue;
|
|
229
|
+
message.fromName = profile.displayName;
|
|
230
|
+
message.fromPictureUrl = profile.pictureUrl;
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
return messages
|
|
234
|
+
.sort((a, b) => a.createdTime - b.createdTime)
|
|
235
|
+
.slice(-limit);
|
|
236
|
+
},
|
|
237
|
+
async getSquareChatStatus(chatId) {
|
|
238
|
+
if (!isSquareChatMid(chatId))
|
|
239
|
+
throw new Error('Invalid OpenChat MID');
|
|
240
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareChatStatus', [structField(1, [stringField(2, chatId)])]);
|
|
241
|
+
return result.fields?.[0]?.[1] || null;
|
|
242
|
+
},
|
|
243
|
+
async getSquareChatLatestMessage(chatId) {
|
|
244
|
+
if (!isSquareChatMid(chatId))
|
|
245
|
+
throw new Error('Invalid OpenChat MID');
|
|
246
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'getSquareChatStatus', [structField(1, [stringField(2, chatId)])]);
|
|
247
|
+
const status = result.fields?.[0]?.[1];
|
|
248
|
+
const raw = status?.[3]?.[1];
|
|
249
|
+
if (!raw?.[4])
|
|
250
|
+
return null;
|
|
251
|
+
const fromMid = String(raw?.[1] || '');
|
|
252
|
+
const profile = fromMid ? await resolveMember(fromMid) : null;
|
|
253
|
+
return {
|
|
254
|
+
id: String(raw[4]),
|
|
255
|
+
chatId,
|
|
256
|
+
fromMid,
|
|
257
|
+
fromName: profile?.displayName || String(status?.[4] || '') || '匿名成員',
|
|
258
|
+
fromPictureUrl: profile?.pictureUrl || null,
|
|
259
|
+
isSelf: false,
|
|
260
|
+
text: typeof raw?.[10] === 'string' ? raw[10] : '',
|
|
261
|
+
createdTime: Number(raw?.[6] || raw?.[5] || 0),
|
|
262
|
+
contentType: Number(raw?.[15] || 0),
|
|
263
|
+
contentMetadata: raw?.[18] && typeof raw[18] === 'object' ? raw[18] : {},
|
|
264
|
+
threadMid: null,
|
|
265
|
+
isThreadRoot: false,
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
async markSquareChatRead(chatId, messageId) {
|
|
269
|
+
if (!isSquareChatMid(chatId))
|
|
270
|
+
throw new Error('Invalid OpenChat MID');
|
|
271
|
+
if (!messageId)
|
|
272
|
+
throw new Error('OpenChat message id required');
|
|
273
|
+
await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'markAsRead', [
|
|
274
|
+
structField(1, [stringField(2, chatId), stringField(4, messageId)]),
|
|
275
|
+
]);
|
|
276
|
+
},
|
|
277
|
+
async sendSquareMessage(chatId, text, contentMetadata = {}) {
|
|
278
|
+
if (!isSquareChatMid(chatId))
|
|
279
|
+
throw new Error('Invalid OpenChat MID');
|
|
280
|
+
const messageFields = [
|
|
281
|
+
stringField(2, chatId),
|
|
282
|
+
stringField(10, text),
|
|
283
|
+
i32Field(15, 0),
|
|
284
|
+
];
|
|
285
|
+
if (Object.keys(contentMetadata).length > 0) {
|
|
286
|
+
messageFields.push(mapField(18, 11, 11, contentMetadata));
|
|
287
|
+
}
|
|
288
|
+
const result = await runtime.sendCompact(LINE_APP_CONFIG.squarePath, 'sendMessage', [
|
|
289
|
+
structField(1, [
|
|
290
|
+
i32Field(1, runtime.seq++),
|
|
291
|
+
stringField(2, chatId),
|
|
292
|
+
structField(3, [structField(1, messageFields), i64Field(4, 4)]),
|
|
293
|
+
]),
|
|
294
|
+
]);
|
|
295
|
+
return result.fields?.[0]?.[1] || null;
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -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.
|
|
@@ -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: {
|
|
@@ -190,6 +190,15 @@ export function buildSendChatCheckedRequest(chatMid, lastMessageId, sessionId =
|
|
|
190
190
|
byteField(4, sessionId),
|
|
191
191
|
];
|
|
192
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
|
+
}
|
|
193
202
|
/**
|
|
194
203
|
* Build the updateChat request fields to rename a group/chat.
|
|
195
204
|
*
|
|
@@ -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
|
*
|
|
@@ -27,6 +27,8 @@ export function interpretChatEvent(message, names) {
|
|
|
27
27
|
: '有人邀請成員加入群組';
|
|
28
28
|
case 'C_MJ':
|
|
29
29
|
return mids.length >= 1 ? `${name(0)} 加入群組` : '有成員加入群組';
|
|
30
|
+
case 'C_ML':
|
|
31
|
+
return mids.length >= 1 ? `${name(0)} 離開群組` : '有成員離開群組';
|
|
30
32
|
default:
|
|
31
33
|
return key ? `[聊天室活動:${key}]` : '[聊天室活動]';
|
|
32
34
|
}
|
|
@@ -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,
|
|
@@ -168,13 +172,24 @@ export function createChatRuntimeService(service) {
|
|
|
168
172
|
async markChatRead(chatId, messageId) {
|
|
169
173
|
let lastMessageId = messageId ?? null;
|
|
170
174
|
if (!lastMessageId) {
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
183
|
}
|
|
174
184
|
if (!lastMessageId) {
|
|
175
185
|
return { marked: false, reason: 'no message to mark read' };
|
|
176
186
|
}
|
|
177
|
-
|
|
187
|
+
if (isSquareChatMid(chatId)) {
|
|
188
|
+
await service.client.markSquareChatRead(chatId, lastMessageId);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
await service.client.sendChatChecked(chatId, lastMessageId, 0);
|
|
192
|
+
}
|
|
178
193
|
return { marked: true, chatId, lastMessageId };
|
|
179
194
|
},
|
|
180
195
|
/**
|