agent-messenger 2.34.0 → 2.36.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/auth/kakao-login.d.ts +2 -0
- package/dist/src/platforms/kakaotalk/auth/kakao-login.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/auth/kakao-login.js +2 -2
- package/dist/src/platforms/kakaotalk/auth/kakao-login.js.map +1 -1
- package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts +28 -0
- package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js +100 -0
- package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js.map +1 -0
- package/dist/src/platforms/kakaotalk/client.d.ts +2 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +14 -1
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +3 -1
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +1 -0
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +9 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.js +32 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +29 -20
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/types.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/protocol/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/teams/client.d.ts +4 -4
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +14 -14
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/chat.d.ts +2 -0
- package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/chat.js +7 -2
- package/dist/src/platforms/teams/commands/chat.js.map +1 -1
- package/dist/src/platforms/teams/commands/format.d.ts +3 -0
- package/dist/src/platforms/teams/commands/format.d.ts.map +1 -0
- package/dist/src/platforms/teams/commands/format.js +10 -0
- package/dist/src/platforms/teams/commands/format.js.map +1 -0
- package/dist/src/platforms/teams/commands/message.d.ts +1 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +4 -1
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/html-sanitizer.d.ts +17 -0
- package/dist/src/platforms/teams/html-sanitizer.d.ts.map +1 -0
- package/dist/src/platforms/teams/html-sanitizer.js +120 -0
- package/dist/src/platforms/teams/html-sanitizer.js.map +1 -0
- package/dist/src/platforms/teams/index.d.ts +1 -1
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +1 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +1 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/shared/utils/markdown-to-html.d.ts.map +1 -0
- package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.js +0 -0
- package/dist/src/shared/utils/markdown-to-html.js.map +1 -0
- package/docs/content/docs/cli/kakaotalk.mdx +10 -0
- package/docs/content/docs/sdk/kakaotalk.mdx +54 -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 +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 +27 -1
- package/skills/agent-kakaotalk/references/authentication.md +59 -0
- package/skills/agent-kakaotalk/references/common-patterns.md +8 -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 +44 -2
- package/skills/agent-teams/references/common-patterns.md +2 -2
- 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/auth/kakao-login.ts +2 -2
- package/src/platforms/kakaotalk/auth/oauth-refresh.test.ts +225 -0
- package/src/platforms/kakaotalk/auth/oauth-refresh.ts +144 -0
- package/src/platforms/kakaotalk/client.test.ts +55 -0
- package/src/platforms/kakaotalk/client.ts +19 -2
- package/src/platforms/kakaotalk/index.test.ts +11 -0
- package/src/platforms/kakaotalk/index.ts +8 -0
- package/src/platforms/kakaotalk/protocol/NOTICE.md +19 -4
- package/src/platforms/kakaotalk/protocol/login-response.test.ts +64 -0
- package/src/platforms/kakaotalk/protocol/login-response.ts +40 -0
- package/src/platforms/kakaotalk/protocol/session.ts +29 -21
- package/src/platforms/kakaotalk/protocol/types.ts +1 -0
- package/src/platforms/kakaotalk/types.ts +1 -0
- package/src/platforms/teams/client.test.ts +127 -0
- package/src/platforms/teams/client.ts +27 -14
- package/src/platforms/teams/commands/chat.test.ts +70 -1
- package/src/platforms/teams/commands/chat.ts +15 -4
- package/src/platforms/teams/commands/format.ts +12 -0
- package/src/platforms/teams/commands/message.test.ts +56 -0
- package/src/platforms/teams/commands/message.ts +6 -2
- package/src/platforms/teams/html-sanitizer.test.ts +123 -0
- package/src/platforms/teams/html-sanitizer.ts +134 -0
- package/src/platforms/teams/index.ts +1 -0
- package/src/platforms/teams/types.ts +2 -0
- package/src/platforms/webex/client.ts +2 -1
- package/src/{platforms/webex → shared/utils}/markdown-to-html.test.ts +7 -0
- package/src/{platforms/webex → shared/utils}/markdown-to-html.ts +0 -0
- package/dist/src/platforms/webex/markdown-to-html.d.ts.map +0 -1
- package/dist/src/platforms/webex/markdown-to-html.js.map +0 -1
- /package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.d.ts +0 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getLocoDeviceConfig,
|
|
16
16
|
} from './config'
|
|
17
17
|
import { LocoConnection } from './connection'
|
|
18
|
+
import { validateLoginListResponse } from './login-response'
|
|
18
19
|
import type { BookingResponse, CheckinResponse, LoginListResponse, LocoPacket, SyncState } from './types'
|
|
19
20
|
|
|
20
21
|
// LOCO opcode string emitted on the wire for typing indicator pulses.
|
|
@@ -81,27 +82,34 @@ export class LocoSession {
|
|
|
81
82
|
const lastTokenId = syncState ? new Long(syncState.lastTokenId.low, syncState.lastTokenId.high) : Long.fromNumber(0)
|
|
82
83
|
const lbk = syncState?.lbk ?? 0
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
try {
|
|
86
|
+
const response = await this.connection.sendPacket('LOGINLIST', {
|
|
87
|
+
appVer: deviceConfig.appVersion,
|
|
88
|
+
prtVer: PROTOCOL_VERSION,
|
|
89
|
+
os: deviceConfig.os,
|
|
90
|
+
lang: LANG,
|
|
91
|
+
dtype: DTYPE,
|
|
92
|
+
duuid: deviceUuid,
|
|
93
|
+
oauthToken,
|
|
94
|
+
ntype: 0,
|
|
95
|
+
MCCMNC: MCCMNC,
|
|
96
|
+
revision: syncState?.revision ?? 0,
|
|
97
|
+
chatIds,
|
|
98
|
+
maxIds,
|
|
99
|
+
lastTokenId,
|
|
100
|
+
lbk,
|
|
101
|
+
rp: new Binary(Buffer.from([0x00, 0x00, 0xff, 0xff, 0x00, 0x00])),
|
|
102
|
+
bg: false,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const loginResult = validateLoginListResponse(response)
|
|
106
|
+
this.startPing()
|
|
107
|
+
return loginResult
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.connection.close()
|
|
110
|
+
this.connection = null
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
private async bookAndCheckin(
|
|
@@ -328,6 +328,38 @@ describe('TeamsClient', () => {
|
|
|
328
328
|
}),
|
|
329
329
|
)
|
|
330
330
|
})
|
|
331
|
+
|
|
332
|
+
it('converts markdown to HTML when format is markdown', async () => {
|
|
333
|
+
mockResponse({ OriginalArrivalTime: 1704067200000 })
|
|
334
|
+
|
|
335
|
+
const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
|
|
336
|
+
const message = await client.sendChatMessage('19:1on1@unq.gbl.spaces', '**bold** and `code`', 'markdown')
|
|
337
|
+
|
|
338
|
+
// The return value echoes the caller's original markdown, not the converted HTML
|
|
339
|
+
expect(message.content).toBe('**bold** and `code`')
|
|
340
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
341
|
+
JSON.stringify({
|
|
342
|
+
content: '<strong>bold</strong> and <code>code</code>',
|
|
343
|
+
messagetype: 'RichText/Html',
|
|
344
|
+
contenttype: 'text',
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('passes content through unchanged when format is html', async () => {
|
|
350
|
+
mockResponse({ OriginalArrivalTime: 1704067200000 })
|
|
351
|
+
|
|
352
|
+
const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
|
|
353
|
+
await client.sendChatMessage('19:1on1@unq.gbl.spaces', '<b>raw</b>', 'html')
|
|
354
|
+
|
|
355
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
content: '<b>raw</b>',
|
|
358
|
+
messagetype: 'RichText/Html',
|
|
359
|
+
contenttype: 'text',
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
362
|
+
})
|
|
331
363
|
})
|
|
332
364
|
|
|
333
365
|
describe('editChatMessage', () => {
|
|
@@ -352,6 +384,39 @@ describe('TeamsClient', () => {
|
|
|
352
384
|
}),
|
|
353
385
|
)
|
|
354
386
|
})
|
|
387
|
+
|
|
388
|
+
it('converts markdown to HTML when format is markdown', async () => {
|
|
389
|
+
mockResponse({ edittime: 1704067200000 })
|
|
390
|
+
|
|
391
|
+
const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
|
|
392
|
+
const message = await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', '**bold**', 'markdown')
|
|
393
|
+
|
|
394
|
+
expect(message.content).toBe('**bold**')
|
|
395
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
396
|
+
JSON.stringify({
|
|
397
|
+
content: '<strong>bold</strong>',
|
|
398
|
+
messagetype: 'RichText/Html',
|
|
399
|
+
contenttype: 'text',
|
|
400
|
+
skypeeditedid: 'msg1',
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('passes content through unchanged when format is html', async () => {
|
|
406
|
+
mockResponse({ edittime: 1704067200000 })
|
|
407
|
+
|
|
408
|
+
const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
|
|
409
|
+
await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', '<b>raw</b>', 'html')
|
|
410
|
+
|
|
411
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
412
|
+
JSON.stringify({
|
|
413
|
+
content: '<b>raw</b>',
|
|
414
|
+
messagetype: 'RichText/Html',
|
|
415
|
+
contenttype: 'text',
|
|
416
|
+
skypeeditedid: 'msg1',
|
|
417
|
+
}),
|
|
418
|
+
)
|
|
419
|
+
})
|
|
355
420
|
})
|
|
356
421
|
|
|
357
422
|
describe('getTeam', () => {
|
|
@@ -437,6 +502,68 @@ describe('TeamsClient', () => {
|
|
|
437
502
|
expect(message.parent_message_id).toBe('root1')
|
|
438
503
|
expect(message.is_thread_reply).toBe(true)
|
|
439
504
|
})
|
|
505
|
+
|
|
506
|
+
it('escapes HTML in channel messages when format is text', async () => {
|
|
507
|
+
mockResponse({
|
|
508
|
+
id: 'msg1',
|
|
509
|
+
channel_id: 'ch1',
|
|
510
|
+
author: { id: '123', displayName: 'Test User' },
|
|
511
|
+
content: 'a <b> & c',
|
|
512
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
516
|
+
await client.sendMessage('111', 'ch1', 'a <b> & c')
|
|
517
|
+
|
|
518
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'a <b> & c' }))
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('converts markdown to HTML for channel messages', async () => {
|
|
522
|
+
mockResponse({
|
|
523
|
+
id: 'msg1',
|
|
524
|
+
channel_id: 'ch1',
|
|
525
|
+
author: { id: '123', displayName: 'Test User' },
|
|
526
|
+
content: '<strong>bold</strong>',
|
|
527
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
531
|
+
await client.sendMessage('111', 'ch1', '**bold**', undefined, 'markdown')
|
|
532
|
+
|
|
533
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: '<strong>bold</strong>' }))
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('converts markdown to HTML for channel thread replies', async () => {
|
|
537
|
+
mockResponse({
|
|
538
|
+
id: 'reply1',
|
|
539
|
+
channel_id: 'ch1',
|
|
540
|
+
author: { id: '123', displayName: 'Test User' },
|
|
541
|
+
content: '<strong>bold</strong>',
|
|
542
|
+
timestamp: '2024-01-01T00:01:00.000Z',
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
546
|
+
await client.sendMessage('111', 'ch1', '**bold**', 'root1', 'markdown')
|
|
547
|
+
|
|
548
|
+
expect(fetchCalls[0].options?.body).toBe(
|
|
549
|
+
JSON.stringify({ content: '<strong>bold</strong>', parentMessageId: 'root1' }),
|
|
550
|
+
)
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('passes content through unchanged when format is html', async () => {
|
|
554
|
+
mockResponse({
|
|
555
|
+
id: 'msg1',
|
|
556
|
+
channel_id: 'ch1',
|
|
557
|
+
author: { id: '123', displayName: 'Test User' },
|
|
558
|
+
content: 'Hey <at id="29:xyz">John</at>',
|
|
559
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
563
|
+
await client.sendMessage('111', 'ch1', 'Hey <at id="29:xyz">John</at>', undefined, 'html')
|
|
564
|
+
|
|
565
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Hey <at id="29:xyz">John</at>' }))
|
|
566
|
+
})
|
|
440
567
|
})
|
|
441
568
|
|
|
442
569
|
describe('getMessages', () => {
|
|
@@ -2,8 +2,11 @@ import { randomUUID } from 'node:crypto'
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { basename } from 'node:path'
|
|
4
4
|
|
|
5
|
+
import { escapeHtml, markdownToHtml } from '@/shared/utils/markdown-to-html'
|
|
6
|
+
|
|
5
7
|
import { SUBSTRATE_SEARCH_URL } from './app-config'
|
|
6
8
|
import { TeamsCredentialManager } from './credential-manager'
|
|
9
|
+
import { sanitizeTeamsHtml } from './html-sanitizer'
|
|
7
10
|
import { TeamsTokenProvider } from './token-provider'
|
|
8
11
|
import type {
|
|
9
12
|
TeamsAccountType,
|
|
@@ -12,6 +15,7 @@ import type {
|
|
|
12
15
|
TeamsChatType,
|
|
13
16
|
TeamsFile,
|
|
14
17
|
TeamsMessage,
|
|
18
|
+
TeamsMessageFormat,
|
|
15
19
|
TeamsRegion,
|
|
16
20
|
TeamsSearchResult,
|
|
17
21
|
TeamsTeam,
|
|
@@ -230,13 +234,9 @@ async function readDownloadResponse(response: Response, codePrefix: string): Pro
|
|
|
230
234
|
return Buffer.from(await response.arrayBuffer())
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
function
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
.replaceAll('<', '<')
|
|
237
|
-
.replaceAll('>', '>')
|
|
238
|
-
.replaceAll('"', '"')
|
|
239
|
-
.replaceAll("'", ''')
|
|
237
|
+
function formatContent(content: string, format: TeamsMessageFormat): string {
|
|
238
|
+
if (format === 'html') return sanitizeTeamsHtml(content)
|
|
239
|
+
return format === 'markdown' ? markdownToHtml(content) : escapeHtml(content)
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
function withThreadMetadata(message: RawTeamsMessage, rootMessageId?: string): TeamsMessage {
|
|
@@ -692,13 +692,13 @@ export class TeamsClient {
|
|
|
692
692
|
}))
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
async sendChatMessage(chatId: string, content: string): Promise<TeamsMessage> {
|
|
695
|
+
async sendChatMessage(chatId: string, content: string, format: TeamsMessageFormat = 'text'): Promise<TeamsMessage> {
|
|
696
696
|
interface SendResponse {
|
|
697
697
|
OriginalArrivalTime?: number
|
|
698
698
|
}
|
|
699
699
|
const encodedChatId = encodeURIComponent(chatId)
|
|
700
700
|
const response = await this.request<SendResponse>('POST', `/users/ME/conversations/${encodedChatId}/messages`, {
|
|
701
|
-
content:
|
|
701
|
+
content: formatContent(content, format),
|
|
702
702
|
messagetype: 'RichText/Html',
|
|
703
703
|
contenttype: 'text',
|
|
704
704
|
})
|
|
@@ -713,7 +713,12 @@ export class TeamsClient {
|
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
-
async editChatMessage(
|
|
716
|
+
async editChatMessage(
|
|
717
|
+
chatId: string,
|
|
718
|
+
messageId: string,
|
|
719
|
+
content: string,
|
|
720
|
+
format: TeamsMessageFormat = 'text',
|
|
721
|
+
): Promise<TeamsMessage> {
|
|
717
722
|
interface EditResponse {
|
|
718
723
|
edittime?: string | number
|
|
719
724
|
}
|
|
@@ -724,7 +729,7 @@ export class TeamsClient {
|
|
|
724
729
|
'PUT',
|
|
725
730
|
`/users/ME/conversations/${encodedChatId}/messages/${encodedMessageId}`,
|
|
726
731
|
{
|
|
727
|
-
content:
|
|
732
|
+
content: formatContent(content, format),
|
|
728
733
|
messagetype: 'RichText/Html',
|
|
729
734
|
contenttype: 'text',
|
|
730
735
|
skypeeditedid: messageId,
|
|
@@ -758,12 +763,20 @@ export class TeamsClient {
|
|
|
758
763
|
)
|
|
759
764
|
}
|
|
760
765
|
|
|
761
|
-
async sendMessage(
|
|
766
|
+
async sendMessage(
|
|
767
|
+
teamId: string,
|
|
768
|
+
channelId: string,
|
|
769
|
+
content: string,
|
|
770
|
+
rootMessageId?: string,
|
|
771
|
+
format: TeamsMessageFormat = 'text',
|
|
772
|
+
): Promise<TeamsMessage> {
|
|
773
|
+
const body = formatContent(content, format)
|
|
774
|
+
|
|
762
775
|
if (rootMessageId) {
|
|
763
776
|
const response = await this.request<RawTeamsMessage>(
|
|
764
777
|
'POST',
|
|
765
778
|
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies`,
|
|
766
|
-
{ content, parentMessageId: rootMessageId },
|
|
779
|
+
{ content: body, parentMessageId: rootMessageId },
|
|
767
780
|
CSA_API_BASE,
|
|
768
781
|
)
|
|
769
782
|
return withThreadMetadata(response, rootMessageId)
|
|
@@ -772,7 +785,7 @@ export class TeamsClient {
|
|
|
772
785
|
return this.request<TeamsMessage>(
|
|
773
786
|
'POST',
|
|
774
787
|
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`,
|
|
775
|
-
{ content },
|
|
788
|
+
{ content: body },
|
|
776
789
|
CSA_API_BASE,
|
|
777
790
|
)
|
|
778
791
|
}
|
|
@@ -107,6 +107,7 @@ it('send: returns sent message', async () => {
|
|
|
107
107
|
expect(consoleSpy).toHaveBeenCalled()
|
|
108
108
|
const output = consoleSpy.mock.calls[0][0]
|
|
109
109
|
expect(output).toContain('Hello world')
|
|
110
|
+
expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'Hello world', 'text')
|
|
110
111
|
})
|
|
111
112
|
|
|
112
113
|
it('edit: edits a chat message and returns updated content', async () => {
|
|
@@ -115,8 +116,76 @@ it('edit: edits a chat message and returns updated content', async () => {
|
|
|
115
116
|
|
|
116
117
|
await editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', { pretty: false })
|
|
117
118
|
|
|
118
|
-
expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content')
|
|
119
|
+
expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', 'text')
|
|
119
120
|
expect(consoleSpy).toHaveBeenCalled()
|
|
120
121
|
const output = consoleSpy.mock.calls[0][0]
|
|
121
122
|
expect(output).toContain('Edited content')
|
|
122
123
|
})
|
|
124
|
+
|
|
125
|
+
it('send: passes markdown format to the client', async () => {
|
|
126
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
127
|
+
console.log = consoleSpy
|
|
128
|
+
|
|
129
|
+
await sendAction('19:1on1@unq.gbl.spaces', '**bold**', { pretty: false, format: 'markdown' })
|
|
130
|
+
|
|
131
|
+
expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', '**bold**', 'markdown')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('send: defaults to text format when --format is omitted', async () => {
|
|
135
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
136
|
+
console.log = consoleSpy
|
|
137
|
+
|
|
138
|
+
await sendAction('19:1on1@unq.gbl.spaces', 'plain', { pretty: false })
|
|
139
|
+
|
|
140
|
+
expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'plain', 'text')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('send: rejects an invalid format', async () => {
|
|
144
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
145
|
+
console.log = consoleSpy
|
|
146
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
147
|
+
throw new Error(`exit:${code}`)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await expect(sendAction('19:1on1@unq.gbl.spaces', 'hi', { pretty: false, format: 'invalid' })).rejects.toThrow(
|
|
152
|
+
'exit:1',
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Invalid format')
|
|
156
|
+
expect(clientSendChatMessageSpy).not.toHaveBeenCalled()
|
|
157
|
+
} finally {
|
|
158
|
+
// Restore in finally: a failing assertion above would otherwise leave
|
|
159
|
+
// process.exit mocked, so every later test would throw instead of exiting.
|
|
160
|
+
exitSpy.mockRestore()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('edit: passes markdown format to the client', async () => {
|
|
165
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
166
|
+
console.log = consoleSpy
|
|
167
|
+
|
|
168
|
+
await editAction('19:1on1@unq.gbl.spaces', 'msg_123', '**bold**', { pretty: false, format: 'markdown' })
|
|
169
|
+
|
|
170
|
+
expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', '**bold**', 'markdown')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('edit: rejects an invalid format', async () => {
|
|
174
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
175
|
+
console.log = consoleSpy
|
|
176
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
177
|
+
throw new Error(`exit:${code}`)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await expect(
|
|
182
|
+
editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'hi', { pretty: false, format: 'invalid' }),
|
|
183
|
+
).rejects.toThrow('exit:1')
|
|
184
|
+
|
|
185
|
+
expect(clientEditChatMessageSpy).not.toHaveBeenCalled()
|
|
186
|
+
} finally {
|
|
187
|
+
// Restore in finally: a failing assertion above would otherwise leave
|
|
188
|
+
// process.exit mocked, so every later test would throw instead of exiting.
|
|
189
|
+
exitSpy.mockRestore()
|
|
190
|
+
}
|
|
191
|
+
})
|
|
@@ -5,6 +5,7 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
5
5
|
|
|
6
6
|
import { TeamsClient } from '../client'
|
|
7
7
|
import { TeamsCredentialManager } from '../credential-manager'
|
|
8
|
+
import { resolveFormat } from './format'
|
|
8
9
|
|
|
9
10
|
export async function listAction(options: { pretty?: boolean }): Promise<void> {
|
|
10
11
|
try {
|
|
@@ -70,7 +71,13 @@ export async function historyAction(chatId: string, options: { limit?: number; p
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
export async function sendAction(
|
|
74
|
+
export async function sendAction(
|
|
75
|
+
chatId: string,
|
|
76
|
+
content: string,
|
|
77
|
+
options: { pretty?: boolean; format?: string },
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const format = resolveFormat(options.format, options.pretty)
|
|
80
|
+
|
|
74
81
|
try {
|
|
75
82
|
const credManager = new TeamsCredentialManager()
|
|
76
83
|
const cred = await credManager.getTokenWithExpiry()
|
|
@@ -86,7 +93,7 @@ export async function sendAction(chatId: string, content: string, options: { pre
|
|
|
86
93
|
accountType: cred.accountType,
|
|
87
94
|
region: cred.region,
|
|
88
95
|
})
|
|
89
|
-
const message = await client.sendChatMessage(chatId, content)
|
|
96
|
+
const message = await client.sendChatMessage(chatId, content, format)
|
|
90
97
|
|
|
91
98
|
const output = {
|
|
92
99
|
id: message.id,
|
|
@@ -104,8 +111,10 @@ export async function editAction(
|
|
|
104
111
|
chatId: string,
|
|
105
112
|
messageId: string,
|
|
106
113
|
content: string,
|
|
107
|
-
options: { pretty?: boolean },
|
|
114
|
+
options: { pretty?: boolean; format?: string },
|
|
108
115
|
): Promise<void> {
|
|
116
|
+
const format = resolveFormat(options.format, options.pretty)
|
|
117
|
+
|
|
109
118
|
try {
|
|
110
119
|
const credManager = new TeamsCredentialManager()
|
|
111
120
|
const cred = await credManager.getTokenWithExpiry()
|
|
@@ -121,7 +130,7 @@ export async function editAction(
|
|
|
121
130
|
accountType: cred.accountType,
|
|
122
131
|
region: cred.region,
|
|
123
132
|
})
|
|
124
|
-
const message = await client.editChatMessage(chatId, messageId, content)
|
|
133
|
+
const message = await client.editChatMessage(chatId, messageId, content, format)
|
|
125
134
|
|
|
126
135
|
const output = {
|
|
127
136
|
id: message.id,
|
|
@@ -161,6 +170,7 @@ export const chatCommand = new Command('chat')
|
|
|
161
170
|
.description('Send a message to a chat')
|
|
162
171
|
.argument('<chat-id>', 'Chat ID')
|
|
163
172
|
.argument('<content>', 'Message content')
|
|
173
|
+
.option('--format <format>', 'Message format: text, markdown, or html', 'text')
|
|
164
174
|
.option('--pretty', 'Pretty print JSON output')
|
|
165
175
|
.action(sendAction),
|
|
166
176
|
)
|
|
@@ -170,6 +180,7 @@ export const chatCommand = new Command('chat')
|
|
|
170
180
|
.argument('<chat-id>', 'Chat ID')
|
|
171
181
|
.argument('<message-id>', 'Message ID')
|
|
172
182
|
.argument('<content>', 'New message content')
|
|
183
|
+
.option('--format <format>', 'Message format: text, markdown, or html', 'text')
|
|
173
184
|
.option('--pretty', 'Pretty print JSON output')
|
|
174
185
|
.action(editAction),
|
|
175
186
|
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
2
|
+
|
|
3
|
+
import type { TeamsMessageFormat } from '../types'
|
|
4
|
+
|
|
5
|
+
export function resolveFormat(value: string | undefined, pretty: boolean | undefined): TeamsMessageFormat {
|
|
6
|
+
const format = value ?? 'text'
|
|
7
|
+
if (format !== 'text' && format !== 'markdown' && format !== 'html') {
|
|
8
|
+
console.log(formatOutput({ error: `Invalid format: ${format}. Use "text", "markdown", or "html".` }, pretty))
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
return format
|
|
12
|
+
}
|
|
@@ -207,3 +207,59 @@ it('delete: returns success', async () => {
|
|
|
207
207
|
const output = consoleSpy.mock.calls[0][0]
|
|
208
208
|
expect(output).toContain('deleted')
|
|
209
209
|
})
|
|
210
|
+
|
|
211
|
+
it('send: passes markdown format to the client', async () => {
|
|
212
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
213
|
+
console.log = consoleSpy
|
|
214
|
+
|
|
215
|
+
await sendAction('team_123', 'ch_456', '**bold**', { pretty: false, format: 'markdown' })
|
|
216
|
+
|
|
217
|
+
expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '**bold**', undefined, 'markdown')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('send: defaults to text format when --format is omitted', async () => {
|
|
221
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
222
|
+
console.log = consoleSpy
|
|
223
|
+
|
|
224
|
+
await sendAction('team_123', 'ch_456', 'plain', { pretty: false })
|
|
225
|
+
|
|
226
|
+
expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', 'plain', undefined, 'text')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('send: rejects an invalid format', async () => {
|
|
230
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
231
|
+
console.log = consoleSpy
|
|
232
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
233
|
+
throw new Error(`exit:${code}`)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await expect(sendAction('team_123', 'ch_456', 'hi', { pretty: false, format: 'invalid' })).rejects.toThrow('exit:1')
|
|
238
|
+
|
|
239
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
240
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Invalid format')
|
|
241
|
+
expect(clientSendMessageSpy).not.toHaveBeenCalled()
|
|
242
|
+
} finally {
|
|
243
|
+
// Restore in finally: a failing assertion above would otherwise leave
|
|
244
|
+
// process.exit mocked, so every later test would throw instead of exiting.
|
|
245
|
+
exitSpy.mockRestore()
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('send: passes html format to the client', async () => {
|
|
250
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
251
|
+
console.log = consoleSpy
|
|
252
|
+
|
|
253
|
+
await sendAction('team_123', 'ch_456', '<at id="29:x">John</at>', { pretty: false, format: 'html' })
|
|
254
|
+
|
|
255
|
+
expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '<at id="29:x">John</at>', undefined, 'html')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('send: passes markdown format together with --thread', async () => {
|
|
259
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
260
|
+
console.log = consoleSpy
|
|
261
|
+
|
|
262
|
+
await sendAction('team_123', 'ch_456', '**bold**', { pretty: false, thread: 'root_1', format: 'markdown' })
|
|
263
|
+
|
|
264
|
+
expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '**bold**', 'root_1', 'markdown')
|
|
265
|
+
})
|
|
@@ -7,13 +7,16 @@ import { TeamsClient } from '../client'
|
|
|
7
7
|
import { TeamsCredentialManager } from '../credential-manager'
|
|
8
8
|
import type { TeamsMessage } from '../types'
|
|
9
9
|
import { TeamsAuthCapabilityError } from '../types'
|
|
10
|
+
import { resolveFormat } from './format'
|
|
10
11
|
|
|
11
12
|
export async function sendAction(
|
|
12
13
|
teamId: string,
|
|
13
14
|
channelId: string,
|
|
14
15
|
content: string,
|
|
15
|
-
options: { pretty?: boolean; thread?: string },
|
|
16
|
+
options: { pretty?: boolean; thread?: string; format?: string },
|
|
16
17
|
): Promise<void> {
|
|
18
|
+
const format = resolveFormat(options.format, options.pretty)
|
|
19
|
+
|
|
17
20
|
try {
|
|
18
21
|
const credManager = new TeamsCredentialManager()
|
|
19
22
|
const cred = await credManager.getTokenWithExpiry()
|
|
@@ -29,7 +32,7 @@ export async function sendAction(
|
|
|
29
32
|
accountType: cred.accountType,
|
|
30
33
|
region: cred.region,
|
|
31
34
|
})
|
|
32
|
-
const message = await client.sendMessage(teamId, channelId, content, options.thread)
|
|
35
|
+
const message = await client.sendMessage(teamId, channelId, content, options.thread, format)
|
|
33
36
|
|
|
34
37
|
const output = {
|
|
35
38
|
id: message.id,
|
|
@@ -274,6 +277,7 @@ export const messageCommand = new Command('message')
|
|
|
274
277
|
.argument('<channel-id>', 'Channel ID')
|
|
275
278
|
.argument('<content>', 'Message content')
|
|
276
279
|
.option('--thread <message-id>', 'Reply to a thread root message')
|
|
280
|
+
.option('--format <format>', 'Message format: text, markdown, or html', 'text')
|
|
277
281
|
.option('--pretty', 'Pretty print JSON output')
|
|
278
282
|
.action(sendAction),
|
|
279
283
|
)
|