agent-messenger 2.35.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/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/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-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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-teams
|
|
3
3
|
description: Interact with Microsoft Teams - send messages, read channels, manage reactions
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.36.0
|
|
5
5
|
allowed-tools: Bash(agent-teams:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -205,6 +205,11 @@ Output includes the authenticated user's identity information.
|
|
|
205
205
|
agent-teams message send <team-id> <channel-id> <content>
|
|
206
206
|
agent-teams message send <team-id> 19:abc123@thread.tacv2 "Hello world"
|
|
207
207
|
|
|
208
|
+
# Send a rich-text message written in markdown
|
|
209
|
+
agent-teams message send <team-id> <channel-id> "## Deploy done
|
|
210
|
+
- service A
|
|
211
|
+
- service B" --format markdown
|
|
212
|
+
|
|
208
213
|
# List messages
|
|
209
214
|
agent-teams message list <team-id> <channel-id>
|
|
210
215
|
agent-teams message list <team-id> 19:abc123@thread.tacv2 --limit 50
|
|
@@ -249,15 +254,52 @@ agent-teams chat history <chat-id> --limit 100
|
|
|
249
254
|
|
|
250
255
|
# Send a message to a chat
|
|
251
256
|
agent-teams chat send <chat-id> "Hello"
|
|
257
|
+
agent-teams chat send <chat-id> "**Important**: see the [board](https://example.com)" --format markdown
|
|
252
258
|
|
|
253
259
|
# Edit one of your own chat messages
|
|
254
260
|
agent-teams chat edit <chat-id> <message-id> "Updated text"
|
|
261
|
+
agent-teams chat edit <chat-id> <message-id> "Fixed \`bug\`" --format markdown
|
|
255
262
|
```
|
|
256
263
|
|
|
257
264
|
Chat IDs look like `19:guid1_guid2@unq.gbl.spaces` (1:1) or `19:guid@thread.tacv2` (group). Get them from `chat list`. The `48:notes` chat (`type: self`) is your personal "to me" notes thread.
|
|
258
265
|
|
|
259
266
|
`chat edit` only works on your own messages, and only for chats — Teams channel messages are not editable through this API.
|
|
260
267
|
|
|
268
|
+
### Message Formatting
|
|
269
|
+
|
|
270
|
+
`message send`, `chat send`, and `chat edit` accept `--format <text|markdown|html>` (default `text`).
|
|
271
|
+
|
|
272
|
+
In `text` mode the content is HTML-escaped, so `<`, `>`, and `&` are sent literally.
|
|
273
|
+
|
|
274
|
+
In `markdown` mode the content is converted to HTML before sending. Supported syntax:
|
|
275
|
+
|
|
276
|
+
| Syntax | Renders as |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
| `# Heading` through `#### Heading` | headings |
|
|
279
|
+
| `##### Heading`, `###### Heading` | **not supported** — indistinguishable from body text |
|
|
280
|
+
| `**bold**`, `_italic_`, `***bold italic***` | emphasis |
|
|
281
|
+
| `` `inline code` `` | inline code |
|
|
282
|
+
| triple-backtick fence | fenced code block, without syntax highlighting |
|
|
283
|
+
| `- item` | bulleted list |
|
|
284
|
+
| `1. item` | numbered list |
|
|
285
|
+
| `> quote` | blockquote |
|
|
286
|
+
| `[label](https://example.com)` | link |
|
|
287
|
+
| `---` | horizontal rule |
|
|
288
|
+
|
|
289
|
+
Heading levels 5 and 6 are unusable: the Teams client renders `<h5>` and `<h6>` at body size, so they are indistinguishable from surrounding text. Only go as deep as `####`. Fenced code blocks render as monospaced blocks but are never syntax-highlighted — a language tag (` ```ts `) is accepted and then ignored. Both behaviors were verified against a live account.
|
|
290
|
+
|
|
291
|
+
Links are restricted to `http:`, `https:`, `mailto:`, root-relative (`/`), and anchor (`#`) URLs; anything else renders as plain text.
|
|
292
|
+
|
|
293
|
+
In `html` mode the content is filtered through a tag whitelist rather than sent verbatim. Use it when you need Teams-specific markup that markdown cannot express — most commonly @mentions:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
agent-teams message send <team-id> <channel-id> "Hey <at id=\"29:abc\">John</at>, build is ready" --format html
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Allowed tags: `at`, `a`, `b`, `i`, `u`, `s`, `strong`, `em`, `code`, `pre`, `br`, `p`, `ul`, `ol`, `li`, `blockquote`, `h1`–`h6`, `hr`. `<at>` only keeps its `id` attribute; `<a>` only keeps `href`, and only when it matches the same URL whitelist as markdown mode; every other tag keeps no attributes at all. Anything not on the list — `<script>`, `<img>`, `on*` event attributes, malformed or unrecognized tags — is escaped or stripped rather than passed through.
|
|
300
|
+
|
|
301
|
+
The whitelist limits the blast radius, but it does not make the mode safe for arbitrary input: only pass content you construct yourself. Untrusted input belongs in `text` mode.
|
|
302
|
+
|
|
261
303
|
### Team Commands
|
|
262
304
|
|
|
263
305
|
```bash
|
|
@@ -385,7 +427,7 @@ agent-teams channel list --pretty
|
|
|
385
427
|
| Channel identifiers | UUID format (19:xxx@thread.tacv2) | Snowflake IDs | Channel name or ID |
|
|
386
428
|
| Token storage | Cookies SQLite | LevelDB | LevelDB |
|
|
387
429
|
| Token expiry | **60-90 minutes** | Rarely expires | Rarely expires |
|
|
388
|
-
| Mentions | `<at id="user-id">Name</at>`
|
|
430
|
+
| Mentions | `<at id="user-id">Name</at>` (needs `--format html`) | `<@user_id>` | `<@USER_ID>` |
|
|
389
431
|
|
|
390
432
|
**Important**: Teams uses UUID-style channel IDs (like `19:abc123@thread.tacv2`). You cannot use channel names directly - use `channel list` to find IDs first.
|
|
391
433
|
|
|
@@ -279,12 +279,12 @@ if [ -z "$USER_ID" ] || [ "$USER_ID" = "null" ]; then
|
|
|
279
279
|
fi
|
|
280
280
|
|
|
281
281
|
# Send message with mention (Teams format)
|
|
282
|
-
agent-teams message send "$TEAM_ID" "$CHANNEL_ID" "Hey <at id=\"$USER_ID\">$USER_NAME</at>, the build is ready for review!"
|
|
282
|
+
agent-teams message send "$TEAM_ID" "$CHANNEL_ID" "Hey <at id=\"$USER_ID\">$USER_NAME</at>, the build is ready for review!" --format html
|
|
283
283
|
```
|
|
284
284
|
|
|
285
285
|
**When to use**: Notifications, task assignments, code review requests.
|
|
286
286
|
|
|
287
|
-
**Note**: Teams mentions use format `<at id="USER_ID">Display Name</at
|
|
287
|
+
**Note**: Teams mentions use format `<at id="USER_ID">Display Name</at>` and require `--format html` — in the default `text` mode the tags are escaped and render literally.
|
|
288
288
|
|
|
289
289
|
## Pattern 8: Reaction-Based Workflow
|
|
290
290
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-webexbot
|
|
3
3
|
description: Interact with Cisco Webex using bot tokens - send messages, reply in threads, upload and download files, look up people, read spaces, manage memberships, stream real-time events
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.36.0
|
|
5
5
|
allowed-tools: Bash(agent-webexbot:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -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
|
+
})
|