agent-messenger 2.32.2 → 2.33.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/discord/client.d.ts +3 -1
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +6 -0
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/member.d.ts +3 -0
- package/dist/src/platforms/discord/commands/member.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/member.js +72 -1
- package/dist/src/platforms/discord/commands/member.js.map +1 -1
- package/dist/src/platforms/discord/index.d.ts +1 -1
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +29 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +15 -5
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/docs/content/docs/cli/discord.mdx +8 -0
- package/docs/content/docs/sdk/discord.mdx +8 -0
- 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 +9 -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 +1 -1
- 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/discord/client.test.ts +53 -0
- package/src/platforms/discord/client.ts +9 -0
- package/src/platforms/discord/commands/member.test.ts +177 -0
- package/src/platforms/discord/commands/member.ts +82 -0
- package/src/platforms/discord/index.ts +3 -0
- package/src/platforms/discord/types.ts +32 -0
- package/src/platforms/kakaotalk/client.test.ts +96 -4
- package/src/platforms/kakaotalk/client.ts +16 -5
|
@@ -2,11 +2,52 @@ import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import { DiscordClient } from '../client'
|
|
4
4
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
5
|
+
import { meAction } from './member'
|
|
5
6
|
|
|
7
|
+
let clientGetMyGuildMemberSpy: ReturnType<typeof spyOn>
|
|
8
|
+
let clientListRolesSpy: ReturnType<typeof spyOn>
|
|
6
9
|
let clientSearchMembersSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
7
11
|
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
8
12
|
|
|
9
13
|
beforeEach(() => {
|
|
14
|
+
clientGetMyGuildMemberSpy = spyOn(DiscordClient.prototype, 'getMyGuildMember').mockResolvedValue({
|
|
15
|
+
user: {
|
|
16
|
+
id: 'user-1',
|
|
17
|
+
username: 'alice',
|
|
18
|
+
global_name: 'Alice Smith',
|
|
19
|
+
avatar: 'avatar-hash-1',
|
|
20
|
+
bot: false,
|
|
21
|
+
},
|
|
22
|
+
nick: 'AliceNick',
|
|
23
|
+
roles: ['role-low', 'role-missing', 'role-high'],
|
|
24
|
+
joined_at: '2024-01-15T10:00:00Z',
|
|
25
|
+
deaf: false,
|
|
26
|
+
mute: false,
|
|
27
|
+
flags: 0,
|
|
28
|
+
})
|
|
29
|
+
clientListRolesSpy = spyOn(DiscordClient.prototype, 'listRoles').mockResolvedValue([
|
|
30
|
+
{
|
|
31
|
+
id: 'role-low',
|
|
32
|
+
name: 'Member',
|
|
33
|
+
color: 0,
|
|
34
|
+
hoist: false,
|
|
35
|
+
position: 1,
|
|
36
|
+
permissions: '1024',
|
|
37
|
+
managed: false,
|
|
38
|
+
mentionable: false,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'role-high',
|
|
42
|
+
name: 'Admin',
|
|
43
|
+
color: 16711680,
|
|
44
|
+
hoist: true,
|
|
45
|
+
position: 10,
|
|
46
|
+
permissions: '8',
|
|
47
|
+
managed: false,
|
|
48
|
+
mentionable: true,
|
|
49
|
+
},
|
|
50
|
+
])
|
|
10
51
|
clientSearchMembersSpy = spyOn(DiscordClient.prototype, 'searchMembers').mockResolvedValue([
|
|
11
52
|
{
|
|
12
53
|
user: {
|
|
@@ -47,10 +88,14 @@ beforeEach(() => {
|
|
|
47
88
|
'guild-1': { server_id: 'guild-1', server_name: 'Test Guild' },
|
|
48
89
|
},
|
|
49
90
|
})
|
|
91
|
+
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
50
92
|
})
|
|
51
93
|
|
|
52
94
|
afterEach(() => {
|
|
95
|
+
clientGetMyGuildMemberSpy?.mockRestore()
|
|
96
|
+
clientListRolesSpy?.mockRestore()
|
|
53
97
|
clientSearchMembersSpy?.mockRestore()
|
|
98
|
+
consoleLogSpy?.mockRestore()
|
|
54
99
|
credManagerLoadSpy?.mockRestore()
|
|
55
100
|
})
|
|
56
101
|
|
|
@@ -97,3 +142,135 @@ it('search: uses default limit of 10', async () => {
|
|
|
97
142
|
|
|
98
143
|
expect(members).toHaveLength(2)
|
|
99
144
|
})
|
|
145
|
+
|
|
146
|
+
it("me: returns the current user's member object", async () => {
|
|
147
|
+
await meAction(undefined, {})
|
|
148
|
+
|
|
149
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
150
|
+
expect(output).toMatchObject({
|
|
151
|
+
user: {
|
|
152
|
+
id: 'user-1',
|
|
153
|
+
username: 'alice',
|
|
154
|
+
global_name: 'Alice Smith',
|
|
155
|
+
avatar: 'avatar-hash-1',
|
|
156
|
+
bot: false,
|
|
157
|
+
},
|
|
158
|
+
nick: 'AliceNick',
|
|
159
|
+
joined_at: '2024-01-15T10:00:00Z',
|
|
160
|
+
deaf: false,
|
|
161
|
+
mute: false,
|
|
162
|
+
flags: 0,
|
|
163
|
+
})
|
|
164
|
+
expect(clientGetMyGuildMemberSpy).toHaveBeenCalledWith('guild-1')
|
|
165
|
+
expect(clientListRolesSpy).toHaveBeenCalledWith('guild-1')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('me: returns roles resolved to names (not raw IDs)', async () => {
|
|
169
|
+
await meAction('guild-2', {})
|
|
170
|
+
|
|
171
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
172
|
+
expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['Admin', 'Member'])
|
|
173
|
+
expect(output.roles).not.toContain('role-high')
|
|
174
|
+
expect(clientGetMyGuildMemberSpy).toHaveBeenCalledWith('guild-2')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('me: roles are sorted by position descending', async () => {
|
|
178
|
+
await meAction(undefined, {})
|
|
179
|
+
|
|
180
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
181
|
+
expect(output.roles.map((role: { position: number }) => role.position)).toEqual([10, 1])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('me: role IDs with no matching role definition are dropped', async () => {
|
|
185
|
+
await meAction(undefined, {})
|
|
186
|
+
|
|
187
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
188
|
+
expect(output.roles).toHaveLength(2)
|
|
189
|
+
expect(output.roles.map((role: { id: string }) => role.id)).not.toContain('role-missing')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('me: breaks equal-position ties by snowflake, ranking the smaller ID higher', async () => {
|
|
193
|
+
// given two roles sharing a position whose IDs collide once coerced to Number
|
|
194
|
+
clientGetMyGuildMemberSpy.mockResolvedValue({
|
|
195
|
+
user: { id: 'user-1', username: 'alice', global_name: 'Alice Smith', avatar: 'avatar-hash-1', bot: false },
|
|
196
|
+
nick: null,
|
|
197
|
+
roles: ['1234567890123456789', '1234567890123456788'],
|
|
198
|
+
joined_at: '2024-01-15T10:00:00Z',
|
|
199
|
+
deaf: false,
|
|
200
|
+
mute: false,
|
|
201
|
+
flags: 0,
|
|
202
|
+
})
|
|
203
|
+
clientListRolesSpy.mockResolvedValue([
|
|
204
|
+
{
|
|
205
|
+
id: '1234567890123456789',
|
|
206
|
+
name: 'Newer',
|
|
207
|
+
color: 0,
|
|
208
|
+
position: 5,
|
|
209
|
+
hoist: false,
|
|
210
|
+
managed: false,
|
|
211
|
+
mentionable: false,
|
|
212
|
+
permissions: '0',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: '1234567890123456788',
|
|
216
|
+
name: 'Older',
|
|
217
|
+
color: 0,
|
|
218
|
+
position: 5,
|
|
219
|
+
hoist: false,
|
|
220
|
+
managed: false,
|
|
221
|
+
mentionable: false,
|
|
222
|
+
permissions: '0',
|
|
223
|
+
},
|
|
224
|
+
])
|
|
225
|
+
|
|
226
|
+
await meAction(undefined, {})
|
|
227
|
+
|
|
228
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
229
|
+
expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['Older', 'Newer'])
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('me: ranks by position before falling back to the snowflake', async () => {
|
|
233
|
+
clientGetMyGuildMemberSpy.mockResolvedValue({
|
|
234
|
+
user: { id: 'user-1', username: 'alice', global_name: 'Alice Smith', avatar: 'avatar-hash-1', bot: false },
|
|
235
|
+
nick: null,
|
|
236
|
+
roles: ['100', '200'],
|
|
237
|
+
joined_at: '2024-01-15T10:00:00Z',
|
|
238
|
+
deaf: false,
|
|
239
|
+
mute: false,
|
|
240
|
+
flags: 0,
|
|
241
|
+
})
|
|
242
|
+
clientListRolesSpy.mockResolvedValue([
|
|
243
|
+
{
|
|
244
|
+
id: '100',
|
|
245
|
+
name: 'Low',
|
|
246
|
+
color: 0,
|
|
247
|
+
position: 1,
|
|
248
|
+
hoist: false,
|
|
249
|
+
managed: false,
|
|
250
|
+
mentionable: false,
|
|
251
|
+
permissions: '0',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: '200',
|
|
255
|
+
name: 'High',
|
|
256
|
+
color: 0,
|
|
257
|
+
position: 9,
|
|
258
|
+
hoist: false,
|
|
259
|
+
managed: false,
|
|
260
|
+
mentionable: false,
|
|
261
|
+
permissions: '0',
|
|
262
|
+
},
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
await meAction(undefined, {})
|
|
266
|
+
|
|
267
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
268
|
+
expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['High', 'Low'])
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('me: preserves the raw role IDs even when a definition is unresolvable', async () => {
|
|
272
|
+
await meAction(undefined, {})
|
|
273
|
+
|
|
274
|
+
const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
|
|
275
|
+
expect(output.role_ids).toEqual(['role-low', 'role-missing', 'role-high'])
|
|
276
|
+
})
|
|
@@ -5,6 +5,7 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
5
5
|
|
|
6
6
|
import { DiscordClient } from '../client'
|
|
7
7
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
8
|
+
import type { DiscordRole } from '../types'
|
|
8
9
|
|
|
9
10
|
async function searchAction(
|
|
10
11
|
guildId: string,
|
|
@@ -46,6 +47,80 @@ async function searchAction(
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export async function meAction(guildId: string | undefined, options: { pretty?: boolean }): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const credManager = new DiscordCredentialManager()
|
|
53
|
+
const config = await credManager.load()
|
|
54
|
+
|
|
55
|
+
if (!config.token) {
|
|
56
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const targetGuildId = guildId ?? config.current_server
|
|
61
|
+
if (!targetGuildId) {
|
|
62
|
+
console.log(formatOutput({ error: 'No current server set. Run "server switch <id>" first.' }, options.pretty))
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
67
|
+
const [member, roles] = await Promise.all([client.getMyGuildMember(targetGuildId), client.listRoles(targetGuildId)])
|
|
68
|
+
const rolesById = new Map<string, DiscordRole>(roles.map((role) => [role.id, role]))
|
|
69
|
+
const resolvedRoles = member.roles
|
|
70
|
+
.map((roleId) => rolesById.get(roleId))
|
|
71
|
+
.filter((role): role is DiscordRole => role !== undefined)
|
|
72
|
+
.sort(compareRolesByHierarchy)
|
|
73
|
+
.map((role) => ({
|
|
74
|
+
id: role.id,
|
|
75
|
+
name: role.name,
|
|
76
|
+
color: role.color,
|
|
77
|
+
position: role.position,
|
|
78
|
+
hoist: role.hoist,
|
|
79
|
+
managed: role.managed,
|
|
80
|
+
mentionable: role.mentionable,
|
|
81
|
+
permissions: role.permissions,
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
const output = {
|
|
85
|
+
user: {
|
|
86
|
+
id: member.user.id,
|
|
87
|
+
username: member.user.username,
|
|
88
|
+
global_name: member.user.global_name,
|
|
89
|
+
avatar: member.user.avatar,
|
|
90
|
+
bot: member.user.bot,
|
|
91
|
+
},
|
|
92
|
+
nick: member.nick,
|
|
93
|
+
role_ids: member.roles,
|
|
94
|
+
roles: resolvedRoles,
|
|
95
|
+
joined_at: member.joined_at,
|
|
96
|
+
deaf: member.deaf,
|
|
97
|
+
mute: member.mute,
|
|
98
|
+
flags: member.flags,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(formatOutput(output, options.pretty))
|
|
102
|
+
} catch (error) {
|
|
103
|
+
handleError(error as Error)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Discord ranks roles by descending position and breaks ties on the snowflake,
|
|
108
|
+
// where the smaller ID is the higher role. Snowflakes exceed Number.MAX_SAFE_INTEGER,
|
|
109
|
+
// so they are compared as BigInt rather than subtracted.
|
|
110
|
+
function compareRolesByHierarchy(left: DiscordRole, right: DiscordRole): number {
|
|
111
|
+
if (left.position !== right.position) {
|
|
112
|
+
return right.position - left.position
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const leftId = BigInt(left.id)
|
|
116
|
+
const rightId = BigInt(right.id)
|
|
117
|
+
if (leftId === rightId) {
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return leftId < rightId ? -1 : 1
|
|
122
|
+
}
|
|
123
|
+
|
|
49
124
|
export const memberCommand = new Command('member')
|
|
50
125
|
.description('Member commands')
|
|
51
126
|
.addCommand(
|
|
@@ -57,3 +132,10 @@ export const memberCommand = new Command('member')
|
|
|
57
132
|
.option('--pretty', 'Pretty print JSON output')
|
|
58
133
|
.action(searchAction),
|
|
59
134
|
)
|
|
135
|
+
.addCommand(
|
|
136
|
+
new Command('me')
|
|
137
|
+
.description('Get your own membership and roles in a guild')
|
|
138
|
+
.argument('[guild-id]', 'Guild ID (defaults to current server)')
|
|
139
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
140
|
+
.action(meAction),
|
|
141
|
+
)
|
|
@@ -207,6 +207,38 @@ export interface DiscordGuildMember {
|
|
|
207
207
|
flags: number
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
export interface DiscordRoleColors {
|
|
211
|
+
primary_color: number
|
|
212
|
+
secondary_color: number | null
|
|
213
|
+
tertiary_color: number | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface DiscordRoleTags {
|
|
217
|
+
bot_id?: string
|
|
218
|
+
integration_id?: string
|
|
219
|
+
premium_subscriber?: null
|
|
220
|
+
subscription_listing_id?: string
|
|
221
|
+
available_for_purchase?: null
|
|
222
|
+
guild_connections?: null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface DiscordRole {
|
|
226
|
+
id: string
|
|
227
|
+
name: string
|
|
228
|
+
/** @deprecated Discord still returns this; prefer {@link DiscordRoleColors.primary_color}. */
|
|
229
|
+
color: number
|
|
230
|
+
colors: DiscordRoleColors
|
|
231
|
+
hoist: boolean
|
|
232
|
+
position: number
|
|
233
|
+
permissions: string
|
|
234
|
+
managed: boolean
|
|
235
|
+
mentionable: boolean
|
|
236
|
+
flags: number
|
|
237
|
+
icon?: string | null
|
|
238
|
+
unicode_emoji?: string | null
|
|
239
|
+
tags?: DiscordRoleTags
|
|
240
|
+
}
|
|
241
|
+
|
|
210
242
|
export interface DiscordUserProfile {
|
|
211
243
|
user: DiscordUser & { bio?: string }
|
|
212
244
|
connected_accounts: Array<{
|
|
@@ -805,12 +805,12 @@ describe('KakaoTalkClient', () => {
|
|
|
805
805
|
|
|
806
806
|
describe('getMessages', () => {
|
|
807
807
|
it('falls back to SYNCMSG when MCHATLOGS succeeds with an empty result', async () => {
|
|
808
|
+
const loginResult = structuredClone(DEFAULT_LOGIN_RESULT)
|
|
809
|
+
loginResult.chatDatas[0]!.ll = makeLong(10)
|
|
810
|
+
mockLogin.mockResolvedValueOnce(loginResult)
|
|
808
811
|
mockGetChatLogs.mockResolvedValueOnce({
|
|
809
812
|
body: { status: 0, chatLogs: [], eof: true },
|
|
810
813
|
})
|
|
811
|
-
mockGetChatInfo.mockResolvedValueOnce({
|
|
812
|
-
body: { status: 0, l: makeLong(10) },
|
|
813
|
-
})
|
|
814
814
|
mockSyncMessages.mockResolvedValueOnce({
|
|
815
815
|
body: {
|
|
816
816
|
status: 0,
|
|
@@ -826,8 +826,100 @@ describe('KakaoTalkClient', () => {
|
|
|
826
826
|
|
|
827
827
|
expect(messages).toHaveLength(1)
|
|
828
828
|
expect(messages[0]?.message).toBe('fallback')
|
|
829
|
-
expect(mockGetChatInfo).
|
|
829
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
830
830
|
expect(mockSyncMessages).toHaveBeenCalledTimes(1)
|
|
831
|
+
expect(mockSyncMessages.mock.calls[0]?.[3]?.toString()).toBe('10')
|
|
832
|
+
|
|
833
|
+
client.close()
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('does not enter or sync a room when the MCHATLOGS cursor is already current', async () => {
|
|
837
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
838
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
842
|
+
const messages = await client.getMessages('100', { from: '999' })
|
|
843
|
+
|
|
844
|
+
expect(messages).toEqual([])
|
|
845
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
846
|
+
expect(mockSyncMessages).not.toHaveBeenCalled()
|
|
847
|
+
|
|
848
|
+
client.close()
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
it('uses SYNCMSG without entering the room when the chat-list watermark is ahead of the cursor', async () => {
|
|
852
|
+
const loginResult = structuredClone(DEFAULT_LOGIN_RESULT)
|
|
853
|
+
loginResult.chatDatas[0]!.ll = makeLong(10)
|
|
854
|
+
mockLogin.mockResolvedValueOnce(loginResult)
|
|
855
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
856
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
857
|
+
})
|
|
858
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
859
|
+
body: {
|
|
860
|
+
status: 0,
|
|
861
|
+
isOK: true,
|
|
862
|
+
chatLogs: [
|
|
863
|
+
{ logId: makeLong(10), chatId: 100, type: 1, authorId: 42, message: 'incremental', sendAt: 1700000001 },
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
869
|
+
const messages = await client.getMessages('100', { from: '9' })
|
|
870
|
+
|
|
871
|
+
expect(messages).toHaveLength(1)
|
|
872
|
+
expect(messages[0]?.message).toBe('incremental')
|
|
873
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
874
|
+
expect(mockSyncMessages.mock.calls[0]?.[2]?.toString()).toBe('9')
|
|
875
|
+
expect(mockSyncMessages.mock.calls[0]?.[3]?.toString()).toBe('10')
|
|
876
|
+
|
|
877
|
+
client.close()
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
it('uses the last-message log ID when the primary chat-list watermark is missing', async () => {
|
|
881
|
+
const loginResult = structuredClone(DEFAULT_LOGIN_RESULT)
|
|
882
|
+
const chat = loginResult.chatDatas[0]! as Record<string, unknown>
|
|
883
|
+
delete chat.ll
|
|
884
|
+
chat.l = { logId: makeLong(12) }
|
|
885
|
+
mockLogin.mockResolvedValueOnce(loginResult)
|
|
886
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
887
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
888
|
+
})
|
|
889
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
890
|
+
body: {
|
|
891
|
+
status: 0,
|
|
892
|
+
isOK: true,
|
|
893
|
+
chatLogs: [
|
|
894
|
+
{ logId: makeLong(12), chatId: 100, type: 1, authorId: 42, message: 'last-log', sendAt: 1700000001 },
|
|
895
|
+
],
|
|
896
|
+
},
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
900
|
+
const messages = await client.getMessages('100', { from: '11' })
|
|
901
|
+
|
|
902
|
+
expect(messages).toHaveLength(1)
|
|
903
|
+
expect(messages[0]?.message).toBe('last-log')
|
|
904
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
905
|
+
expect(mockSyncMessages.mock.calls[0]?.[2]?.toString()).toBe('11')
|
|
906
|
+
expect(mockSyncMessages.mock.calls[0]?.[3]?.toString()).toBe('12')
|
|
907
|
+
|
|
908
|
+
client.close()
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('returns empty without entering the room when no chat-list watermark is available', async () => {
|
|
912
|
+
mockLogin.mockResolvedValueOnce({ ...structuredClone(DEFAULT_LOGIN_RESULT), chatDatas: [] })
|
|
913
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
914
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
918
|
+
const messages = await client.getMessages('100')
|
|
919
|
+
|
|
920
|
+
expect(messages).toEqual([])
|
|
921
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
922
|
+
expect(mockSyncMessages).not.toHaveBeenCalled()
|
|
831
923
|
|
|
832
924
|
client.close()
|
|
833
925
|
})
|
|
@@ -242,6 +242,14 @@ function getOpenLinkId(chat: ChatData): Long | null {
|
|
|
242
242
|
return bsonToLong(chat.li) ?? null
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function getLoginMaxLogId(loginResult: LoginListResponse, chatId: string): Long | null {
|
|
246
|
+
const chat = (loginResult.chatDatas ?? []).find((entry) => longToString(entry.c) === chatId)
|
|
247
|
+
if (!chat) return null
|
|
248
|
+
|
|
249
|
+
const lastLog = chat.l as Record<string, unknown> | null | undefined
|
|
250
|
+
return bsonToLong(chat.ll) ?? bsonToLong(lastLog?.logId) ?? null
|
|
251
|
+
}
|
|
252
|
+
|
|
245
253
|
function matchesSearch(chat: ChatData, term: string): boolean {
|
|
246
254
|
const names = (chat.k ?? []) as string[]
|
|
247
255
|
const lower = term.toLowerCase()
|
|
@@ -827,7 +835,7 @@ export class KakaoTalkClient {
|
|
|
827
835
|
}
|
|
828
836
|
|
|
829
837
|
async getMessages(chatId: string, options?: { count?: number; from?: string }): Promise<KakaoMessage[]> {
|
|
830
|
-
return this.executeWithReconnect(async ({ session }) => {
|
|
838
|
+
return this.executeWithReconnect(async ({ session, loginResult }) => {
|
|
831
839
|
try {
|
|
832
840
|
const count = options?.count ?? 20
|
|
833
841
|
const cursor = options?.from ? parseLong(options.from) : undefined
|
|
@@ -880,10 +888,13 @@ export class KakaoTalkClient {
|
|
|
880
888
|
return formatMessages(allMessages, count, chatId, this.nameCache)
|
|
881
889
|
}
|
|
882
890
|
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const maxLogId =
|
|
891
|
+
// CHATONROOM represents entering a room and can advance unread state on
|
|
892
|
+
// the server. Use the login-time chat-list watermark for the read-only
|
|
893
|
+
// SYNCMSG fallback instead, and skip sync when the cursor is current.
|
|
894
|
+
const maxLogId = getLoginMaxLogId(loginResult, chatId)
|
|
895
|
+
if (!maxLogId || maxLogId.lessThanOrEqual(cur)) {
|
|
896
|
+
return []
|
|
897
|
+
}
|
|
887
898
|
|
|
888
899
|
let reachedEnd = false
|
|
889
900
|
for (let page = 0; page < MAX_PAGES; page++) {
|