agent-messenger 2.13.0 → 2.14.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/discordbot/index.d.ts +2 -1
- package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/index.js.map +1 -1
- package/dist/src/platforms/discordbot/listener.d.ts +4 -3
- package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/listener.js.map +1 -1
- package/dist/src/platforms/discordbot/types.d.ts +21 -0
- package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +11 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +49 -0
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.js +21 -1
- package/dist/src/platforms/kakaotalk/commands/message.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +6 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +14 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +12 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +6 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/slackbot/index.d.ts +2 -2
- package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/index.js +1 -1
- package/dist/src/platforms/slackbot/index.js.map +1 -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-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +25 -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-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discordbot/index.test.ts +76 -0
- package/src/platforms/discordbot/index.ts +3 -0
- package/src/platforms/discordbot/listener.test.ts +41 -0
- package/src/platforms/discordbot/listener.ts +5 -1
- package/src/platforms/discordbot/types.ts +23 -1
- package/src/platforms/kakaotalk/client.test.ts +171 -0
- package/src/platforms/kakaotalk/client.ts +59 -1
- package/src/platforms/kakaotalk/commands/message.test.ts +79 -0
- package/src/platforms/kakaotalk/commands/message.ts +28 -0
- package/src/platforms/kakaotalk/protocol/session.ts +14 -0
- package/src/platforms/kakaotalk/types.ts +14 -0
- package/src/platforms/slackbot/index.test.ts +10 -0
- package/src/platforms/slackbot/index.ts +4 -0
|
@@ -13,6 +13,7 @@ const mockGetAllMembers = mock(() => Promise.resolve({}))
|
|
|
13
13
|
const mockGetMembersByIds = mock(() => Promise.resolve({}))
|
|
14
14
|
const mockSyncMessages = mock(() => Promise.resolve({}))
|
|
15
15
|
const mockSendMessage = mock(() => Promise.resolve({}))
|
|
16
|
+
const mockMarkRead = mock(() => Promise.resolve({}))
|
|
16
17
|
const mockClose = mock(() => {})
|
|
17
18
|
const mockOnClose = mock((_handler: () => void) => {})
|
|
18
19
|
const mockOnPush = mock((_handler: (packet: unknown) => void) => {})
|
|
@@ -29,6 +30,7 @@ mock.module('./protocol/session', () => ({
|
|
|
29
30
|
getMembersByIds = mockGetMembersByIds
|
|
30
31
|
syncMessages = mockSyncMessages
|
|
31
32
|
sendMessage = mockSendMessage
|
|
33
|
+
markRead = mockMarkRead
|
|
32
34
|
close = mockClose
|
|
33
35
|
onClose = mockOnClose
|
|
34
36
|
onPush = mockOnPush
|
|
@@ -50,6 +52,7 @@ function resetAllMocks() {
|
|
|
50
52
|
mockGetMembersByIds.mockReset()
|
|
51
53
|
mockSyncMessages.mockReset()
|
|
52
54
|
mockSendMessage.mockReset()
|
|
55
|
+
mockMarkRead.mockReset()
|
|
53
56
|
mockClose.mockReset()
|
|
54
57
|
mockOnClose.mockReset()
|
|
55
58
|
mockOnPush.mockReset()
|
|
@@ -910,6 +913,174 @@ describe('KakaoTalkClient', () => {
|
|
|
910
913
|
})
|
|
911
914
|
})
|
|
912
915
|
|
|
916
|
+
describe('markRead', () => {
|
|
917
|
+
it('calls session.markRead with parsed chatId/watermark and no linkId for normal chat', async () => {
|
|
918
|
+
// given
|
|
919
|
+
mockMarkRead.mockResolvedValueOnce({ statusCode: 0, body: { status: 0 } })
|
|
920
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
921
|
+
|
|
922
|
+
// when
|
|
923
|
+
const result = await client.markRead('100', '42')
|
|
924
|
+
|
|
925
|
+
// then
|
|
926
|
+
expect(mockMarkRead).toHaveBeenCalledTimes(1)
|
|
927
|
+
const [chatIdArg, watermarkArg, linkIdArg] = mockMarkRead.mock.calls[0] as [
|
|
928
|
+
{ toString(): string },
|
|
929
|
+
{ toString(): string },
|
|
930
|
+
unknown,
|
|
931
|
+
]
|
|
932
|
+
expect(chatIdArg.toString()).toBe('100')
|
|
933
|
+
expect(watermarkArg.toString()).toBe('42')
|
|
934
|
+
expect(linkIdArg).toBeUndefined()
|
|
935
|
+
expect(result).toEqual({ success: true, status_code: 0, chat_id: '100', watermark: '42' })
|
|
936
|
+
|
|
937
|
+
client.close()
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
it('forwards linkId when caller passes it (open chat)', async () => {
|
|
941
|
+
// given
|
|
942
|
+
mockMarkRead.mockResolvedValueOnce({ statusCode: 0, body: { status: 0 } })
|
|
943
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
944
|
+
|
|
945
|
+
// when
|
|
946
|
+
await client.markRead('200', '999', { linkId: '77777' })
|
|
947
|
+
|
|
948
|
+
// then
|
|
949
|
+
const [, , linkIdArg] = mockMarkRead.mock.calls[0] as [unknown, unknown, { toString(): string }]
|
|
950
|
+
expect(linkIdArg).toBeDefined()
|
|
951
|
+
expect(linkIdArg.toString()).toBe('77777')
|
|
952
|
+
|
|
953
|
+
client.close()
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
it('reports success when body.status is absent (treated as 0)', async () => {
|
|
957
|
+
// given
|
|
958
|
+
mockMarkRead.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
959
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
960
|
+
|
|
961
|
+
// when
|
|
962
|
+
const result = await client.markRead('100', '42')
|
|
963
|
+
|
|
964
|
+
// then
|
|
965
|
+
expect(result).toEqual({ success: true, status_code: 0, chat_id: '100', watermark: '42' })
|
|
966
|
+
|
|
967
|
+
client.close()
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
it('reports failure from body.status (not transport statusCode) - the open-chat-missing-link case', async () => {
|
|
971
|
+
// given
|
|
972
|
+
mockMarkRead.mockResolvedValueOnce({ statusCode: 0, body: { status: -500 } })
|
|
973
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
974
|
+
|
|
975
|
+
// when
|
|
976
|
+
const result = await client.markRead('100', '42')
|
|
977
|
+
|
|
978
|
+
// then
|
|
979
|
+
expect(result).toEqual({ success: false, status_code: -500, chat_id: '100', watermark: '42' })
|
|
980
|
+
|
|
981
|
+
client.close()
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
it('throws (so executeWithReconnect retries) on transport-level statusCode != 0', async () => {
|
|
985
|
+
// given - simulates LocoConnection.handleClose synthetic packet
|
|
986
|
+
mockMarkRead.mockRejectedValue(new Error('NOTIREAD failed: statusCode=-1'))
|
|
987
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
988
|
+
|
|
989
|
+
// when / then
|
|
990
|
+
try {
|
|
991
|
+
await client.markRead('100', '42')
|
|
992
|
+
throw new Error('expected to throw')
|
|
993
|
+
} catch (e) {
|
|
994
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
995
|
+
expect((e as KakaoTalkError).code).toBe('mark_read_failed')
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
client.close()
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it('treats synthetic disconnect packet (statusCode: -1) as transport failure (throws)', async () => {
|
|
1002
|
+
// given
|
|
1003
|
+
mockMarkRead.mockResolvedValueOnce({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1004
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1005
|
+
|
|
1006
|
+
// when / then
|
|
1007
|
+
try {
|
|
1008
|
+
await client.markRead('100', '42')
|
|
1009
|
+
throw new Error('expected to throw')
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1012
|
+
expect((e as KakaoTalkError).code).toBe('mark_read_failed')
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
client.close()
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('throws KakaoTalkError(invalid_chat_id) for non-numeric chatId', async () => {
|
|
1019
|
+
// given
|
|
1020
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1021
|
+
|
|
1022
|
+
// when / then
|
|
1023
|
+
try {
|
|
1024
|
+
await client.markRead('not-a-number', '42')
|
|
1025
|
+
throw new Error('expected to throw')
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1028
|
+
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
client.close()
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
it('throws KakaoTalkError(invalid_log_id) for non-numeric logId', async () => {
|
|
1035
|
+
// given
|
|
1036
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1037
|
+
|
|
1038
|
+
// when / then
|
|
1039
|
+
try {
|
|
1040
|
+
await client.markRead('100', 'not-a-number')
|
|
1041
|
+
throw new Error('expected to throw')
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1044
|
+
expect((e as KakaoTalkError).code).toBe('invalid_log_id')
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
client.close()
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
it('throws KakaoTalkError(invalid_link_id) for non-numeric linkId', async () => {
|
|
1051
|
+
// given
|
|
1052
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1053
|
+
|
|
1054
|
+
// when / then
|
|
1055
|
+
try {
|
|
1056
|
+
await client.markRead('100', '42', { linkId: 'not-a-number' })
|
|
1057
|
+
throw new Error('expected to throw')
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1060
|
+
expect((e as KakaoTalkError).code).toBe('invalid_link_id')
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
client.close()
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
it('wraps transport errors as KakaoTalkError(mark_read_failed)', async () => {
|
|
1067
|
+
// given
|
|
1068
|
+
mockMarkRead.mockRejectedValue(new Error('Socket closed'))
|
|
1069
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1070
|
+
|
|
1071
|
+
// when / then
|
|
1072
|
+
try {
|
|
1073
|
+
await client.markRead('100', '42')
|
|
1074
|
+
throw new Error('expected to throw')
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1077
|
+
expect((e as KakaoTalkError).code).toBe('mark_read_failed')
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
client.close()
|
|
1081
|
+
})
|
|
1082
|
+
})
|
|
1083
|
+
|
|
913
1084
|
describe('getMembers / getMembersByIds', () => {
|
|
914
1085
|
it('returns formatted members from GETMEM with normalized fields', async () => {
|
|
915
1086
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
@@ -10,7 +10,15 @@ import { warn } from '@/shared/utils/stderr'
|
|
|
10
10
|
import { LANG, PC_OS_NAME, getLocoDeviceConfig } from './protocol/config'
|
|
11
11
|
import { LocoSession } from './protocol/session'
|
|
12
12
|
import type { ChatListResponse, LocoPacket, LoginListResponse, SyncState } from './protocol/types'
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
KakaoChat,
|
|
15
|
+
KakaoDeviceType,
|
|
16
|
+
KakaoMarkReadResult,
|
|
17
|
+
KakaoMember,
|
|
18
|
+
KakaoMessage,
|
|
19
|
+
KakaoProfile,
|
|
20
|
+
KakaoSendResult,
|
|
21
|
+
} from './types'
|
|
14
22
|
|
|
15
23
|
export type KakaoSessionEvent =
|
|
16
24
|
| { type: 'connected'; userId: string }
|
|
@@ -128,6 +136,22 @@ function parseUserId(userId: string): Long {
|
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
function parseLogId(logId: string): Long {
|
|
140
|
+
try {
|
|
141
|
+
return parseLong(logId)
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
throw new KakaoTalkError(`Invalid logId: ${logId}`, 'invalid_log_id', { cause })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseLinkId(linkId: string): Long {
|
|
148
|
+
try {
|
|
149
|
+
return parseLong(linkId)
|
|
150
|
+
} catch (cause) {
|
|
151
|
+
throw new KakaoTalkError(`Invalid linkId: ${linkId}`, 'invalid_link_id', { cause })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
function formatChat(chat: ChatData, title: string | null, nameCache: MemberNameCache): KakaoChat {
|
|
132
156
|
const memberNames = (chat.k ?? []) as string[]
|
|
133
157
|
const lastLog = chat.l as Record<string, unknown> | null
|
|
@@ -858,6 +882,40 @@ export class KakaoTalkClient {
|
|
|
858
882
|
})
|
|
859
883
|
}
|
|
860
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Advance the read watermark for `chatId` up to and including `logId`.
|
|
887
|
+
* The caller decides open vs normal: pass `opts.linkId` for open chats
|
|
888
|
+
* (오픈채팅) and omit it for normal chats. Open chats without a `linkId`
|
|
889
|
+
* are rejected by the server with a non-zero `body.status` — this method
|
|
890
|
+
* does not auto-detect.
|
|
891
|
+
*/
|
|
892
|
+
async markRead(chatId: string, logId: string, opts?: { linkId?: string }): Promise<KakaoMarkReadResult> {
|
|
893
|
+
const parsedChatId = parseChatId(chatId)
|
|
894
|
+
const parsedWatermark = parseLogId(logId)
|
|
895
|
+
const parsedLinkId = opts?.linkId !== undefined ? parseLinkId(opts.linkId) : undefined
|
|
896
|
+
|
|
897
|
+
return this.executeWithReconnect(async ({ session }) => {
|
|
898
|
+
try {
|
|
899
|
+
const response = await session.markRead(parsedChatId, parsedWatermark, parsedLinkId)
|
|
900
|
+
// Throw on transport-level failure (incl. synthetic { statusCode: -1 }
|
|
901
|
+
// from LocoConnection.handleClose) so executeWithReconnect retries on
|
|
902
|
+
// a fresh session. The NOTIREAD command result lives in body.status.
|
|
903
|
+
if (response.statusCode !== 0) {
|
|
904
|
+
throw new Error(`NOTIREAD failed: statusCode=${response.statusCode}`)
|
|
905
|
+
}
|
|
906
|
+
const bodyStatus = typeof response.body.status === 'number' ? response.body.status : 0
|
|
907
|
+
return {
|
|
908
|
+
success: bodyStatus === 0,
|
|
909
|
+
status_code: bodyStatus,
|
|
910
|
+
chat_id: chatId,
|
|
911
|
+
watermark: logId,
|
|
912
|
+
}
|
|
913
|
+
} catch (error) {
|
|
914
|
+
throw wrapError(error, 'mark_read_failed')
|
|
915
|
+
}
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
|
|
861
919
|
async getProfile(): Promise<KakaoProfile> {
|
|
862
920
|
this.ensureAuth()
|
|
863
921
|
try {
|
|
@@ -12,9 +12,16 @@ const mockGetMessages = mock(() =>
|
|
|
12
12
|
|
|
13
13
|
const mockSendMessage = mock(() => Promise.resolve({ log_id: '2', message: 'Hi there', created_at: 2000 }))
|
|
14
14
|
|
|
15
|
+
const mockMarkRead = mock(() =>
|
|
16
|
+
Promise.resolve({ success: true, status_code: 0, chat_id: 'chat-123', watermark: '42' }),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const originalExit = process.exit
|
|
20
|
+
|
|
15
21
|
const mockClient = {
|
|
16
22
|
getMessages: mockGetMessages,
|
|
17
23
|
sendMessage: mockSendMessage,
|
|
24
|
+
markRead: mockMarkRead,
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
mock.module('./shared', () => ({
|
|
@@ -30,6 +37,7 @@ describe('message commands', () => {
|
|
|
30
37
|
mockWithKakaoClient.mockReset()
|
|
31
38
|
mockGetMessages.mockReset()
|
|
32
39
|
mockSendMessage.mockReset()
|
|
40
|
+
mockMarkRead.mockReset()
|
|
33
41
|
|
|
34
42
|
mockWithKakaoClient.mockImplementation(async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
|
|
35
43
|
return fn(mockClient)
|
|
@@ -38,6 +46,9 @@ describe('message commands', () => {
|
|
|
38
46
|
Promise.resolve([{ log_id: '1', message: 'Hello', sender_id: 'user-1', created_at: 1000 }]),
|
|
39
47
|
)
|
|
40
48
|
mockSendMessage.mockImplementation(() => Promise.resolve({ log_id: '2', message: 'Hi there', created_at: 2000 }))
|
|
49
|
+
mockMarkRead.mockImplementation(() =>
|
|
50
|
+
Promise.resolve({ success: true, status_code: 0, chat_id: 'chat-123', watermark: '42' }),
|
|
51
|
+
)
|
|
41
52
|
|
|
42
53
|
consoleLogSpy = mock((..._args: unknown[]) => {})
|
|
43
54
|
console.log = consoleLogSpy
|
|
@@ -45,6 +56,7 @@ describe('message commands', () => {
|
|
|
45
56
|
|
|
46
57
|
afterEach(() => {
|
|
47
58
|
console.log = originalConsoleLog
|
|
59
|
+
process.exit = originalExit
|
|
48
60
|
})
|
|
49
61
|
|
|
50
62
|
describe('list', () => {
|
|
@@ -101,4 +113,71 @@ describe('message commands', () => {
|
|
|
101
113
|
)
|
|
102
114
|
})
|
|
103
115
|
})
|
|
116
|
+
|
|
117
|
+
describe('mark-read', () => {
|
|
118
|
+
it('calls markRead with chat-id and log-id, no opts for normal chat', async () => {
|
|
119
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42'], { from: 'user' })
|
|
120
|
+
|
|
121
|
+
expect(mockMarkRead).toHaveBeenCalledWith('chat-123', '42', undefined)
|
|
122
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
|
|
123
|
+
expect(output).toEqual({ success: true, status_code: 0, chat_id: 'chat-123', watermark: '42' })
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('forwards --link-id when provided (open chat)', async () => {
|
|
127
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42', '--link-id', '77777'], { from: 'user' })
|
|
128
|
+
|
|
129
|
+
expect(mockMarkRead).toHaveBeenCalledWith('chat-123', '42', { linkId: '77777' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('--pretty prints pretty-formatted JSON (consistent with list/send)', async () => {
|
|
133
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42', '--pretty'], { from: 'user' })
|
|
134
|
+
|
|
135
|
+
const printed = consoleLogSpy.mock.calls[0][0] as string
|
|
136
|
+
expect(printed).toContain('\n')
|
|
137
|
+
expect(JSON.parse(printed)).toEqual({
|
|
138
|
+
success: true,
|
|
139
|
+
status_code: 0,
|
|
140
|
+
chat_id: 'chat-123',
|
|
141
|
+
watermark: '42',
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('exits non-zero when result.success is false (e.g. open chat missing --link-id)', async () => {
|
|
146
|
+
const exitSpy = mock((_code?: number): never => {
|
|
147
|
+
throw new Error('process.exit called')
|
|
148
|
+
})
|
|
149
|
+
process.exit = exitSpy as unknown as typeof process.exit
|
|
150
|
+
mockMarkRead.mockImplementationOnce(() =>
|
|
151
|
+
Promise.resolve({ success: false, status_code: -500, chat_id: 'chat-123', watermark: '42' }),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42'], { from: 'user' })
|
|
156
|
+
} catch {
|
|
157
|
+
// process.exit stub throws to abort the action
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('exits zero on success', async () => {
|
|
164
|
+
const exitSpy = mock((_code?: number): never => {
|
|
165
|
+
throw new Error('process.exit called')
|
|
166
|
+
})
|
|
167
|
+
process.exit = exitSpy as unknown as typeof process.exit
|
|
168
|
+
|
|
169
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42'], { from: 'user' })
|
|
170
|
+
|
|
171
|
+
expect(exitSpy).not.toHaveBeenCalled()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('passes account option to withKakaoClient', async () => {
|
|
175
|
+
await messageCommand.parseAsync(['mark-read', 'chat-123', '42', '--account', 'my-account'], { from: 'user' })
|
|
176
|
+
|
|
177
|
+
expect(mockWithKakaoClient).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({ account: 'my-account' }),
|
|
179
|
+
expect.any(Function),
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
104
183
|
})
|
|
@@ -33,6 +33,24 @@ async function sendAction(
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
async function markReadAction(
|
|
37
|
+
chatId: string,
|
|
38
|
+
logId: string,
|
|
39
|
+
options: { account?: string; linkId?: string; pretty?: boolean },
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
const result = await withKakaoClient(options, (client) =>
|
|
43
|
+
client.markRead(chatId, logId, options.linkId !== undefined ? { linkId: options.linkId } : undefined),
|
|
44
|
+
)
|
|
45
|
+
console.log(formatOutput(result, options.pretty))
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
handleError(error as Error)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
export const messageCommand = new Command('message')
|
|
37
55
|
.description('KakaoTalk message commands')
|
|
38
56
|
.addCommand(
|
|
@@ -54,3 +72,13 @@ export const messageCommand = new Command('message')
|
|
|
54
72
|
.option('--pretty', 'Pretty print JSON output')
|
|
55
73
|
.action(sendAction),
|
|
56
74
|
)
|
|
75
|
+
.addCommand(
|
|
76
|
+
new Command('mark-read')
|
|
77
|
+
.description('Mark messages in a chat room as read up to a given log ID')
|
|
78
|
+
.argument('<chat-id>', 'Chat room ID')
|
|
79
|
+
.argument('<log-id>', 'Watermark log ID (mark messages up to and including this log_id as read)')
|
|
80
|
+
.option('--account <id>', 'Use a specific KakaoTalk account')
|
|
81
|
+
.option('--link-id <li>', 'Open-chat link ID (REQUIRED for open chats / 오픈채팅)')
|
|
82
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
83
|
+
.action(markReadAction),
|
|
84
|
+
)
|
|
@@ -203,6 +203,20 @@ export class LocoSession {
|
|
|
203
203
|
})
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Advance the server-side read watermark for a chat (NOTIREAD). Open chats
|
|
208
|
+
* supply `linkId`; normal chats must omit it (the wire `li` key must be
|
|
209
|
+
* absent, not null/0).
|
|
210
|
+
*/
|
|
211
|
+
async markRead(chatId: Long, watermark: Long, linkId?: Long): Promise<LocoPacket> {
|
|
212
|
+
if (!this.connection) throw new Error('Not connected')
|
|
213
|
+
const body: Record<string, unknown> = { chatId, watermark }
|
|
214
|
+
if (linkId !== undefined) {
|
|
215
|
+
body.li = linkId
|
|
216
|
+
}
|
|
217
|
+
return this.connection.sendPacket('NOTIREAD', body)
|
|
218
|
+
}
|
|
219
|
+
|
|
206
220
|
onPush(handler: (packet: LocoPacket) => void): void {
|
|
207
221
|
this.pushHandler = handler
|
|
208
222
|
if (this.connection) {
|
|
@@ -118,6 +118,13 @@ export interface KakaoSendResult {
|
|
|
118
118
|
sent_at: number
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export interface KakaoMarkReadResult {
|
|
122
|
+
success: boolean
|
|
123
|
+
status_code: number
|
|
124
|
+
chat_id: string
|
|
125
|
+
watermark: string
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
export const KakaoChatSchema = z.object({
|
|
122
129
|
chat_id: z.string(),
|
|
123
130
|
type: z.number(),
|
|
@@ -166,6 +173,13 @@ export const KakaoSendResultSchema = z.object({
|
|
|
166
173
|
sent_at: z.number(),
|
|
167
174
|
})
|
|
168
175
|
|
|
176
|
+
export const KakaoMarkReadResultSchema = z.object({
|
|
177
|
+
success: z.boolean(),
|
|
178
|
+
status_code: z.number(),
|
|
179
|
+
chat_id: z.string(),
|
|
180
|
+
watermark: z.string(),
|
|
181
|
+
})
|
|
182
|
+
|
|
169
183
|
export interface KakaoProfile {
|
|
170
184
|
user_id: string
|
|
171
185
|
nickname: string
|
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
SlackBotConfigSchema,
|
|
6
6
|
SlackBotCredentialManager,
|
|
7
7
|
SlackBotCredentialsSchema,
|
|
8
|
+
SlackBotEntrySchema,
|
|
8
9
|
SlackBotError,
|
|
9
10
|
SlackBotListener,
|
|
11
|
+
SlackBotWorkspaceSchema,
|
|
10
12
|
SlackChannelSchema,
|
|
11
13
|
SlackFileSchema,
|
|
12
14
|
SlackMessageSchema,
|
|
@@ -38,6 +40,14 @@ it('SlackBotCredentialsSchema is exported from barrel', () => {
|
|
|
38
40
|
expect(typeof SlackBotCredentialsSchema.parse).toBe('function')
|
|
39
41
|
})
|
|
40
42
|
|
|
43
|
+
it('SlackBotEntrySchema is exported from barrel', () => {
|
|
44
|
+
expect(typeof SlackBotEntrySchema.parse).toBe('function')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('SlackBotWorkspaceSchema is exported from barrel', () => {
|
|
48
|
+
expect(typeof SlackBotWorkspaceSchema.parse).toBe('function')
|
|
49
|
+
})
|
|
50
|
+
|
|
41
51
|
it('SlackChannelSchema is exported from barrel', () => {
|
|
42
52
|
expect(typeof SlackChannelSchema.parse).toBe('function')
|
|
43
53
|
})
|
|
@@ -5,7 +5,9 @@ export type { SlackBotListenerOptions } from './listener'
|
|
|
5
5
|
export type {
|
|
6
6
|
SlackBotConfig,
|
|
7
7
|
SlackBotCredentials,
|
|
8
|
+
SlackBotEntry,
|
|
8
9
|
SlackBotListenerEventMap,
|
|
10
|
+
SlackBotWorkspace,
|
|
9
11
|
SlackChannel,
|
|
10
12
|
SlackFile,
|
|
11
13
|
SlackMessage,
|
|
@@ -34,7 +36,9 @@ export type {
|
|
|
34
36
|
export {
|
|
35
37
|
SlackBotConfigSchema,
|
|
36
38
|
SlackBotCredentialsSchema,
|
|
39
|
+
SlackBotEntrySchema,
|
|
37
40
|
SlackBotError,
|
|
41
|
+
SlackBotWorkspaceSchema,
|
|
38
42
|
SlackChannelSchema,
|
|
39
43
|
SlackFileSchema,
|
|
40
44
|
SlackMessageSchema,
|