agent-messenger 2.36.1 → 2.37.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 (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +2 -1
  4. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
  5. package/dist/src/platforms/kakaotalk/chat-classifier.js +8 -6
  6. package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
  7. package/dist/src/platforms/kakaotalk/client.d.ts +21 -1
  8. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  9. package/dist/src/platforms/kakaotalk/client.js +272 -32
  10. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  11. package/dist/src/platforms/kakaotalk/index.d.ts +3 -3
  12. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  13. package/dist/src/platforms/kakaotalk/index.js +2 -2
  14. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  15. package/dist/src/platforms/kakaotalk/types.d.ts +48 -2
  16. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  17. package/dist/src/platforms/kakaotalk/types.js +13 -1
  18. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  19. package/docs/content/docs/cli/kakaotalk.mdx +7 -1
  20. package/docs/content/docs/sdk/kakaotalk.mdx +29 -1
  21. package/package.json +1 -1
  22. package/skills/agent-channeltalk/SKILL.md +1 -1
  23. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  24. package/skills/agent-discord/SKILL.md +1 -1
  25. package/skills/agent-discordbot/SKILL.md +1 -1
  26. package/skills/agent-imessage/SKILL.md +1 -1
  27. package/skills/agent-instagram/SKILL.md +1 -1
  28. package/skills/agent-kakaotalk/SKILL.md +1 -1
  29. package/skills/agent-kakaotalk/references/common-patterns.md +36 -0
  30. package/skills/agent-line/SKILL.md +1 -1
  31. package/skills/agent-slack/SKILL.md +1 -1
  32. package/skills/agent-slackbot/SKILL.md +1 -1
  33. package/skills/agent-teams/SKILL.md +1 -1
  34. package/skills/agent-telegram/SKILL.md +1 -1
  35. package/skills/agent-telegrambot/SKILL.md +1 -1
  36. package/skills/agent-webex/SKILL.md +1 -1
  37. package/skills/agent-webexbot/SKILL.md +1 -1
  38. package/skills/agent-wechatbot/SKILL.md +1 -1
  39. package/skills/agent-whatsapp/SKILL.md +1 -1
  40. package/skills/agent-whatsappbot/SKILL.md +1 -1
  41. package/src/platforms/kakaotalk/chat-classifier.test.ts +1 -1
  42. package/src/platforms/kakaotalk/chat-classifier.ts +10 -7
  43. package/src/platforms/kakaotalk/client.test.ts +593 -24
  44. package/src/platforms/kakaotalk/client.ts +337 -35
  45. package/src/platforms/kakaotalk/index.test.ts +12 -0
  46. package/src/platforms/kakaotalk/index.ts +7 -1
  47. package/src/platforms/kakaotalk/types.ts +33 -2
@@ -8,6 +8,7 @@ import { getConfigDir } from '@/shared/utils/config-dir'
8
8
  import { warn } from '@/shared/utils/stderr'
9
9
 
10
10
  import { type AttachmentInput, type ResolvedAttachment, planAttachments } from './attachment-router'
11
+ import { isOpenKakaoChatType } from './chat-classifier'
11
12
  import { detectImageDimensions } from './image-meta'
12
13
  import { sha1Hex } from './media-upload'
13
14
  import { LANG, PC_OS_NAME, getLocoDeviceConfig } from './protocol/config'
@@ -21,7 +22,9 @@ import {
21
22
  type KakaoLeaveChatResult,
22
23
  type KakaoMarkReadResult,
23
24
  type KakaoMember,
25
+ type KakaoMemberSnapshot,
24
26
  type KakaoMessage,
27
+ type KakaoMessagePage,
25
28
  type KakaoMultiPhotoExtra,
26
29
  type KakaoProfile,
27
30
  type KakaoReplyExtra,
@@ -184,7 +187,7 @@ function formatChat(chat: ChatData, title: string | null, nameCache: MemberNameC
184
187
 
185
188
  return {
186
189
  chat_id: chatId,
187
- type: chat.t as number,
190
+ type: chat.t as KakaoChat['type'],
188
191
  display_name: displayName,
189
192
  title,
190
193
  active_members: chat.a as number,
@@ -207,10 +210,22 @@ interface ChannelMetaEntry {
207
210
  content?: string
208
211
  }
209
212
 
213
+ interface ChannelInfoData {
214
+ chatId?: unknown
215
+ type?: unknown
216
+ activeMembersCount?: unknown
217
+ newMessageCount?: unknown
218
+ invalidNewMessageCount?: unknown
219
+ lastLogId?: unknown
220
+ lastSeenLogId?: unknown
221
+ lastChatLog?: Record<string, unknown> | null
222
+ displayMembers?: Array<Record<string, unknown>>
223
+ chatMetas?: ChannelMetaEntry[]
224
+ li?: unknown
225
+ }
226
+
210
227
  interface ChannelInfoResponse {
211
- chatInfo?: {
212
- chatMetas?: ChannelMetaEntry[]
213
- }
228
+ chatInfo?: ChannelInfoData
214
229
  }
215
230
 
216
231
  function extractTitle(body: Record<string, unknown>): string | null {
@@ -223,6 +238,69 @@ function extractTitle(body: Record<string, unknown>): string | null {
223
238
  return typeof content === 'string' && content.length > 0 ? content : null
224
239
  }
225
240
 
241
+ function extractChannelInfo(body: Record<string, unknown>, expectedChatId: string): ChannelInfoData {
242
+ const info = (body as ChannelInfoResponse).chatInfo
243
+ if (!info || typeof info !== 'object') {
244
+ throw new Error('CHATINFO response missing chatInfo')
245
+ }
246
+ const responseChatId = longToString(info.chatId)
247
+ if (responseChatId !== expectedChatId) {
248
+ throw new Error('CHATINFO response chatId mismatch')
249
+ }
250
+ return info
251
+ }
252
+
253
+ function requiredNonNegativeInteger(value: unknown, field: string): number {
254
+ if (!Number.isInteger(value) || (value as number) < 0) {
255
+ throw new Error(`CHATINFO response invalid ${field}`)
256
+ }
257
+ return value as number
258
+ }
259
+
260
+ function requiredChatType(value: unknown): KakaoChat['type'] {
261
+ if (typeof value === 'string' && isOpenKakaoChatType(value)) {
262
+ return value
263
+ }
264
+ return requiredNonNegativeInteger(value, 'type')
265
+ }
266
+
267
+ function channelInfoToChatData(body: Record<string, unknown>, expectedChatId: string): ChatData {
268
+ const info = extractChannelInfo(body, expectedChatId)
269
+ const type = requiredChatType(info.type)
270
+ const activeMembers = requiredNonNegativeInteger(info.activeMembersCount, 'activeMembersCount')
271
+ const unreadCount =
272
+ info.invalidNewMessageCount === true ? 0 : requiredNonNegativeInteger(info.newMessageCount, 'newMessageCount')
273
+ const displayMembers = Array.isArray(info.displayMembers) ? info.displayMembers : []
274
+ const memberIds: unknown[] = []
275
+ const memberNames: string[] = []
276
+
277
+ for (const member of displayMembers) {
278
+ if (!member || typeof member !== 'object') continue
279
+ if (member.userId === undefined || typeof member.nickName !== 'string') continue
280
+ memberIds.push(member.userId)
281
+ memberNames.push(member.nickName)
282
+ }
283
+
284
+ return {
285
+ c: info.chatId,
286
+ t: type,
287
+ a: activeMembers,
288
+ n: unreadCount,
289
+ l: info.lastChatLog ?? null,
290
+ ll: info.lastLogId,
291
+ i: memberIds,
292
+ k: memberNames,
293
+ li: info.li,
294
+ }
295
+ }
296
+
297
+ function extractChannelMaxLogId(body: Record<string, unknown>, expectedChatId: string): Long {
298
+ const info = extractChannelInfo(body, expectedChatId)
299
+ const maxLogId = bsonToLong(info.lastLogId)
300
+ if (!maxLogId) throw new Error('CHATINFO response missing lastLogId')
301
+ return maxLogId
302
+ }
303
+
226
304
  interface OpenLinkInfoResponse {
227
305
  ols?: Array<{ ln?: unknown }>
228
306
  }
@@ -234,10 +312,8 @@ function extractOpenLinkName(body: Record<string, unknown>): string | null {
234
312
  return typeof ln === 'string' && ln.length > 0 ? ln : null
235
313
  }
236
314
 
237
- const OPEN_CHAT_TYPES = new Set(['OM', 'OD'])
238
-
239
315
  function isOpenChat(chat: ChatData): boolean {
240
- return typeof chat.t === 'string' && OPEN_CHAT_TYPES.has(chat.t)
316
+ return isOpenKakaoChatType(chat.t)
241
317
  }
242
318
 
243
319
  function getOpenLinkId(chat: ChatData): Long | null {
@@ -452,9 +528,29 @@ function formatMember(member: Record<string, unknown>): KakaoMember {
452
528
  }
453
529
 
454
530
  function memberIdKey(member: Record<string, unknown>): string | null {
455
- if (member.userId === undefined || member.userId === null) return null
456
- const key = longToString(member.userId)
457
- return key === '0' ? null : key
531
+ const value = member.userId
532
+ if (typeof value === 'number') {
533
+ return Number.isSafeInteger(value) && value > 0 ? String(value) : null
534
+ }
535
+ if (!value || typeof value !== 'object' || !('low' in value) || !('high' in value)) {
536
+ return null
537
+ }
538
+ const { low, high } = value as {
539
+ low: unknown
540
+ high: unknown
541
+ }
542
+ if (
543
+ !Number.isInteger(low) ||
544
+ !Number.isInteger(high) ||
545
+ (low as number) < -0x80000000 ||
546
+ (low as number) > 0xffffffff ||
547
+ (high as number) < -0x80000000 ||
548
+ (high as number) > 0xffffffff
549
+ ) {
550
+ return null
551
+ }
552
+ const numeric = (BigInt((high as number) >>> 0) << 32n) | BigInt((low as number) >>> 0)
553
+ return numeric > 0n && numeric <= BigInt(Number.MAX_SAFE_INTEGER) ? numeric.toString() : null
458
554
  }
459
555
 
460
556
  function mergeMemberRecords(
@@ -484,19 +580,59 @@ function mergeMemberRecords(
484
580
  return merged
485
581
  }
486
582
 
487
- function extractChatInfoMembers(body: unknown): Array<Record<string, unknown>> {
488
- if (!body || typeof body !== 'object') return []
489
-
490
- const record = body as Record<string, unknown>
491
- if (Array.isArray(record.m)) {
492
- return record.m as Array<Record<string, unknown>>
583
+ function exactMemberRecords(
584
+ value: unknown,
585
+ source: string,
586
+ ): {
587
+ records: Array<Record<string, unknown>>
588
+ ids: Set<string>
589
+ } {
590
+ if (!Array.isArray(value)) {
591
+ throw new Error(`${source} member snapshot missing`)
592
+ }
593
+ const records: Array<Record<string, unknown>> = []
594
+ const ids = new Set<string>()
595
+ for (const raw of value) {
596
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
597
+ throw new Error(`${source} member snapshot invalid`)
598
+ }
599
+ const record = raw as Record<string, unknown>
600
+ const id = memberIdKey(record)
601
+ if (!id || ids.has(id)) {
602
+ throw new Error(`${source} member snapshot invalid`)
603
+ }
604
+ ids.add(id)
605
+ records.push(record)
493
606
  }
607
+ return { records, ids }
608
+ }
494
609
 
495
- const chatInfo = record.chatInfo
496
- if (!chatInfo || typeof chatInfo !== 'object') return []
610
+ function sameIds(left: Set<string>, right: Set<string>): boolean {
611
+ return left.size === right.size && [...left].every((id) => right.has(id))
612
+ }
497
613
 
498
- const displayMembers = (chatInfo as Record<string, unknown>).displayMembers
499
- return Array.isArray(displayMembers) ? (displayMembers as Array<Record<string, unknown>>) : []
614
+ function subsetIds(subset: Set<string>, superset: Set<string>): boolean {
615
+ return [...subset].every((id) => superset.has(id))
616
+ }
617
+
618
+ function channelMemberView(
619
+ body: Record<string, unknown>,
620
+ expectedChatId: string,
621
+ ): {
622
+ activeMembers: number
623
+ records: Array<Record<string, unknown>>
624
+ ids: Set<string>
625
+ } {
626
+ const info = extractChannelInfo(body, expectedChatId)
627
+ const activeMembers = requiredNonNegativeInteger(info.activeMembersCount, 'activeMembersCount')
628
+ const displayMembers = exactMemberRecords(info.displayMembers ?? [], 'CHATINFO')
629
+ if (displayMembers.records.length > activeMembers) {
630
+ throw new Error('CHATINFO member snapshot invalid')
631
+ }
632
+ return {
633
+ activeMembers,
634
+ ...displayMembers,
635
+ }
500
636
  }
501
637
 
502
638
  function formatMessages(
@@ -518,6 +654,50 @@ function formatMessages(
518
654
  }))
519
655
  }
520
656
 
657
+ function formatForwardMessage(log: Record<string, unknown>, chatId: string, nameCache: MemberNameCache): KakaoMessage {
658
+ return {
659
+ log_id: longToString(log.logId),
660
+ type: log.type as number,
661
+ author_id: log.authorId as number,
662
+ author_name: nameCache.lookup(chatId, log.authorId as number),
663
+ message: log.message as string,
664
+ attachment: parseAttachmentJson(log.attachment),
665
+ sent_at: log.sendAt as number,
666
+ }
667
+ }
668
+
669
+ function buildMessagePage(
670
+ logs: Array<Record<string, unknown>>,
671
+ count: number,
672
+ cursor: Long,
673
+ protocolComplete: boolean,
674
+ chatId: string,
675
+ nameCache: MemberNameCache,
676
+ ): KakaoMessagePage {
677
+ const cursorValue = BigInt(cursor.toString())
678
+ const unique = new Map<string, Record<string, unknown>>()
679
+
680
+ for (const log of logs) {
681
+ const logId = longToString(log.logId)
682
+ if (BigInt(logId) <= cursorValue) continue
683
+ if (!unique.has(logId)) unique.set(logId, log)
684
+ }
685
+
686
+ const ordered = [...unique.entries()].sort(([left], [right]) => {
687
+ const a = BigInt(left)
688
+ const b = BigInt(right)
689
+ return a < b ? -1 : a > b ? 1 : 0
690
+ })
691
+ const selected = ordered.slice(0, count)
692
+ const nextCursor = selected.at(-1)?.[0] ?? null
693
+
694
+ return {
695
+ messages: selected.map(([, log]) => formatForwardMessage(log, chatId, nameCache)),
696
+ next_cursor: nextCursor,
697
+ complete: protocolComplete && ordered.length <= count,
698
+ }
699
+ }
700
+
521
701
  function buildReplyExtra(target: KakaoReplyTarget): KakaoReplyExtra {
522
702
  return {
523
703
  attach_only: false,
@@ -810,6 +990,44 @@ export class KakaoTalkClient {
810
990
  })
811
991
  }
812
992
 
993
+ /**
994
+ * Fetch one chat directly by id via read-only CHATINFO.
995
+ *
996
+ * Unlike `getChats()`, this does not depend on the login-time snapshot or
997
+ * LCHATLIST pagination, so a room first observed from a live push can be
998
+ * resolved without interpreting protocol fields outside this SDK.
999
+ */
1000
+ async getChat(chatId: string): Promise<KakaoChat> {
1001
+ const parsedChatId = parseChatId(chatId)
1002
+ const normalizedChatId = longToString(parsedChatId)
1003
+ return this.executeWithReconnect(async ({ session }) => {
1004
+ try {
1005
+ const response = await session.getChannelInfo(parsedChatId)
1006
+ assertLocoOk(response, 'CHATINFO')
1007
+ const body = response.body as Record<string, unknown>
1008
+ const chat = channelInfoToChatData(body, normalizedChatId)
1009
+ this.nameCache.ingest([chat])
1010
+
1011
+ let title = extractTitle(body)
1012
+ if (!title && isOpenChat(chat)) {
1013
+ const linkId = getOpenLinkId(chat)
1014
+ if (linkId) {
1015
+ try {
1016
+ const openLinkResponse = await session.getOpenLinkInfo([linkId])
1017
+ title = extractOpenLinkName(openLinkResponse.body as Record<string, unknown>)
1018
+ } catch {
1019
+ title = null
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ return formatChat(chat, title, this.nameCache)
1025
+ } catch (error) {
1026
+ throw wrapError(error, 'get_chat_failed')
1027
+ }
1028
+ })
1029
+ }
1030
+
813
1031
  /**
814
1032
  * Resolve the user-set room title via CHATINFO. Returns null on any error
815
1033
  * (network, malformed response, or no TITLE meta present). Designed to be
@@ -851,6 +1069,60 @@ export class KakaoTalkClient {
851
1069
  }
852
1070
  }
853
1071
 
1072
+ /**
1073
+ * Fetch one forward-only, lossless history page after `from`.
1074
+ *
1075
+ * The page never keeps only the newest `count` entries. If the protocol
1076
+ * returns more entries than requested, the oldest entries are returned and
1077
+ * `complete` remains false so callers can continue from `next_cursor`.
1078
+ */
1079
+ async getMessagePage(chatId: string, options?: { count?: number; from?: string }): Promise<KakaoMessagePage> {
1080
+ const count = options?.count ?? 100
1081
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
1082
+ throw new KakaoTalkError('Message page count must be an integer between 1 and 100', 'invalid_message_page_count')
1083
+ }
1084
+ const parsedChatId = parseChatId(chatId)
1085
+ const normalizedChatId = longToString(parsedChatId)
1086
+ const cursor = options?.from ? parseLogId(options.from) : Long.fromNumber(0)
1087
+
1088
+ return this.executeWithReconnect(async ({ session }) => {
1089
+ try {
1090
+ const response = await session.getChatLogs([parsedChatId], [cursor])
1091
+ assertLocoOk(response, 'MCHATLOGS')
1092
+ const batch = ((response.body.chatLogs ?? []) as Array<Record<string, unknown>>).filter(
1093
+ (log) => longToString(log.chatId) === normalizedChatId,
1094
+ )
1095
+
1096
+ if (batch.length > 0) {
1097
+ return buildMessagePage(batch, count, cursor, response.body.eof === true, normalizedChatId, this.nameCache)
1098
+ }
1099
+
1100
+ // CHATINFO is read-only and supplies a fresh lastLogId even for rooms
1101
+ // absent from the login snapshot. This avoids CHATONROOM, which can
1102
+ // advance server-side unread state.
1103
+ const chatInfoResponse = await session.getChannelInfo(parsedChatId)
1104
+ assertLocoOk(chatInfoResponse, 'CHATINFO')
1105
+ const maxLogId = extractChannelMaxLogId(chatInfoResponse.body as Record<string, unknown>, normalizedChatId)
1106
+ if (maxLogId.lessThanOrEqual(cursor)) {
1107
+ return { messages: [], next_cursor: null, complete: true }
1108
+ }
1109
+
1110
+ const syncResponse = await session.syncMessages(parsedChatId, Math.min(count, 80), cursor, maxLogId)
1111
+ assertLocoOk(syncResponse, 'SYNCMSG')
1112
+ const syncBatch = ((syncResponse.body.chatLogs ?? []) as Array<Record<string, unknown>>).filter(
1113
+ (log) => longToString(log.chatId) === normalizedChatId,
1114
+ )
1115
+ const maxReturned = findMaxLogId(syncBatch, 'logId')
1116
+ const protocolComplete =
1117
+ syncResponse.body.isOK === true || (maxReturned !== null && maxReturned.greaterThanOrEqual(maxLogId))
1118
+
1119
+ return buildMessagePage(syncBatch, count, cursor, protocolComplete, normalizedChatId, this.nameCache)
1120
+ } catch (error) {
1121
+ throw wrapError(error, 'get_message_page_failed')
1122
+ }
1123
+ })
1124
+ }
1125
+
854
1126
  async getMessages(chatId: string, options?: { count?: number; from?: string }): Promise<KakaoMessage[]> {
855
1127
  return this.executeWithReconnect(async ({ session, loginResult }) => {
856
1128
  try {
@@ -949,30 +1221,60 @@ export class KakaoTalkClient {
949
1221
  })
950
1222
  }
951
1223
 
952
- async getMembers(chatId: string): Promise<KakaoMember[]> {
1224
+ async getMemberSnapshot(chatId: string): Promise<KakaoMemberSnapshot> {
953
1225
  const parsedChatId = parseChatId(chatId)
1226
+ const normalizedChatId = longToString(parsedChatId)
954
1227
  return this.executeWithReconnect(async ({ session }) => {
955
1228
  try {
956
- const response = await session.getAllMembers(parsedChatId)
957
- assertLocoOk(response, 'GETMEM')
958
- const members = (response.body.members ?? []) as Array<Record<string, unknown>>
959
- let fallbackMembers: Array<Record<string, unknown>> = []
960
- try {
961
- // Some KakaoTalk rooms return only a subset from GETMEM even though
962
- // CHATONROOM carries the full active member list in `m`.
963
- const chatInfo = await session.getChatInfo(parsedChatId)
964
- fallbackMembers = extractChatInfoMembers(chatInfo.body)
965
- } catch {
966
- fallbackMembers = []
1229
+ const firstChatResponse = await session.getChannelInfo(parsedChatId)
1230
+ assertLocoOk(firstChatResponse, 'CHATINFO')
1231
+ const firstChat = channelMemberView(firstChatResponse.body as Record<string, unknown>, normalizedChatId)
1232
+ const firstMemberResponse = await session.getAllMembers(parsedChatId)
1233
+ assertLocoOk(firstMemberResponse, 'GETMEM')
1234
+ const firstMembers = exactMemberRecords(firstMemberResponse.body.members ?? [], 'GETMEM')
1235
+ const secondChatResponse = await session.getChannelInfo(parsedChatId)
1236
+ assertLocoOk(secondChatResponse, 'CHATINFO')
1237
+ const secondChat = channelMemberView(secondChatResponse.body as Record<string, unknown>, normalizedChatId)
1238
+ const secondMemberResponse = await session.getAllMembers(parsedChatId)
1239
+ assertLocoOk(secondMemberResponse, 'GETMEM')
1240
+ const secondMembers = exactMemberRecords(secondMemberResponse.body.members ?? [], 'GETMEM')
1241
+ if (
1242
+ firstChat.activeMembers !== secondChat.activeMembers ||
1243
+ firstMembers.records.length !== firstChat.activeMembers ||
1244
+ secondMembers.records.length !== secondChat.activeMembers ||
1245
+ !sameIds(firstChat.ids, secondChat.ids) ||
1246
+ !sameIds(firstMembers.ids, secondMembers.ids) ||
1247
+ !subsetIds(secondChat.ids, secondMembers.ids)
1248
+ ) {
1249
+ throw new Error('member_snapshot_changed_or_incomplete')
1250
+ }
1251
+ return {
1252
+ chat_id: normalizedChatId,
1253
+ active_members: secondChat.activeMembers,
1254
+ members: mergeMemberRecords(secondMembers.records, secondChat.records).map(formatMember),
1255
+ complete: true,
1256
+ consistency_basis: 'stable_double_read_chatinfo_getmem',
967
1257
  }
968
-
969
- return mergeMemberRecords(members, fallbackMembers).map(formatMember)
970
1258
  } catch (error) {
971
- throw wrapError(error, 'get_members_failed')
1259
+ throw wrapError(error, 'get_member_snapshot_failed')
972
1260
  }
973
1261
  })
974
1262
  }
975
1263
 
1264
+ async getMembers(chatId: string): Promise<KakaoMember[]> {
1265
+ try {
1266
+ return (await this.getMemberSnapshot(chatId)).members
1267
+ } catch (error) {
1268
+ if (error instanceof KakaoTalkError && error.code === 'get_member_snapshot_failed') {
1269
+ throw new KakaoTalkError(error.message, 'get_members_failed', {
1270
+ cause: error,
1271
+ serverStatus: error.serverStatus,
1272
+ })
1273
+ }
1274
+ throw error
1275
+ }
1276
+ }
1277
+
976
1278
  async getMembersByIds(chatId: string, userIds: string[]): Promise<KakaoMember[]> {
977
1279
  if (userIds.length === 0) return []
978
1280
  const parsedChatId = parseChatId(chatId)
@@ -10,10 +10,12 @@ import {
10
10
  KakaoChatSchema,
11
11
  KakaoConfigSchema,
12
12
  KakaoLeaveChatResultSchema,
13
+ KakaoMessagePageSchema,
13
14
  KakaoMessageSchema,
14
15
  KakaoSendResultSchema,
15
16
  KakaoTalkClient,
16
17
  KakaoTalkError,
18
+ isOpenKakaoChatType,
17
19
  KakaoTalkListener,
18
20
  KakaoTalkPushMemberEventSchema,
19
21
  KakaoTalkPushMessageEventSchema,
@@ -51,6 +53,10 @@ it('KakaoMessageSchema is exported from barrel', () => {
51
53
  expect(typeof KakaoMessageSchema.parse).toBe('function')
52
54
  })
53
55
 
56
+ it('KakaoMessagePageSchema is exported from barrel', () => {
57
+ expect(typeof KakaoMessagePageSchema.parse).toBe('function')
58
+ })
59
+
54
60
  it('KakaoSendResultSchema is exported from barrel', () => {
55
61
  expect(typeof KakaoSendResultSchema.parse).toBe('function')
56
62
  })
@@ -83,6 +89,12 @@ it('classifyKakaoChat is exported from barrel', () => {
83
89
  expect(typeof classifyKakaoChat).toBe('function')
84
90
  })
85
91
 
92
+ it('isOpenKakaoChatType is exported from barrel', () => {
93
+ expect(isOpenKakaoChatType('OM')).toBe(true)
94
+ expect(isOpenKakaoChatType(13)).toBe(true)
95
+ expect(isOpenKakaoChatType('UNKNOWN')).toBe(false)
96
+ })
97
+
86
98
  it('KakaoLeaveChatResultSchema is exported from barrel', () => {
87
99
  expect(typeof KakaoLeaveChatResultSchema.parse).toBe('function')
88
100
  })
@@ -1,5 +1,5 @@
1
1
  export { KakaoTalkClient, KakaoTalkError } from './client'
2
- export { classifyKakaoChat } from './chat-classifier'
2
+ export { classifyKakaoChat, isOpenKakaoChatType } from './chat-classifier'
3
3
  export type { KakaoChatKind } from './chat-classifier'
4
4
  export { KakaoCredentialManager, CredentialManager } from './credential-manager'
5
5
  export { KakaoTalkListener } from './listener'
@@ -9,6 +9,8 @@ export type {
9
9
  KakaoAuthErrorCode,
10
10
  KakaoAuthMethod,
11
11
  KakaoChat,
12
+ KakaoChatType,
13
+ KakaoOpenChatType,
12
14
  KakaoConfig,
13
15
  KakaoDeviceType,
14
16
  KakaoEmoticonKind,
@@ -18,7 +20,9 @@ export type {
18
20
  KakaoLoginResult,
19
21
  KakaoMarkReadResult,
20
22
  KakaoMember,
23
+ KakaoMemberSnapshot,
21
24
  KakaoMessage,
25
+ KakaoMessagePage,
22
26
  KakaoMultiPhotoExtra,
23
27
  KakaoPhotoExtra,
24
28
  KakaoProfile,
@@ -44,7 +48,9 @@ export {
44
48
  KakaoLeaveChatResultSchema,
45
49
  KakaoMarkReadResultSchema,
46
50
  KakaoMemberSchema,
51
+ KakaoMemberSnapshotSchema,
47
52
  KakaoMessageSchema,
53
+ KakaoMessagePageSchema,
48
54
  KakaoProfileSchema,
49
55
  KakaoSendResultSchema,
50
56
  KakaoTalkPushEmoticonEventSchema,
@@ -70,9 +70,12 @@ export const KAKAO_NEXT_ACTIONS: Record<string, { next_action: string; message:
70
70
  },
71
71
  }
72
72
 
73
+ export type KakaoOpenChatType = 2 | 13 | 14 | 15 | 16 | 'OM' | 'OD'
74
+ export type KakaoChatType = number | Extract<KakaoOpenChatType, string>
75
+
73
76
  export interface KakaoChat {
74
77
  chat_id: string
75
- type: number
78
+ type: KakaoChatType
76
79
  display_name: string | null
77
80
  title: string | null
78
81
  active_members: number
@@ -95,6 +98,12 @@ export interface KakaoMessage {
95
98
  sent_at: number
96
99
  }
97
100
 
101
+ export interface KakaoMessagePage {
102
+ messages: KakaoMessage[]
103
+ next_cursor: string | null
104
+ complete: boolean
105
+ }
106
+
98
107
  export interface KakaoMember {
99
108
  user_id: string
100
109
  nickname: string
@@ -112,6 +121,14 @@ export interface KakaoMember {
112
121
  open_permission: number | null
113
122
  }
114
123
 
124
+ export interface KakaoMemberSnapshot {
125
+ chat_id: string
126
+ active_members: number
127
+ members: KakaoMember[]
128
+ complete: true
129
+ consistency_basis: 'stable_double_read_chatinfo_getmem'
130
+ }
131
+
115
132
  export interface KakaoSendResult {
116
133
  success: boolean
117
134
  status_code: number
@@ -222,7 +239,7 @@ export interface KakaoTypingResult {
222
239
 
223
240
  export const KakaoChatSchema = z.object({
224
241
  chat_id: z.string(),
225
- type: z.number(),
242
+ type: z.union([z.number(), z.literal('OM'), z.literal('OD')]),
226
243
  display_name: z.string().nullable(),
227
244
  title: z.string().nullable(),
228
245
  active_members: z.number(),
@@ -247,6 +264,12 @@ export const KakaoMessageSchema = z.object({
247
264
  sent_at: z.number(),
248
265
  })
249
266
 
267
+ export const KakaoMessagePageSchema = z.object({
268
+ messages: z.array(KakaoMessageSchema),
269
+ next_cursor: z.string().nullable(),
270
+ complete: z.boolean(),
271
+ })
272
+
250
273
  export const KakaoMemberSchema = z.object({
251
274
  user_id: z.string(),
252
275
  nickname: z.string(),
@@ -261,6 +284,14 @@ export const KakaoMemberSchema = z.object({
261
284
  open_permission: z.number().nullable(),
262
285
  })
263
286
 
287
+ export const KakaoMemberSnapshotSchema = z.object({
288
+ chat_id: z.string(),
289
+ active_members: z.number().int().nonnegative(),
290
+ members: z.array(KakaoMemberSchema),
291
+ complete: z.literal(true),
292
+ consistency_basis: z.literal('stable_double_read_chatinfo_getmem'),
293
+ })
294
+
264
295
  export const KakaoSendResultSchema = z.object({
265
296
  success: z.boolean(),
266
297
  status_code: z.number(),