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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.js +8 -6
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +21 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +272 -32
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +3 -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/types.d.ts +48 -2
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +13 -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 +1 -1
- package/src/platforms/kakaotalk/chat-classifier.ts +10 -7
- package/src/platforms/kakaotalk/client.test.ts +593 -24
- package/src/platforms/kakaotalk/client.ts +337 -35
- package/src/platforms/kakaotalk/index.test.ts +12 -0
- package/src/platforms/kakaotalk/index.ts +7 -1
- package/src/platforms/kakaotalk/types.ts +33 -2
|
@@ -47,6 +47,23 @@ function makeLong(n: number): { low: number; high: number } {
|
|
|
47
47
|
return { low: n, high: 0 }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function memberChannelInfo(
|
|
51
|
+
activeMembersCount: number,
|
|
52
|
+
displayMembers: Array<Record<string, unknown>> = [],
|
|
53
|
+
chatId = 100,
|
|
54
|
+
) {
|
|
55
|
+
return {
|
|
56
|
+
statusCode: 0,
|
|
57
|
+
body: {
|
|
58
|
+
chatInfo: {
|
|
59
|
+
chatId: makeLong(chatId),
|
|
60
|
+
activeMembersCount,
|
|
61
|
+
displayMembers,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
function resetAllMocks() {
|
|
51
68
|
mockLogin.mockReset()
|
|
52
69
|
mockGetChatList.mockReset()
|
|
@@ -111,6 +128,7 @@ describe('KakaoTalkClient', () => {
|
|
|
111
128
|
})
|
|
112
129
|
|
|
113
130
|
afterEach(() => {
|
|
131
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
114
132
|
resetAllMocks()
|
|
115
133
|
})
|
|
116
134
|
|
|
@@ -858,7 +876,365 @@ describe('KakaoTalkClient', () => {
|
|
|
858
876
|
})
|
|
859
877
|
})
|
|
860
878
|
|
|
879
|
+
describe('getChat', () => {
|
|
880
|
+
it('returns one fully normalized late chat from read-only CHATINFO', async () => {
|
|
881
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
882
|
+
statusCode: 0,
|
|
883
|
+
body: {
|
|
884
|
+
chatInfo: {
|
|
885
|
+
chatId: makeLong(300),
|
|
886
|
+
type: 11,
|
|
887
|
+
activeMembersCount: 2,
|
|
888
|
+
newMessageCount: 4,
|
|
889
|
+
invalidNewMessageCount: false,
|
|
890
|
+
lastLogId: makeLong(77),
|
|
891
|
+
lastSeenLogId: makeLong(73),
|
|
892
|
+
lastChatLog: {
|
|
893
|
+
logId: makeLong(77),
|
|
894
|
+
authorId: 42,
|
|
895
|
+
type: 1,
|
|
896
|
+
message: 'latest',
|
|
897
|
+
sendAt: 1700000077,
|
|
898
|
+
},
|
|
899
|
+
displayMembers: [
|
|
900
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
901
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
902
|
+
],
|
|
903
|
+
chatMetas: [{ type: 3, content: 'Late room' }],
|
|
904
|
+
pushAlert: true,
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
910
|
+
const chat = await client.getChat('300')
|
|
911
|
+
|
|
912
|
+
expect(chat).toEqual({
|
|
913
|
+
chat_id: '300',
|
|
914
|
+
type: 11,
|
|
915
|
+
display_name: 'Alice, Bob',
|
|
916
|
+
title: 'Late room',
|
|
917
|
+
active_members: 2,
|
|
918
|
+
unread_count: 4,
|
|
919
|
+
last_message: {
|
|
920
|
+
author_id: 42,
|
|
921
|
+
author_name: 'Alice',
|
|
922
|
+
message: 'latest',
|
|
923
|
+
sent_at: 1700000077,
|
|
924
|
+
},
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
client.close()
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
for (const openChatType of ['OM', 'OD'] as const) {
|
|
931
|
+
it(`preserves ${openChatType} and falls back to INFOLINK when CHATINFO has no title`, async () => {
|
|
932
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
933
|
+
statusCode: 0,
|
|
934
|
+
body: {
|
|
935
|
+
chatInfo: {
|
|
936
|
+
chatId: makeLong(300),
|
|
937
|
+
type: openChatType,
|
|
938
|
+
activeMembersCount: 2,
|
|
939
|
+
newMessageCount: 0,
|
|
940
|
+
invalidNewMessageCount: false,
|
|
941
|
+
lastLogId: makeLong(77),
|
|
942
|
+
lastSeenLogId: makeLong(73),
|
|
943
|
+
lastChatLog: null,
|
|
944
|
+
displayMembers: [
|
|
945
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
946
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
947
|
+
],
|
|
948
|
+
chatMetas: [],
|
|
949
|
+
li: makeLong(7777),
|
|
950
|
+
pushAlert: true,
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
})
|
|
954
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Open Group Title' }] } })
|
|
955
|
+
|
|
956
|
+
const client = await new KakaoTalkClient().login({
|
|
957
|
+
oauthToken: 'token',
|
|
958
|
+
userId: 'user1',
|
|
959
|
+
deviceUuid: 'device1',
|
|
960
|
+
})
|
|
961
|
+
const chat = await client.getChat('300')
|
|
962
|
+
|
|
963
|
+
expect(chat).toEqual({
|
|
964
|
+
chat_id: '300',
|
|
965
|
+
type: openChatType,
|
|
966
|
+
display_name: 'Alice, Bob',
|
|
967
|
+
title: 'Open Group Title',
|
|
968
|
+
active_members: 2,
|
|
969
|
+
unread_count: 0,
|
|
970
|
+
last_message: null,
|
|
971
|
+
})
|
|
972
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
973
|
+
|
|
974
|
+
client.close()
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
it('falls back to INFOLINK for a numeric open-chat code', async () => {
|
|
979
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
980
|
+
statusCode: 0,
|
|
981
|
+
body: {
|
|
982
|
+
chatInfo: {
|
|
983
|
+
chatId: makeLong(300),
|
|
984
|
+
type: 13,
|
|
985
|
+
activeMembersCount: 2,
|
|
986
|
+
newMessageCount: 0,
|
|
987
|
+
invalidNewMessageCount: false,
|
|
988
|
+
lastLogId: makeLong(77),
|
|
989
|
+
lastSeenLogId: makeLong(73),
|
|
990
|
+
lastChatLog: null,
|
|
991
|
+
displayMembers: [],
|
|
992
|
+
chatMetas: [],
|
|
993
|
+
li: makeLong(7777),
|
|
994
|
+
pushAlert: true,
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
})
|
|
998
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Numeric Open Title' }] } })
|
|
999
|
+
|
|
1000
|
+
const client = await new KakaoTalkClient().login({
|
|
1001
|
+
oauthToken: 'token',
|
|
1002
|
+
userId: 'user1',
|
|
1003
|
+
deviceUuid: 'device1',
|
|
1004
|
+
})
|
|
1005
|
+
const chat = await client.getChat('300')
|
|
1006
|
+
|
|
1007
|
+
expect(chat.type).toBe(13)
|
|
1008
|
+
expect(chat.title).toBe('Numeric Open Title')
|
|
1009
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
1010
|
+
|
|
1011
|
+
client.close()
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
it('rejects an unknown string CHATINFO type without contacting INFOLINK', async () => {
|
|
1015
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1016
|
+
statusCode: 0,
|
|
1017
|
+
body: {
|
|
1018
|
+
chatInfo: {
|
|
1019
|
+
chatId: makeLong(300),
|
|
1020
|
+
type: 'UNKNOWN',
|
|
1021
|
+
activeMembersCount: 2,
|
|
1022
|
+
newMessageCount: 0,
|
|
1023
|
+
invalidNewMessageCount: false,
|
|
1024
|
+
lastLogId: makeLong(1),
|
|
1025
|
+
lastSeenLogId: makeLong(0),
|
|
1026
|
+
displayMembers: [],
|
|
1027
|
+
chatMetas: [],
|
|
1028
|
+
li: makeLong(7777),
|
|
1029
|
+
pushAlert: true,
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
const client = await new KakaoTalkClient().login({
|
|
1035
|
+
oauthToken: 'token',
|
|
1036
|
+
userId: 'user1',
|
|
1037
|
+
deviceUuid: 'device1',
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
await expect(client.getChat('300')).rejects.toMatchObject({ code: 'get_chat_failed' })
|
|
1041
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1042
|
+
|
|
1043
|
+
client.close()
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
it('rejects a CHATINFO response bound to a different chat id', async () => {
|
|
1047
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1048
|
+
statusCode: 0,
|
|
1049
|
+
body: {
|
|
1050
|
+
chatInfo: {
|
|
1051
|
+
chatId: makeLong(999),
|
|
1052
|
+
type: 11,
|
|
1053
|
+
activeMembersCount: 2,
|
|
1054
|
+
newMessageCount: 0,
|
|
1055
|
+
invalidNewMessageCount: false,
|
|
1056
|
+
lastLogId: makeLong(1),
|
|
1057
|
+
lastSeenLogId: makeLong(0),
|
|
1058
|
+
displayMembers: [],
|
|
1059
|
+
chatMetas: [],
|
|
1060
|
+
pushAlert: true,
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1066
|
+
|
|
1067
|
+
await expect(client.getChat('300')).rejects.toMatchObject({ code: 'get_chat_failed' })
|
|
1068
|
+
|
|
1069
|
+
client.close()
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('rejects an invalid chat id before opening a LOCO session', async () => {
|
|
1073
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1074
|
+
|
|
1075
|
+
await expect(client.getChat('not-a-number')).rejects.toMatchObject({ code: 'invalid_chat_id' })
|
|
1076
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1077
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
1078
|
+
|
|
1079
|
+
client.close()
|
|
1080
|
+
})
|
|
1081
|
+
})
|
|
1082
|
+
|
|
861
1083
|
describe('getMessages', () => {
|
|
1084
|
+
it('returns a lossless forward page without discarding the oldest messages', async () => {
|
|
1085
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1086
|
+
statusCode: 0,
|
|
1087
|
+
body: {
|
|
1088
|
+
status: 0,
|
|
1089
|
+
chatLogs: Array.from({ length: 5 }, (_, index) => ({
|
|
1090
|
+
logId: makeLong(index + 1),
|
|
1091
|
+
chatId: 100,
|
|
1092
|
+
type: 1,
|
|
1093
|
+
authorId: 42,
|
|
1094
|
+
message: `message-${index + 1}`,
|
|
1095
|
+
sendAt: 1700000000 + index,
|
|
1096
|
+
})),
|
|
1097
|
+
eof: true,
|
|
1098
|
+
},
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1102
|
+
const page = await client.getMessagePage('100', { count: 2 })
|
|
1103
|
+
|
|
1104
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['1', '2'])
|
|
1105
|
+
expect(page.next_cursor).toBe('2')
|
|
1106
|
+
expect(page.complete).toBe(false)
|
|
1107
|
+
|
|
1108
|
+
client.close()
|
|
1109
|
+
})
|
|
1110
|
+
|
|
1111
|
+
it('returns a complete forward page when the protocol page is exhausted', async () => {
|
|
1112
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1113
|
+
statusCode: 0,
|
|
1114
|
+
body: {
|
|
1115
|
+
status: 0,
|
|
1116
|
+
chatLogs: [
|
|
1117
|
+
{ logId: makeLong(11), chatId: 100, type: 1, authorId: 42, message: 'eleven', sendAt: 11 },
|
|
1118
|
+
{ logId: makeLong(10), chatId: 100, type: 1, authorId: 42, message: 'ten', sendAt: 10 },
|
|
1119
|
+
],
|
|
1120
|
+
eof: true,
|
|
1121
|
+
},
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1125
|
+
const page = await client.getMessagePage('100', { from: '9', count: 10 })
|
|
1126
|
+
|
|
1127
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['10', '11'])
|
|
1128
|
+
expect(page.next_cursor).toBe('11')
|
|
1129
|
+
expect(page.complete).toBe(true)
|
|
1130
|
+
|
|
1131
|
+
client.close()
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
it('continues exclusively after the supplied cursor when the server repeats it', async () => {
|
|
1135
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1136
|
+
statusCode: 0,
|
|
1137
|
+
body: {
|
|
1138
|
+
status: 0,
|
|
1139
|
+
chatLogs: [
|
|
1140
|
+
{ logId: makeLong(2), chatId: 100, type: 1, authorId: 42, message: 'repeat', sendAt: 2 },
|
|
1141
|
+
{ logId: makeLong(3), chatId: 100, type: 1, authorId: 42, message: 'three', sendAt: 3 },
|
|
1142
|
+
{ logId: makeLong(4), chatId: 100, type: 1, authorId: 42, message: 'four', sendAt: 4 },
|
|
1143
|
+
],
|
|
1144
|
+
eof: true,
|
|
1145
|
+
},
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1149
|
+
const page = await client.getMessagePage('100', { from: '2', count: 10 })
|
|
1150
|
+
|
|
1151
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['3', '4'])
|
|
1152
|
+
expect(page.next_cursor).toBe('4')
|
|
1153
|
+
expect(page.complete).toBe(true)
|
|
1154
|
+
|
|
1155
|
+
client.close()
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
it('does not report completion when the protocol makes no cursor progress', async () => {
|
|
1159
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1160
|
+
statusCode: 0,
|
|
1161
|
+
body: { status: 0, chatLogs: [], eof: false },
|
|
1162
|
+
})
|
|
1163
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1164
|
+
statusCode: 0,
|
|
1165
|
+
body: {
|
|
1166
|
+
chatInfo: {
|
|
1167
|
+
chatId: makeLong(100),
|
|
1168
|
+
type: 11,
|
|
1169
|
+
activeMembersCount: 2,
|
|
1170
|
+
newMessageCount: 0,
|
|
1171
|
+
invalidNewMessageCount: false,
|
|
1172
|
+
lastLogId: makeLong(999),
|
|
1173
|
+
lastSeenLogId: makeLong(0),
|
|
1174
|
+
displayMembers: [],
|
|
1175
|
+
chatMetas: [],
|
|
1176
|
+
pushAlert: true,
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
1181
|
+
statusCode: 0,
|
|
1182
|
+
body: { status: 0, chatLogs: [], isOK: false },
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1186
|
+
const page = await client.getMessagePage('100', { from: '9', count: 10 })
|
|
1187
|
+
|
|
1188
|
+
expect(page).toEqual({ messages: [], next_cursor: null, complete: false })
|
|
1189
|
+
|
|
1190
|
+
client.close()
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
it('uses read-only CHATINFO to paginate a late room missing from the login snapshot', async () => {
|
|
1194
|
+
mockLogin.mockResolvedValueOnce({ ...structuredClone(DEFAULT_LOGIN_RESULT), chatDatas: [] })
|
|
1195
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1196
|
+
statusCode: 0,
|
|
1197
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
1198
|
+
})
|
|
1199
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1200
|
+
statusCode: 0,
|
|
1201
|
+
body: {
|
|
1202
|
+
chatInfo: {
|
|
1203
|
+
chatId: makeLong(300),
|
|
1204
|
+
type: 11,
|
|
1205
|
+
activeMembersCount: 2,
|
|
1206
|
+
newMessageCount: 1,
|
|
1207
|
+
invalidNewMessageCount: false,
|
|
1208
|
+
lastLogId: makeLong(12),
|
|
1209
|
+
lastSeenLogId: makeLong(0),
|
|
1210
|
+
displayMembers: [],
|
|
1211
|
+
chatMetas: [],
|
|
1212
|
+
pushAlert: true,
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
})
|
|
1216
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
1217
|
+
statusCode: 0,
|
|
1218
|
+
body: {
|
|
1219
|
+
status: 0,
|
|
1220
|
+
isOK: true,
|
|
1221
|
+
chatLogs: [{ logId: makeLong(12), chatId: 300, type: 1, authorId: 42, message: 'late', sendAt: 12 }],
|
|
1222
|
+
},
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1226
|
+
const page = await client.getMessagePage('300', { from: '11', count: 10 })
|
|
1227
|
+
|
|
1228
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['12'])
|
|
1229
|
+
expect(page.next_cursor).toBe('12')
|
|
1230
|
+
expect(page.complete).toBe(true)
|
|
1231
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1232
|
+
expect(mockSyncMessages.mock.calls[0]?.[2]?.toString()).toBe('11')
|
|
1233
|
+
expect(mockSyncMessages.mock.calls[0]?.[3]?.toString()).toBe('12')
|
|
1234
|
+
|
|
1235
|
+
client.close()
|
|
1236
|
+
})
|
|
1237
|
+
|
|
862
1238
|
it('falls back to SYNCMSG when MCHATLOGS succeeds with an empty result', async () => {
|
|
863
1239
|
const loginResult = structuredClone(DEFAULT_LOGIN_RESULT)
|
|
864
1240
|
loginResult.chatDatas[0]!.ll = makeLong(10)
|
|
@@ -1610,7 +1986,8 @@ describe('KakaoTalkClient', () => {
|
|
|
1610
1986
|
|
|
1611
1987
|
describe('getMembers / getMembersByIds', () => {
|
|
1612
1988
|
it('returns formatted members from GETMEM with normalized fields', async () => {
|
|
1613
|
-
|
|
1989
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2)).mockResolvedValueOnce(memberChannelInfo(2))
|
|
1990
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1614
1991
|
statusCode: 0,
|
|
1615
1992
|
body: {
|
|
1616
1993
|
members: [
|
|
@@ -1670,12 +2047,14 @@ describe('KakaoTalkClient', () => {
|
|
|
1670
2047
|
open_profile_link_id: '99',
|
|
1671
2048
|
open_permission: 4,
|
|
1672
2049
|
})
|
|
2050
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1673
2051
|
|
|
1674
2052
|
client.close()
|
|
1675
2053
|
})
|
|
1676
2054
|
|
|
1677
|
-
it('
|
|
1678
|
-
|
|
2055
|
+
it('rejects a partial GETMEM snapshot without entering the room', async () => {
|
|
2056
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(3)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2057
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1679
2058
|
statusCode: 0,
|
|
1680
2059
|
body: {
|
|
1681
2060
|
members: [
|
|
@@ -1689,43 +2068,223 @@ describe('KakaoTalkClient', () => {
|
|
|
1689
2068
|
token: 0,
|
|
1690
2069
|
},
|
|
1691
2070
|
})
|
|
1692
|
-
|
|
2071
|
+
|
|
2072
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
2073
|
+
await expect(client.getMembers('100')).rejects.toMatchObject({
|
|
2074
|
+
code: 'get_members_failed',
|
|
2075
|
+
})
|
|
2076
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2077
|
+
|
|
2078
|
+
client.close()
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
it('returns a stable empty snapshot for a zero-member room without CHATONROOM', async () => {
|
|
2082
|
+
mockGetChannelInfo.mockResolvedValue(memberChannelInfo(0))
|
|
2083
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2084
|
+
statusCode: 0,
|
|
2085
|
+
body: { members: [] },
|
|
2086
|
+
})
|
|
2087
|
+
|
|
2088
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
2089
|
+
const snapshot = await client.getMemberSnapshot('100')
|
|
2090
|
+
|
|
2091
|
+
expect(snapshot).toMatchObject({
|
|
2092
|
+
chat_id: '100',
|
|
2093
|
+
active_members: 0,
|
|
2094
|
+
complete: true,
|
|
2095
|
+
consistency_basis: 'stable_double_read_chatinfo_getmem',
|
|
2096
|
+
})
|
|
2097
|
+
expect(snapshot.members).toEqual([])
|
|
2098
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(2)
|
|
2099
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2100
|
+
|
|
2101
|
+
client.close()
|
|
2102
|
+
})
|
|
2103
|
+
|
|
2104
|
+
it('returns a stable CHATINFO GETMEM snapshot without CHATONROOM', async () => {
|
|
2105
|
+
const visible = [
|
|
2106
|
+
{
|
|
2107
|
+
userId: makeLong(42),
|
|
2108
|
+
nickName: 'Alice',
|
|
2109
|
+
type: 100,
|
|
2110
|
+
},
|
|
2111
|
+
]
|
|
2112
|
+
mockGetChannelInfo
|
|
2113
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2114
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2115
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1693
2116
|
statusCode: 0,
|
|
1694
2117
|
body: {
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
{
|
|
1698
|
-
|
|
1699
|
-
|
|
2118
|
+
members: [
|
|
2119
|
+
visible[0],
|
|
2120
|
+
{
|
|
2121
|
+
userId: makeLong(43),
|
|
2122
|
+
nickName: 'Bob',
|
|
2123
|
+
type: 100,
|
|
2124
|
+
},
|
|
1700
2125
|
],
|
|
1701
2126
|
},
|
|
1702
2127
|
})
|
|
1703
2128
|
|
|
1704
|
-
const client = await new KakaoTalkClient().login({
|
|
1705
|
-
|
|
2129
|
+
const client = await new KakaoTalkClient().login({
|
|
2130
|
+
oauthToken: 'token',
|
|
2131
|
+
userId: 'user1',
|
|
2132
|
+
deviceUuid: 'device1',
|
|
2133
|
+
})
|
|
2134
|
+
const snapshot = await client.getMemberSnapshot('100')
|
|
1706
2135
|
|
|
1707
|
-
expect(
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2136
|
+
expect(snapshot).toMatchObject({
|
|
2137
|
+
chat_id: '100',
|
|
2138
|
+
active_members: 2,
|
|
2139
|
+
complete: true,
|
|
2140
|
+
consistency_basis: 'stable_double_read_chatinfo_getmem',
|
|
2141
|
+
})
|
|
2142
|
+
expect(snapshot.members.map((member) => member.user_id)).toEqual(['42', '43'])
|
|
2143
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(2)
|
|
2144
|
+
expect(mockGetChannelInfo).toHaveBeenCalledTimes(2)
|
|
2145
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1712
2146
|
|
|
1713
2147
|
client.close()
|
|
1714
2148
|
})
|
|
1715
2149
|
|
|
1716
|
-
it('
|
|
1717
|
-
|
|
2150
|
+
it('fails closed when active membership changes across the snapshot', async () => {
|
|
2151
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2152
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2153
|
+
statusCode: 0,
|
|
2154
|
+
body: {
|
|
2155
|
+
members: [
|
|
2156
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
2157
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
2158
|
+
],
|
|
2159
|
+
},
|
|
2160
|
+
})
|
|
1718
2161
|
|
|
1719
|
-
const client = await new KakaoTalkClient().login({
|
|
1720
|
-
|
|
2162
|
+
const client = await new KakaoTalkClient().login({
|
|
2163
|
+
oauthToken: 'token',
|
|
2164
|
+
userId: 'user1',
|
|
2165
|
+
deviceUuid: 'device1',
|
|
2166
|
+
})
|
|
2167
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2168
|
+
code: 'get_member_snapshot_failed',
|
|
2169
|
+
})
|
|
2170
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1721
2171
|
|
|
1722
|
-
|
|
2172
|
+
client.close()
|
|
2173
|
+
})
|
|
2174
|
+
|
|
2175
|
+
it('fails closed on a same-cardinality hidden member swap', async () => {
|
|
2176
|
+
const visible = [{ userId: makeLong(42), nickName: 'Visible' }]
|
|
2177
|
+
mockGetChannelInfo
|
|
2178
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2179
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2180
|
+
mockGetAllMembers
|
|
2181
|
+
.mockResolvedValueOnce({
|
|
2182
|
+
statusCode: 0,
|
|
2183
|
+
body: {
|
|
2184
|
+
members: [visible[0], { userId: makeLong(43), nickName: 'Departed' }],
|
|
2185
|
+
},
|
|
2186
|
+
})
|
|
2187
|
+
.mockResolvedValueOnce({
|
|
2188
|
+
statusCode: 0,
|
|
2189
|
+
body: {
|
|
2190
|
+
members: [visible[0], { userId: makeLong(44), nickName: 'Joined' }],
|
|
2191
|
+
},
|
|
2192
|
+
})
|
|
2193
|
+
|
|
2194
|
+
const client = await new KakaoTalkClient().login({
|
|
2195
|
+
oauthToken: 'token',
|
|
2196
|
+
userId: 'user1',
|
|
2197
|
+
deviceUuid: 'device1',
|
|
2198
|
+
})
|
|
2199
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2200
|
+
code: 'get_member_snapshot_failed',
|
|
2201
|
+
})
|
|
1723
2202
|
|
|
1724
2203
|
client.close()
|
|
1725
2204
|
})
|
|
1726
2205
|
|
|
1727
|
-
it('
|
|
2206
|
+
it('fails closed on malformed CHATINFO member identity', async () => {
|
|
2207
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1, [{ userId: {}, nickName: 'Malformed' }]))
|
|
2208
|
+
|
|
2209
|
+
const client = await new KakaoTalkClient().login({
|
|
2210
|
+
oauthToken: 'token',
|
|
2211
|
+
userId: 'user1',
|
|
2212
|
+
deviceUuid: 'device1',
|
|
2213
|
+
})
|
|
2214
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2215
|
+
code: 'get_member_snapshot_failed',
|
|
2216
|
+
})
|
|
2217
|
+
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
2218
|
+
|
|
2219
|
+
client.close()
|
|
2220
|
+
})
|
|
2221
|
+
|
|
2222
|
+
it('fails closed on malformed GETMEM member identity', async () => {
|
|
2223
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1728
2224
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
2225
|
+
statusCode: 0,
|
|
2226
|
+
body: {
|
|
2227
|
+
members: [{ userId: '42', nickName: 'Malformed string id' }],
|
|
2228
|
+
},
|
|
2229
|
+
})
|
|
2230
|
+
|
|
2231
|
+
const client = await new KakaoTalkClient().login({
|
|
2232
|
+
oauthToken: 'token',
|
|
2233
|
+
userId: 'user1',
|
|
2234
|
+
deviceUuid: 'device1',
|
|
2235
|
+
})
|
|
2236
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2237
|
+
code: 'get_member_snapshot_failed',
|
|
2238
|
+
})
|
|
2239
|
+
|
|
2240
|
+
client.close()
|
|
2241
|
+
})
|
|
2242
|
+
|
|
2243
|
+
it('fails closed on duplicate GETMEM identity without CHATONROOM', async () => {
|
|
2244
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2))
|
|
2245
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2246
|
+
statusCode: 0,
|
|
2247
|
+
body: {
|
|
2248
|
+
members: [
|
|
2249
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
2250
|
+
{ userId: makeLong(42), nickName: 'Duplicate' },
|
|
2251
|
+
],
|
|
2252
|
+
},
|
|
2253
|
+
})
|
|
2254
|
+
|
|
2255
|
+
const client = await new KakaoTalkClient().login({
|
|
2256
|
+
oauthToken: 'token',
|
|
2257
|
+
userId: 'user1',
|
|
2258
|
+
deviceUuid: 'device1',
|
|
2259
|
+
})
|
|
2260
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2261
|
+
code: 'get_member_snapshot_failed',
|
|
2262
|
+
})
|
|
2263
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2264
|
+
|
|
2265
|
+
client.close()
|
|
2266
|
+
})
|
|
2267
|
+
|
|
2268
|
+
it('fails closed on CHATINFO chat id mismatch before GETMEM', async () => {
|
|
2269
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1, [], 101))
|
|
2270
|
+
|
|
2271
|
+
const client = await new KakaoTalkClient().login({
|
|
2272
|
+
oauthToken: 'token',
|
|
2273
|
+
userId: 'user1',
|
|
2274
|
+
deviceUuid: 'device1',
|
|
2275
|
+
})
|
|
2276
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2277
|
+
code: 'get_member_snapshot_failed',
|
|
2278
|
+
})
|
|
2279
|
+
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
2280
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2281
|
+
|
|
2282
|
+
client.close()
|
|
2283
|
+
})
|
|
2284
|
+
|
|
2285
|
+
it('normalizes missing user_type to null and treats pli=0 as absent', async () => {
|
|
2286
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(3)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2287
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1729
2288
|
statusCode: 0,
|
|
1730
2289
|
body: {
|
|
1731
2290
|
members: [
|
|
@@ -1749,6 +2308,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1749
2308
|
})
|
|
1750
2309
|
|
|
1751
2310
|
it('wraps GETMEM failures as KakaoTalkError get_members_failed', async () => {
|
|
2311
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1752
2312
|
mockGetAllMembers.mockRejectedValueOnce(new Error('Network error'))
|
|
1753
2313
|
|
|
1754
2314
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
@@ -1764,6 +2324,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1764
2324
|
})
|
|
1765
2325
|
|
|
1766
2326
|
it('throws on synthetic disconnect packet from GETMEM (statusCode != 0)', async () => {
|
|
2327
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1767
2328
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
1768
2329
|
statusCode: -1,
|
|
1769
2330
|
body: { error: 'connection closed' },
|
|
@@ -1782,6 +2343,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1782
2343
|
})
|
|
1783
2344
|
|
|
1784
2345
|
it('throws on GETMEM body.status nonzero', async () => {
|
|
2346
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1785
2347
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
1786
2348
|
statusCode: 0,
|
|
1787
2349
|
body: { status: -500 },
|
|
@@ -1893,6 +2455,8 @@ describe('KakaoTalkClient', () => {
|
|
|
1893
2455
|
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1894
2456
|
}
|
|
1895
2457
|
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
2458
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
2459
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1896
2460
|
expect(mockLogin).not.toHaveBeenCalled()
|
|
1897
2461
|
|
|
1898
2462
|
client.close()
|
|
@@ -1959,17 +2523,22 @@ describe('KakaoTalkClient', () => {
|
|
|
1959
2523
|
if (handler) handler()
|
|
1960
2524
|
return Promise.resolve({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1961
2525
|
})
|
|
1962
|
-
mockGetAllMembers.
|
|
2526
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1963
2527
|
statusCode: 0,
|
|
1964
2528
|
body: { members: [{ userId: makeLong(42), nickName: 'Alice', type: 100 }] },
|
|
1965
2529
|
})
|
|
2530
|
+
mockGetChannelInfo
|
|
2531
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
2532
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
2533
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1966
2534
|
|
|
1967
2535
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1968
2536
|
const members = await client.getMembers('100')
|
|
1969
2537
|
|
|
1970
2538
|
expect(members).toHaveLength(1)
|
|
1971
2539
|
expect(members[0].nickname).toBe('Alice')
|
|
1972
|
-
expect(mockGetAllMembers).toHaveBeenCalledTimes(
|
|
2540
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(3)
|
|
2541
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1973
2542
|
// Login fires twice: once for the initial connect, once for the reconnect
|
|
1974
2543
|
// after the captured onClose handler invalidated this.state.
|
|
1975
2544
|
expect(mockLogin).toHaveBeenCalledTimes(2)
|