agent-messenger 2.36.1 → 2.37.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/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +5 -4
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.js +15 -9
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +33 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +377 -42
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +4 -3
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +2 -2
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +13 -2
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +50 -2
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +18 -1
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +7 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +29 -1
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-kakaotalk/references/common-patterns.md +36 -0
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/kakaotalk/chat-classifier.test.ts +23 -1
- package/src/platforms/kakaotalk/chat-classifier.ts +16 -10
- package/src/platforms/kakaotalk/client.test.ts +1225 -88
- package/src/platforms/kakaotalk/client.ts +476 -45
- package/src/platforms/kakaotalk/index.test.ts +39 -0
- package/src/platforms/kakaotalk/index.ts +12 -1
- package/src/platforms/kakaotalk/protocol/connection.test.ts +87 -0
- package/src/platforms/kakaotalk/protocol/connection.ts +15 -2
- package/src/platforms/kakaotalk/protocol/login-response.test.ts +13 -2
- package/src/platforms/kakaotalk/protocol/login-response.ts +1 -1
- package/src/platforms/kakaotalk/types.ts +41 -2
|
@@ -5,20 +5,40 @@ import { Long } from 'bson';
|
|
|
5
5
|
import { getConfigDir } from '../../shared/utils/config-dir.js';
|
|
6
6
|
import { warn } from '../../shared/utils/stderr.js';
|
|
7
7
|
import { planAttachments } from './attachment-router.js';
|
|
8
|
+
import { isOpenKakaoChatType } from './chat-classifier.js';
|
|
8
9
|
import { detectImageDimensions } from './image-meta.js';
|
|
9
10
|
import { sha1Hex } from './media-upload.js';
|
|
10
11
|
import { LANG, PC_OS_NAME, getLocoDeviceConfig } from './protocol/config.js';
|
|
12
|
+
import { isSyntheticConnectionClose } from './protocol/login-response.js';
|
|
11
13
|
import { uploadMediaToLoco, uploadMultiMediaEntry } from './protocol/media-uploader.js';
|
|
12
14
|
import { LocoSession } from './protocol/session.js';
|
|
13
15
|
import { KAKAO_MESSAGE_TYPE, } from './types.js';
|
|
14
16
|
export class KakaoTalkError extends Error {
|
|
15
17
|
code;
|
|
16
18
|
serverStatus;
|
|
19
|
+
responseFailureKind;
|
|
20
|
+
responseStatusSource;
|
|
21
|
+
getChatFailureReason;
|
|
17
22
|
constructor(message, code, options) {
|
|
18
23
|
super(message, options);
|
|
19
24
|
this.name = 'KakaoTalkError';
|
|
20
25
|
this.code = code;
|
|
21
26
|
this.serverStatus = options?.serverStatus;
|
|
27
|
+
this.responseFailureKind = options?.responseFailureKind;
|
|
28
|
+
this.responseStatusSource = options?.responseStatusSource;
|
|
29
|
+
this.getChatFailureReason = options?.getChatFailureReason;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class LocoResponseError extends Error {
|
|
33
|
+
serverStatus;
|
|
34
|
+
responseFailureKind;
|
|
35
|
+
responseStatusSource;
|
|
36
|
+
constructor(message, responseFailureKind, options) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'LocoResponseError';
|
|
39
|
+
this.serverStatus = options?.serverStatus;
|
|
40
|
+
this.responseFailureKind = responseFailureKind;
|
|
41
|
+
this.responseStatusSource = options?.responseStatusSource;
|
|
22
42
|
}
|
|
23
43
|
}
|
|
24
44
|
class MemberNameCache {
|
|
@@ -141,7 +161,7 @@ function formatChat(chat, title, nameCache) {
|
|
|
141
161
|
const chatId = longToString(chat.c);
|
|
142
162
|
return {
|
|
143
163
|
chat_id: chatId,
|
|
144
|
-
type: chat.t,
|
|
164
|
+
type: requiredChatType(chat.t),
|
|
145
165
|
display_name: displayName,
|
|
146
166
|
title,
|
|
147
167
|
active_members: chat.a,
|
|
@@ -166,6 +186,64 @@ function extractTitle(body) {
|
|
|
166
186
|
const content = titleMeta?.content;
|
|
167
187
|
return typeof content === 'string' && content.length > 0 ? content : null;
|
|
168
188
|
}
|
|
189
|
+
function extractChannelInfo(body, expectedChatId) {
|
|
190
|
+
const info = body.chatInfo;
|
|
191
|
+
if (!info || typeof info !== 'object') {
|
|
192
|
+
throw new Error('CHATINFO response missing chatInfo');
|
|
193
|
+
}
|
|
194
|
+
const responseChatId = longToString(info.chatId);
|
|
195
|
+
if (responseChatId !== expectedChatId) {
|
|
196
|
+
throw new Error('CHATINFO response chatId mismatch');
|
|
197
|
+
}
|
|
198
|
+
return info;
|
|
199
|
+
}
|
|
200
|
+
function requiredNonNegativeInteger(value, field) {
|
|
201
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
202
|
+
throw new Error(`CHATINFO response invalid ${field}`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function requiredChatType(value) {
|
|
207
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
return requiredNonNegativeInteger(value, 'type');
|
|
211
|
+
}
|
|
212
|
+
function channelInfoToChatData(body, expectedChatId) {
|
|
213
|
+
const info = extractChannelInfo(body, expectedChatId);
|
|
214
|
+
const type = requiredChatType(info.type);
|
|
215
|
+
const activeMembers = requiredNonNegativeInteger(info.activeMembersCount, 'activeMembersCount');
|
|
216
|
+
const unreadCount = info.invalidNewMessageCount === true ? 0 : requiredNonNegativeInteger(info.newMessageCount, 'newMessageCount');
|
|
217
|
+
const displayMembers = Array.isArray(info.displayMembers) ? info.displayMembers : [];
|
|
218
|
+
const memberIds = [];
|
|
219
|
+
const memberNames = [];
|
|
220
|
+
for (const member of displayMembers) {
|
|
221
|
+
if (!member || typeof member !== 'object')
|
|
222
|
+
continue;
|
|
223
|
+
if (member.userId === undefined || typeof member.nickName !== 'string')
|
|
224
|
+
continue;
|
|
225
|
+
memberIds.push(member.userId);
|
|
226
|
+
memberNames.push(member.nickName);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
c: info.chatId,
|
|
230
|
+
t: type,
|
|
231
|
+
a: activeMembers,
|
|
232
|
+
n: unreadCount,
|
|
233
|
+
l: info.lastChatLog ?? null,
|
|
234
|
+
ll: info.lastLogId,
|
|
235
|
+
i: memberIds,
|
|
236
|
+
k: memberNames,
|
|
237
|
+
li: info.li,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function extractChannelMaxLogId(body, expectedChatId) {
|
|
241
|
+
const info = extractChannelInfo(body, expectedChatId);
|
|
242
|
+
const maxLogId = bsonToLong(info.lastLogId);
|
|
243
|
+
if (!maxLogId)
|
|
244
|
+
throw new Error('CHATINFO response missing lastLogId');
|
|
245
|
+
return maxLogId;
|
|
246
|
+
}
|
|
169
247
|
function extractOpenLinkName(body) {
|
|
170
248
|
const ols = body.ols;
|
|
171
249
|
if (!Array.isArray(ols) || ols.length === 0)
|
|
@@ -173,9 +251,8 @@ function extractOpenLinkName(body) {
|
|
|
173
251
|
const ln = ols[0]?.ln;
|
|
174
252
|
return typeof ln === 'string' && ln.length > 0 ? ln : null;
|
|
175
253
|
}
|
|
176
|
-
const OPEN_CHAT_TYPES = new Set(['OM', 'OD']);
|
|
177
254
|
function isOpenChat(chat) {
|
|
178
|
-
return
|
|
255
|
+
return isOpenKakaoChatType(chat.t);
|
|
179
256
|
}
|
|
180
257
|
function getOpenLinkId(chat) {
|
|
181
258
|
return bsonToLong(chat.li) ?? null;
|
|
@@ -209,11 +286,39 @@ function collectChats(chatDatas, into, seen) {
|
|
|
209
286
|
}
|
|
210
287
|
}
|
|
211
288
|
}
|
|
212
|
-
function wrapError(error, code) {
|
|
213
|
-
if (error instanceof KakaoTalkError)
|
|
214
|
-
|
|
289
|
+
function wrapError(error, code, options) {
|
|
290
|
+
if (error instanceof KakaoTalkError) {
|
|
291
|
+
const responseFailureKind = error.responseFailureKind ?? (options?.classifyResponseFailure ? 'transient_or_unknown' : undefined);
|
|
292
|
+
const getChatFailureReason = error.getChatFailureReason ?? (options?.classifyGetChatFailure ? 'transport_or_unknown' : undefined);
|
|
293
|
+
if (responseFailureKind === error.responseFailureKind && getChatFailureReason === error.getChatFailureReason) {
|
|
294
|
+
return error;
|
|
295
|
+
}
|
|
296
|
+
return new KakaoTalkError(error.message, error.code, {
|
|
297
|
+
cause: error.cause ?? error,
|
|
298
|
+
serverStatus: error.serverStatus,
|
|
299
|
+
responseFailureKind,
|
|
300
|
+
responseStatusSource: error.responseStatusSource,
|
|
301
|
+
getChatFailureReason,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
215
304
|
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
-
return new KakaoTalkError(message, code, {
|
|
305
|
+
return new KakaoTalkError(message, code, {
|
|
306
|
+
cause: error,
|
|
307
|
+
serverStatus: error instanceof LocoResponseError ? error.serverStatus : undefined,
|
|
308
|
+
responseStatusSource: error instanceof LocoResponseError ? error.responseStatusSource : undefined,
|
|
309
|
+
responseFailureKind: error instanceof LocoResponseError
|
|
310
|
+
? error.responseFailureKind
|
|
311
|
+
: options?.classifyResponseFailure
|
|
312
|
+
? 'transient_or_unknown'
|
|
313
|
+
: undefined,
|
|
314
|
+
getChatFailureReason: options?.classifyGetChatFailure
|
|
315
|
+
? error instanceof LocoResponseError && error.responseFailureKind === 'provider_rejection'
|
|
316
|
+
? 'provider_rejection'
|
|
317
|
+
: error instanceof LocoResponseError && error.responseStatusSource === 'packet' && error.serverStatus === -1
|
|
318
|
+
? 'synthetic_connection_close'
|
|
319
|
+
: 'transport_or_unknown'
|
|
320
|
+
: undefined,
|
|
321
|
+
});
|
|
217
322
|
}
|
|
218
323
|
function isLoginResponseError(error) {
|
|
219
324
|
if (!(error instanceof Error))
|
|
@@ -335,12 +440,27 @@ function isNonZeroLong(v) {
|
|
|
335
440
|
// caller-visible error channel (e.g. GETMEM/MEMBER return `[]` for both empty rooms and dead
|
|
336
441
|
// sockets). Throwing here lets executeWithReconnect detect session death and reconnect.
|
|
337
442
|
function assertLocoOk(response, command) {
|
|
443
|
+
if (typeof response.statusCode !== 'number' || !Number.isFinite(response.statusCode)) {
|
|
444
|
+
throw new LocoResponseError(`${command} failed: response status unavailable`, 'transient_or_unknown');
|
|
445
|
+
}
|
|
446
|
+
if (isSyntheticConnectionClose(response)) {
|
|
447
|
+
throw new LocoResponseError(`${command} failed: connection closed`, 'transient_or_unknown', {
|
|
448
|
+
serverStatus: response.statusCode,
|
|
449
|
+
responseStatusSource: 'packet',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
338
452
|
if (response.statusCode !== 0) {
|
|
339
|
-
throw new
|
|
453
|
+
throw new LocoResponseError(`${command} failed: statusCode=${response.statusCode}`, 'provider_rejection', {
|
|
454
|
+
serverStatus: response.statusCode,
|
|
455
|
+
responseStatusSource: 'packet',
|
|
456
|
+
});
|
|
340
457
|
}
|
|
341
458
|
const bodyStatus = response.body.status;
|
|
342
459
|
if (typeof bodyStatus === 'number' && bodyStatus !== 0) {
|
|
343
|
-
throw new
|
|
460
|
+
throw new LocoResponseError(`${command} failed: body.status=${bodyStatus}`, 'provider_rejection', {
|
|
461
|
+
serverStatus: bodyStatus,
|
|
462
|
+
responseStatusSource: 'body',
|
|
463
|
+
});
|
|
344
464
|
}
|
|
345
465
|
}
|
|
346
466
|
function formatMember(member) {
|
|
@@ -359,10 +479,24 @@ function formatMember(member) {
|
|
|
359
479
|
};
|
|
360
480
|
}
|
|
361
481
|
function memberIdKey(member) {
|
|
362
|
-
|
|
482
|
+
const value = member.userId;
|
|
483
|
+
if (typeof value === 'number') {
|
|
484
|
+
return Number.isSafeInteger(value) && value > 0 ? String(value) : null;
|
|
485
|
+
}
|
|
486
|
+
if (!value || typeof value !== 'object' || !('low' in value) || !('high' in value)) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const { low, high } = value;
|
|
490
|
+
if (!Number.isInteger(low) ||
|
|
491
|
+
!Number.isInteger(high) ||
|
|
492
|
+
low < -0x80000000 ||
|
|
493
|
+
low > 0xffffffff ||
|
|
494
|
+
high < -0x80000000 ||
|
|
495
|
+
high > 0xffffffff) {
|
|
363
496
|
return null;
|
|
364
|
-
|
|
365
|
-
|
|
497
|
+
}
|
|
498
|
+
const numeric = (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
|
|
499
|
+
return numeric > 0n && numeric <= BigInt(Number.MAX_SAFE_INTEGER) ? numeric.toString() : null;
|
|
366
500
|
}
|
|
367
501
|
function mergeMemberRecords(primaryMembers, fallbackMembers) {
|
|
368
502
|
const merged = [];
|
|
@@ -385,18 +519,43 @@ function mergeMemberRecords(primaryMembers, fallbackMembers) {
|
|
|
385
519
|
upsert(member);
|
|
386
520
|
return merged;
|
|
387
521
|
}
|
|
388
|
-
function
|
|
389
|
-
if (!
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
522
|
+
function exactMemberRecords(value, source) {
|
|
523
|
+
if (!Array.isArray(value)) {
|
|
524
|
+
throw new Error(`${source} member snapshot missing`);
|
|
525
|
+
}
|
|
526
|
+
const records = [];
|
|
527
|
+
const ids = new Set();
|
|
528
|
+
for (const raw of value) {
|
|
529
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
530
|
+
throw new Error(`${source} member snapshot invalid`);
|
|
531
|
+
}
|
|
532
|
+
const record = raw;
|
|
533
|
+
const id = memberIdKey(record);
|
|
534
|
+
if (!id || ids.has(id)) {
|
|
535
|
+
throw new Error(`${source} member snapshot invalid`);
|
|
536
|
+
}
|
|
537
|
+
ids.add(id);
|
|
538
|
+
records.push(record);
|
|
539
|
+
}
|
|
540
|
+
return { records, ids };
|
|
541
|
+
}
|
|
542
|
+
function sameIds(left, right) {
|
|
543
|
+
return left.size === right.size && [...left].every((id) => right.has(id));
|
|
544
|
+
}
|
|
545
|
+
function subsetIds(subset, superset) {
|
|
546
|
+
return [...subset].every((id) => superset.has(id));
|
|
547
|
+
}
|
|
548
|
+
function channelMemberView(body, expectedChatId) {
|
|
549
|
+
const info = extractChannelInfo(body, expectedChatId);
|
|
550
|
+
const activeMembers = requiredNonNegativeInteger(info.activeMembersCount, 'activeMembersCount');
|
|
551
|
+
const displayMembers = exactMemberRecords(info.displayMembers ?? [], 'CHATINFO');
|
|
552
|
+
if (displayMembers.records.length > activeMembers) {
|
|
553
|
+
throw new Error('CHATINFO member snapshot invalid');
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
activeMembers,
|
|
557
|
+
...displayMembers,
|
|
558
|
+
};
|
|
400
559
|
}
|
|
401
560
|
function formatMessages(logs, count, chatId, nameCache) {
|
|
402
561
|
logs.sort((a, b) => a.sendAt - b.sendAt);
|
|
@@ -410,6 +569,40 @@ function formatMessages(logs, count, chatId, nameCache) {
|
|
|
410
569
|
sent_at: log.sendAt,
|
|
411
570
|
}));
|
|
412
571
|
}
|
|
572
|
+
function formatForwardMessage(log, chatId, nameCache) {
|
|
573
|
+
return {
|
|
574
|
+
log_id: longToString(log.logId),
|
|
575
|
+
type: log.type,
|
|
576
|
+
author_id: log.authorId,
|
|
577
|
+
author_name: nameCache.lookup(chatId, log.authorId),
|
|
578
|
+
message: log.message,
|
|
579
|
+
attachment: parseAttachmentJson(log.attachment),
|
|
580
|
+
sent_at: log.sendAt,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function buildMessagePage(logs, count, cursor, protocolComplete, chatId, nameCache) {
|
|
584
|
+
const cursorValue = BigInt(cursor.toString());
|
|
585
|
+
const unique = new Map();
|
|
586
|
+
for (const log of logs) {
|
|
587
|
+
const logId = longToString(log.logId);
|
|
588
|
+
if (BigInt(logId) <= cursorValue)
|
|
589
|
+
continue;
|
|
590
|
+
if (!unique.has(logId))
|
|
591
|
+
unique.set(logId, log);
|
|
592
|
+
}
|
|
593
|
+
const ordered = [...unique.entries()].sort(([left], [right]) => {
|
|
594
|
+
const a = BigInt(left);
|
|
595
|
+
const b = BigInt(right);
|
|
596
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
597
|
+
});
|
|
598
|
+
const selected = ordered.slice(0, count);
|
|
599
|
+
const nextCursor = selected.at(-1)?.[0] ?? null;
|
|
600
|
+
return {
|
|
601
|
+
messages: selected.map(([, log]) => formatForwardMessage(log, chatId, nameCache)),
|
|
602
|
+
next_cursor: nextCursor,
|
|
603
|
+
complete: protocolComplete && ordered.length <= count,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
413
606
|
function buildReplyExtra(target) {
|
|
414
607
|
return {
|
|
415
608
|
attach_only: false,
|
|
@@ -636,6 +829,7 @@ export class KakaoTalkClient {
|
|
|
636
829
|
if (options?.all || options?.search || snapshotEmpty) {
|
|
637
830
|
let cursor = loginResult;
|
|
638
831
|
let pages = 0;
|
|
832
|
+
const requireCompleteList = options?.all === true;
|
|
639
833
|
while (pages < MAX_PAGES) {
|
|
640
834
|
// Trust eof only when the snapshot had data. When the snapshot was empty
|
|
641
835
|
// (new device), ignore eof for the first iteration so we always attempt
|
|
@@ -647,14 +841,26 @@ export class KakaoTalkClient {
|
|
|
647
841
|
const lastTokenId = bsonToLong(cursor.lastTokenId);
|
|
648
842
|
const lastChatId = bsonToLong(cursor.lastChatId);
|
|
649
843
|
const response = await session.getChatList(lastTokenId, lastChatId);
|
|
844
|
+
if (requireCompleteList)
|
|
845
|
+
assertLocoOk(response, 'LCHATLIST');
|
|
650
846
|
const body = response.body;
|
|
847
|
+
if (requireCompleteList && !Array.isArray(body.chatDatas)) {
|
|
848
|
+
throw new Error('LCHATLIST pagination incomplete: chatDatas unavailable');
|
|
849
|
+
}
|
|
651
850
|
const chatDatas = (body.chatDatas ?? []);
|
|
652
|
-
|
|
851
|
+
cursor = body;
|
|
852
|
+
pages++;
|
|
853
|
+
if (chatDatas.length === 0) {
|
|
854
|
+
if (requireCompleteList && body.eof !== true) {
|
|
855
|
+
throw new Error('LCHATLIST pagination incomplete: empty non-EOF page');
|
|
856
|
+
}
|
|
653
857
|
break;
|
|
858
|
+
}
|
|
654
859
|
collectChats(chatDatas, allChats, seenChatIds);
|
|
655
860
|
this.nameCache.ingest(chatDatas);
|
|
656
|
-
|
|
657
|
-
|
|
861
|
+
}
|
|
862
|
+
if (requireCompleteList && cursor.eof !== true) {
|
|
863
|
+
throw new Error(`LCHATLIST pagination incomplete after ${pages} pages`);
|
|
658
864
|
}
|
|
659
865
|
}
|
|
660
866
|
allChats.sort((a, b) => (b.o ?? 0) - (a.o ?? 0));
|
|
@@ -672,6 +878,63 @@ export class KakaoTalkClient {
|
|
|
672
878
|
}
|
|
673
879
|
});
|
|
674
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* Fetch one chat directly by id via read-only CHATINFO.
|
|
883
|
+
*
|
|
884
|
+
* Unlike `getChats()`, this does not depend on the login-time snapshot or
|
|
885
|
+
* LCHATLIST pagination, so a room first observed from a live push can be
|
|
886
|
+
* resolved without interpreting protocol fields outside this SDK.
|
|
887
|
+
* The returned chat type preserves numeric values and non-empty protocol
|
|
888
|
+
* strings, including DirectChat, MultiChat, PlusChat, MemoChat, OM, and OD;
|
|
889
|
+
* unknown non-empty strings are preserved for forward compatibility.
|
|
890
|
+
*/
|
|
891
|
+
async getChat(chatId) {
|
|
892
|
+
const parsedChatId = parseChatId(chatId);
|
|
893
|
+
const normalizedChatId = longToString(parsedChatId);
|
|
894
|
+
return this.executeWithReconnect(async ({ session }) => {
|
|
895
|
+
try {
|
|
896
|
+
const response = await session.getChannelInfo(parsedChatId);
|
|
897
|
+
assertLocoOk(response, 'CHATINFO');
|
|
898
|
+
const body = response.body;
|
|
899
|
+
const chatInfo = body.chatInfo;
|
|
900
|
+
const isLeftChatInfo = chatInfo !== null &&
|
|
901
|
+
typeof chatInfo === 'object' &&
|
|
902
|
+
!Array.isArray(chatInfo) &&
|
|
903
|
+
chatInfo.left === true;
|
|
904
|
+
if (isLeftChatInfo) {
|
|
905
|
+
extractChannelInfo(body, normalizedChatId);
|
|
906
|
+
}
|
|
907
|
+
if (chatInfo === undefined || chatInfo === null || isLeftChatInfo) {
|
|
908
|
+
throw new KakaoTalkError('CHATINFO response missing chatInfo', 'get_chat_failed', {
|
|
909
|
+
responseFailureKind: 'transient_or_unknown',
|
|
910
|
+
getChatFailureReason: 'chat_info_absent',
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
const chat = channelInfoToChatData(body, normalizedChatId);
|
|
914
|
+
this.nameCache.ingest([chat]);
|
|
915
|
+
let title = extractTitle(body);
|
|
916
|
+
if (!title && isOpenChat(chat)) {
|
|
917
|
+
const linkId = getOpenLinkId(chat);
|
|
918
|
+
if (linkId) {
|
|
919
|
+
try {
|
|
920
|
+
const openLinkResponse = await session.getOpenLinkInfo([linkId]);
|
|
921
|
+
title = extractOpenLinkName(openLinkResponse.body);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
title = null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return formatChat(chat, title, this.nameCache);
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
throw wrapError(error, 'get_chat_failed', {
|
|
932
|
+
classifyResponseFailure: true,
|
|
933
|
+
classifyGetChatFailure: true,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
675
938
|
/**
|
|
676
939
|
* Resolve the user-set room title via CHATINFO. Returns null on any error
|
|
677
940
|
* (network, malformed response, or no TITLE meta present). Designed to be
|
|
@@ -714,6 +977,50 @@ export class KakaoTalkClient {
|
|
|
714
977
|
return null;
|
|
715
978
|
}
|
|
716
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Fetch one forward-only, lossless history page after `from`.
|
|
982
|
+
*
|
|
983
|
+
* The page never keeps only the newest `count` entries. If the protocol
|
|
984
|
+
* returns more entries than requested, the oldest entries are returned and
|
|
985
|
+
* `complete` remains false so callers can continue from `next_cursor`.
|
|
986
|
+
*/
|
|
987
|
+
async getMessagePage(chatId, options) {
|
|
988
|
+
const count = options?.count ?? 100;
|
|
989
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
990
|
+
throw new KakaoTalkError('Message page count must be an integer between 1 and 100', 'invalid_message_page_count');
|
|
991
|
+
}
|
|
992
|
+
const parsedChatId = parseChatId(chatId);
|
|
993
|
+
const normalizedChatId = longToString(parsedChatId);
|
|
994
|
+
const cursor = options?.from ? parseLogId(options.from) : Long.fromNumber(0);
|
|
995
|
+
return this.executeWithReconnect(async ({ session }) => {
|
|
996
|
+
try {
|
|
997
|
+
const response = await session.getChatLogs([parsedChatId], [cursor]);
|
|
998
|
+
assertLocoOk(response, 'MCHATLOGS');
|
|
999
|
+
const batch = (response.body.chatLogs ?? []).filter((log) => longToString(log.chatId) === normalizedChatId);
|
|
1000
|
+
if (batch.length > 0) {
|
|
1001
|
+
return buildMessagePage(batch, count, cursor, response.body.eof === true, normalizedChatId, this.nameCache);
|
|
1002
|
+
}
|
|
1003
|
+
// CHATINFO is read-only and supplies a fresh lastLogId even for rooms
|
|
1004
|
+
// absent from the login snapshot. This avoids CHATONROOM, which can
|
|
1005
|
+
// advance server-side unread state.
|
|
1006
|
+
const chatInfoResponse = await session.getChannelInfo(parsedChatId);
|
|
1007
|
+
assertLocoOk(chatInfoResponse, 'CHATINFO');
|
|
1008
|
+
const maxLogId = extractChannelMaxLogId(chatInfoResponse.body, normalizedChatId);
|
|
1009
|
+
if (maxLogId.lessThanOrEqual(cursor)) {
|
|
1010
|
+
return { messages: [], next_cursor: null, complete: true };
|
|
1011
|
+
}
|
|
1012
|
+
const syncResponse = await session.syncMessages(parsedChatId, Math.min(count, 80), cursor, maxLogId);
|
|
1013
|
+
assertLocoOk(syncResponse, 'SYNCMSG');
|
|
1014
|
+
const syncBatch = (syncResponse.body.chatLogs ?? []).filter((log) => longToString(log.chatId) === normalizedChatId);
|
|
1015
|
+
const maxReturned = findMaxLogId(syncBatch, 'logId');
|
|
1016
|
+
const protocolComplete = syncResponse.body.isOK === true || (maxReturned !== null && maxReturned.greaterThanOrEqual(maxLogId));
|
|
1017
|
+
return buildMessagePage(syncBatch, count, cursor, protocolComplete, normalizedChatId, this.nameCache);
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
throw wrapError(error, 'get_message_page_failed');
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
717
1024
|
async getMessages(chatId, options) {
|
|
718
1025
|
return this.executeWithReconnect(async ({ session, loginResult }) => {
|
|
719
1026
|
try {
|
|
@@ -798,30 +1105,58 @@ export class KakaoTalkClient {
|
|
|
798
1105
|
}
|
|
799
1106
|
});
|
|
800
1107
|
}
|
|
801
|
-
async
|
|
1108
|
+
async getMemberSnapshot(chatId) {
|
|
802
1109
|
const parsedChatId = parseChatId(chatId);
|
|
1110
|
+
const normalizedChatId = longToString(parsedChatId);
|
|
803
1111
|
return this.executeWithReconnect(async ({ session }) => {
|
|
804
1112
|
try {
|
|
805
|
-
const
|
|
806
|
-
assertLocoOk(
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1113
|
+
const firstChatResponse = await session.getChannelInfo(parsedChatId);
|
|
1114
|
+
assertLocoOk(firstChatResponse, 'CHATINFO');
|
|
1115
|
+
const firstChat = channelMemberView(firstChatResponse.body, normalizedChatId);
|
|
1116
|
+
const firstMemberResponse = await session.getAllMembers(parsedChatId);
|
|
1117
|
+
assertLocoOk(firstMemberResponse, 'GETMEM');
|
|
1118
|
+
const firstMembers = exactMemberRecords(firstMemberResponse.body.members ?? [], 'GETMEM');
|
|
1119
|
+
const secondChatResponse = await session.getChannelInfo(parsedChatId);
|
|
1120
|
+
assertLocoOk(secondChatResponse, 'CHATINFO');
|
|
1121
|
+
const secondChat = channelMemberView(secondChatResponse.body, normalizedChatId);
|
|
1122
|
+
const secondMemberResponse = await session.getAllMembers(parsedChatId);
|
|
1123
|
+
assertLocoOk(secondMemberResponse, 'GETMEM');
|
|
1124
|
+
const secondMembers = exactMemberRecords(secondMemberResponse.body.members ?? [], 'GETMEM');
|
|
1125
|
+
if (firstChat.activeMembers !== secondChat.activeMembers ||
|
|
1126
|
+
firstMembers.records.length !== firstChat.activeMembers ||
|
|
1127
|
+
secondMembers.records.length !== secondChat.activeMembers ||
|
|
1128
|
+
!sameIds(firstChat.ids, secondChat.ids) ||
|
|
1129
|
+
!sameIds(firstMembers.ids, secondMembers.ids) ||
|
|
1130
|
+
!subsetIds(secondChat.ids, secondMembers.ids)) {
|
|
1131
|
+
throw new Error('member_snapshot_changed_or_incomplete');
|
|
814
1132
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1133
|
+
return {
|
|
1134
|
+
chat_id: normalizedChatId,
|
|
1135
|
+
active_members: secondChat.activeMembers,
|
|
1136
|
+
members: mergeMemberRecords(secondMembers.records, secondChat.records).map(formatMember),
|
|
1137
|
+
complete: true,
|
|
1138
|
+
consistency_basis: 'stable_double_read_chatinfo_getmem',
|
|
1139
|
+
};
|
|
819
1140
|
}
|
|
820
1141
|
catch (error) {
|
|
821
|
-
throw wrapError(error, '
|
|
1142
|
+
throw wrapError(error, 'get_member_snapshot_failed');
|
|
822
1143
|
}
|
|
823
1144
|
});
|
|
824
1145
|
}
|
|
1146
|
+
async getMembers(chatId) {
|
|
1147
|
+
try {
|
|
1148
|
+
return (await this.getMemberSnapshot(chatId)).members;
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
if (error instanceof KakaoTalkError && error.code === 'get_member_snapshot_failed') {
|
|
1152
|
+
throw new KakaoTalkError(error.message, 'get_members_failed', {
|
|
1153
|
+
cause: error,
|
|
1154
|
+
serverStatus: error.serverStatus,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
throw error;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
825
1160
|
async getMembersByIds(chatId, userIds) {
|
|
826
1161
|
if (userIds.length === 0)
|
|
827
1162
|
return [];
|