agent-messenger 2.23.1 → 2.23.3
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/webex/client.d.ts +18 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +202 -49
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +9 -6
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/commands/member.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/member.js +2 -0
- package/dist/src/platforms/webex/commands/member.js.map +1 -1
- package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/message.js +2 -0
- package/dist/src/platforms/webex/commands/message.js.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.js +3 -1
- package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/webex/commands/space.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/space.js +5 -0
- package/dist/src/platforms/webex/commands/space.js.map +1 -1
- package/dist/src/platforms/webex/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/whoami.js +2 -0
- package/dist/src/platforms/webex/commands/whoami.js.map +1 -1
- package/dist/src/platforms/webex/id-normalizer.d.ts +11 -0
- package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -1
- package/dist/src/platforms/webex/id-normalizer.js +102 -20
- package/dist/src/platforms/webex/id-normalizer.js.map +1 -1
- package/dist/src/platforms/webex/index.d.ts +2 -2
- package/dist/src/platforms/webex/index.d.ts.map +1 -1
- package/dist/src/platforms/webex/index.js +1 -1
- package/dist/src/platforms/webex/index.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +20 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +10 -0
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/webexbot/client.d.ts +0 -4
- package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/client.js +8 -65
- package/dist/src/platforms/webexbot/client.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/file.d.ts +2 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/file.js +2 -0
- package/dist/src/platforms/webexbot/commands/file.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/member.d.ts +2 -0
- package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/member.js +2 -0
- package/dist/src/platforms/webexbot/commands/member.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/message.d.ts +4 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/message.js +6 -0
- package/dist/src/platforms/webexbot/commands/message.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts +2 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/snapshot.js +10 -2
- package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/space.d.ts +4 -0
- package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/space.js +5 -0
- package/dist/src/platforms/webexbot/commands/space.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/user.d.ts +3 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/user.js +3 -0
- package/dist/src/platforms/webexbot/commands/user.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/whoami.d.ts +2 -0
- package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/whoami.js +2 -0
- package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -1
- package/dist/src/platforms/webexbot/index.d.ts +2 -2
- package/dist/src/platforms/webexbot/index.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/index.js +1 -1
- package/dist/src/platforms/webexbot/index.js.map +1 -1
- package/dist/src/tui/adapters/types.d.ts +3 -0
- package/dist/src/tui/adapters/types.d.ts.map +1 -1
- package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/webex-adapter.js +4 -0
- package/dist/src/tui/adapters/webex-adapter.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +2 -2
- 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 +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 +3 -3
- package/skills/agent-webexbot/SKILL.md +2 -2
- package/skills/agent-webexbot/references/common-patterns.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/webex/client.test.ts +94 -6
- package/src/platforms/webex/client.ts +226 -44
- package/src/platforms/webex/commands/auth.test.ts +3 -1
- package/src/platforms/webex/commands/auth.ts +12 -7
- package/src/platforms/webex/commands/member.test.ts +24 -8
- package/src/platforms/webex/commands/member.ts +2 -0
- package/src/platforms/webex/commands/message.test.ts +37 -23
- package/src/platforms/webex/commands/message.ts +2 -0
- package/src/platforms/webex/commands/snapshot.test.ts +18 -10
- package/src/platforms/webex/commands/snapshot.ts +3 -1
- package/src/platforms/webex/commands/space.test.ts +36 -17
- package/src/platforms/webex/commands/space.ts +5 -0
- package/src/platforms/webex/commands/whoami.test.ts +16 -6
- package/src/platforms/webex/commands/whoami.ts +2 -0
- package/src/platforms/webex/id-normalizer.test.ts +282 -2
- package/src/platforms/webex/id-normalizer.ts +112 -20
- package/src/platforms/webex/index.ts +2 -2
- package/src/platforms/webex/listener.test.ts +3 -0
- package/src/platforms/webex/types.test.ts +20 -0
- package/src/platforms/webex/types.ts +20 -0
- package/src/platforms/webex/typings/webex-message-handler.d.ts +40 -2
- package/src/platforms/webexbot/client.ts +8 -74
- package/src/platforms/webexbot/commands/file.ts +4 -0
- package/src/platforms/webexbot/commands/member.ts +4 -0
- package/src/platforms/webexbot/commands/message.ts +10 -0
- package/src/platforms/webexbot/commands/snapshot.ts +12 -2
- package/src/platforms/webexbot/commands/space.ts +9 -0
- package/src/platforms/webexbot/commands/user.test.ts +15 -5
- package/src/platforms/webexbot/commands/user.ts +6 -0
- package/src/platforms/webexbot/commands/whoami.ts +4 -0
- package/src/platforms/webexbot/index.ts +2 -2
- package/src/tui/adapters/types.ts +3 -0
- package/src/tui/adapters/webex-adapter.ts +4 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import * as jose from 'node-jose'
|
|
4
4
|
|
|
5
5
|
import { WebexClient } from './client'
|
|
6
6
|
import { WebexEncryptionService } from './encryption'
|
|
7
|
+
import { toRestId } from './id-normalizer'
|
|
7
8
|
import { WebexError } from './types'
|
|
8
9
|
|
|
9
10
|
describe('WebexClient', () => {
|
|
@@ -444,6 +445,92 @@ describe('WebexClient', () => {
|
|
|
444
445
|
})
|
|
445
446
|
})
|
|
446
447
|
|
|
448
|
+
describe('id ref resolution', () => {
|
|
449
|
+
const roomUuid = '12345678-1234-1234-1234-1234567890ab'
|
|
450
|
+
const personUuid = '22222222-2222-2222-2222-222222222222'
|
|
451
|
+
const messageUuid = '33333333-3333-3333-3333-333333333333'
|
|
452
|
+
const usRoomId = toRestId(roomUuid, 'ROOM')
|
|
453
|
+
const clusteredRoomId = Buffer.from(`ciscospark://urn:TEAM:us-west-2_r/ROOM/${roomUuid}`).toString('base64url')
|
|
454
|
+
|
|
455
|
+
const isRoomsList = (url: string) => new URL(url).pathname === '/v1/rooms'
|
|
456
|
+
|
|
457
|
+
it('resolves a bare room uuid to the real clustered id before sending', async () => {
|
|
458
|
+
mockResponse({ items: [{ id: clusteredRoomId, title: 'Team', type: 'group' }] })
|
|
459
|
+
mockResponse({ id: 'msg-1', roomId: clusteredRoomId, roomType: 'group' })
|
|
460
|
+
|
|
461
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
462
|
+
await client.sendMessage(roomUuid, 'hello')
|
|
463
|
+
|
|
464
|
+
expect(isRoomsList(fetchCalls[0].url)).toBe(true)
|
|
465
|
+
expect(JSON.parse(fetchCalls[1].options?.body as string).roomId).toBe(clusteredRoomId)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('rewrites a us-cluster room id to the real clustered id for memberships', async () => {
|
|
469
|
+
mockResponse({ items: [{ id: clusteredRoomId, title: 'Team', type: 'group' }] })
|
|
470
|
+
mockResponse({ items: [{ id: 'm1', roomId: clusteredRoomId, personId: 'p1' }] })
|
|
471
|
+
|
|
472
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
473
|
+
await client.listMemberships(usRoomId)
|
|
474
|
+
|
|
475
|
+
const membershipsUrl = new URL(fetchCalls[1].url)
|
|
476
|
+
expect(membershipsUrl.pathname).toBe('/v1/memberships')
|
|
477
|
+
expect(membershipsUrl.searchParams.get('roomId')).toBe(clusteredRoomId)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('passes an already-clustered room id through without a lookup', async () => {
|
|
481
|
+
mockResponse({ id: 'msg-1', roomId: clusteredRoomId, roomType: 'group' })
|
|
482
|
+
|
|
483
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
484
|
+
await client.sendMessage(clusteredRoomId, 'hello')
|
|
485
|
+
|
|
486
|
+
expect(fetchCalls).toHaveLength(1)
|
|
487
|
+
expect(JSON.parse(fetchCalls[0].options?.body as string).roomId).toBe(clusteredRoomId)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('fails open to the reconstructed room id and warns when no room matches', async () => {
|
|
491
|
+
const warnSpy = spyOn(console, 'warn').mockImplementation(() => {})
|
|
492
|
+
try {
|
|
493
|
+
mockResponse({ items: [] })
|
|
494
|
+
mockResponse({ id: 'msg-1', roomId: usRoomId, roomType: 'group' })
|
|
495
|
+
|
|
496
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
497
|
+
await client.sendMessage(roomUuid, 'hello')
|
|
498
|
+
|
|
499
|
+
expect(JSON.parse(fetchCalls[1].options?.body as string).roomId).toBe(usRoomId)
|
|
500
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
501
|
+
} finally {
|
|
502
|
+
warnSpy.mockRestore()
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('resolves a person email through the people search endpoint', async () => {
|
|
507
|
+
const personId = toRestId(personUuid, 'PEOPLE')
|
|
508
|
+
mockResponse({ items: [{ id: personId, emails: ['alice@example.com'], displayName: 'Alice', type: 'person' }] })
|
|
509
|
+
|
|
510
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
511
|
+
const person = await client.getPerson('alice@example.com')
|
|
512
|
+
|
|
513
|
+
expect(person.id).toBe(personId)
|
|
514
|
+
const url = new URL(fetchCalls[0].url)
|
|
515
|
+
expect(url.pathname).toBe('/v1/people')
|
|
516
|
+
expect(url.searchParams.get('email')).toBe('alice@example.com')
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('reconstructs bare person and message uuids for REST calls', async () => {
|
|
520
|
+
const personId = toRestId(personUuid, 'PEOPLE')
|
|
521
|
+
const messageId = toRestId(messageUuid, 'MESSAGE')
|
|
522
|
+
mockResponse({ id: personId, emails: ['alice@example.com'], displayName: 'Alice', type: 'person' })
|
|
523
|
+
mockResponse({ id: messageId, roomId: usRoomId, text: 'hi' })
|
|
524
|
+
|
|
525
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
526
|
+
await client.getPerson(personUuid)
|
|
527
|
+
await client.getMessage(messageUuid)
|
|
528
|
+
|
|
529
|
+
expect(fetchCalls[0].url).toBe(`https://webexapis.com/v1/people/${personId}`)
|
|
530
|
+
expect(fetchCalls[1].url).toBe(`https://webexapis.com/v1/messages/${messageId}`)
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
|
|
447
534
|
describe('rate limiting', () => {
|
|
448
535
|
it('retries on 429 with Retry-After header', async () => {
|
|
449
536
|
mockResponse({ message: 'Rate limited' }, 429, { 'Retry-After': '0.1' })
|
|
@@ -519,6 +606,7 @@ describe('WebexClient', () => {
|
|
|
519
606
|
const CONV_BASE = 'https://conv-r.wbx2.com/conversation/api/v1'
|
|
520
607
|
const TEST_ROOM_ID = Buffer.from('ciscospark://urn:TEAM:us-west-2_r/ROOM/abc123-def456').toString('base64')
|
|
521
608
|
const TEST_CONV_UUID = 'abc123-def456'
|
|
609
|
+
const TEST_ACTIVITY_ID = toRestId('activity-123', 'MESSAGE')
|
|
522
610
|
|
|
523
611
|
const mockActivity = (text: string, overrides?: Partial<Record<string, unknown>>) => ({
|
|
524
612
|
id: 'activity-123',
|
|
@@ -605,7 +693,7 @@ describe('WebexClient', () => {
|
|
|
605
693
|
const client = await createExtractedClient()
|
|
606
694
|
const message = await client.sendMessage(TEST_ROOM_ID, 'Hello world')
|
|
607
695
|
|
|
608
|
-
expect(message.id).toBe(
|
|
696
|
+
expect(message.id).toBe(TEST_ACTIVITY_ID)
|
|
609
697
|
expect(message.text).toBe('Hello world')
|
|
610
698
|
expect(message.personEmail).toBe('test@example.com')
|
|
611
699
|
expect(message.created).toBe('2026-01-01T00:00:00.000Z')
|
|
@@ -670,7 +758,7 @@ describe('WebexClient', () => {
|
|
|
670
758
|
const client = await createExtractedClient()
|
|
671
759
|
const messages = await client.listMessages(TEST_ROOM_ID)
|
|
672
760
|
|
|
673
|
-
expect(messages[0].id).toBe(
|
|
761
|
+
expect(messages[0].id).toBe(TEST_ACTIVITY_ID)
|
|
674
762
|
expect(messages[0].text).toBe('Hello')
|
|
675
763
|
expect(messages[0].personEmail).toBe('test@example.com')
|
|
676
764
|
expect(messages[0].created).toBe('2026-01-01T00:00:00.000Z')
|
|
@@ -713,7 +801,7 @@ describe('WebexClient', () => {
|
|
|
713
801
|
const client = await createExtractedClient()
|
|
714
802
|
const message = await client.getMessage('activity-123')
|
|
715
803
|
|
|
716
|
-
expect(message.id).toBe(
|
|
804
|
+
expect(message.id).toBe(TEST_ACTIVITY_ID)
|
|
717
805
|
expect(message.text).toBe('Hello')
|
|
718
806
|
expect(message.personEmail).toBe('test@example.com')
|
|
719
807
|
})
|
|
@@ -843,7 +931,7 @@ describe('WebexClient', () => {
|
|
|
843
931
|
|
|
844
932
|
const client = await createExtractedClient()
|
|
845
933
|
const message = await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
846
|
-
expect(message.id).toBe(
|
|
934
|
+
expect(message.id).toBe(TEST_ACTIVITY_ID)
|
|
847
935
|
})
|
|
848
936
|
|
|
849
937
|
it('throws when server returns activity linked to a different parent', async () => {
|
|
@@ -916,7 +1004,7 @@ describe('WebexClient', () => {
|
|
|
916
1004
|
expect(fetchCalls[0].url).toContain('/rooms?type=direct&max=100')
|
|
917
1005
|
expect(fetchCalls[1].url).toContain('/memberships?roomId=')
|
|
918
1006
|
expect(fetchCalls[2].url).toBe(`${CONV_BASE}/activities`)
|
|
919
|
-
expect(message.id).toBe(
|
|
1007
|
+
expect(message.id).toBe(TEST_ACTIVITY_ID)
|
|
920
1008
|
})
|
|
921
1009
|
|
|
922
1010
|
it('throws WebexError when no existing direct conversation found', async () => {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { WebexCredentialManager } from './credential-manager'
|
|
2
2
|
import { WebexEncryptionService } from './encryption'
|
|
3
|
+
import {
|
|
4
|
+
decodeWebexId,
|
|
5
|
+
normalizeSdkMembership,
|
|
6
|
+
normalizeSdkMessage,
|
|
7
|
+
normalizeSdkPerson,
|
|
8
|
+
toRestId,
|
|
9
|
+
} from './id-normalizer'
|
|
3
10
|
import { KmsKeyProvider } from './kms-key-provider'
|
|
4
11
|
import { escapeHtml, markdownToHtml, stripMarkdown } from './markdown-to-html'
|
|
5
12
|
import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
@@ -15,6 +22,10 @@ interface RateLimitBucket {
|
|
|
15
22
|
resetAt: number
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
interface WebexClientOptions {
|
|
26
|
+
roomResolutionWarningPrefix?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export class WebexClient {
|
|
19
30
|
private token: string | null = null
|
|
20
31
|
private deviceUrl: string | null = null
|
|
@@ -22,6 +33,13 @@ export class WebexClient {
|
|
|
22
33
|
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
23
34
|
private globalRateLimitUntil: number = 0
|
|
24
35
|
private encryption: WebexEncryptionService | null = null
|
|
36
|
+
private clusteredRoomIds = new Map<string, string>()
|
|
37
|
+
private roomIdLookups = new Map<string, Promise<string>>()
|
|
38
|
+
private roomResolutionWarningPrefix: string
|
|
39
|
+
|
|
40
|
+
constructor(options: WebexClientOptions = {}) {
|
|
41
|
+
this.roomResolutionWarningPrefix = options.roomResolutionWarningPrefix ?? '[webex]'
|
|
42
|
+
}
|
|
25
43
|
|
|
26
44
|
async login(credentials?: { token: string; deviceUrl?: string; tokenType?: string }): Promise<this> {
|
|
27
45
|
if (credentials) {
|
|
@@ -204,15 +222,24 @@ export class WebexClient {
|
|
|
204
222
|
async testAuth(): Promise<WebexPerson> {
|
|
205
223
|
if (this.useInternalAPI) {
|
|
206
224
|
try {
|
|
207
|
-
return await this.request<WebexPerson>('GET', '/people/me')
|
|
225
|
+
return normalizeSdkPerson(await this.request<WebexPerson>('GET', '/people/me'))
|
|
208
226
|
} catch (err) {
|
|
209
227
|
const isAuthError = err instanceof WebexError && (err.code === 'http_401' || err.code === 'http_403')
|
|
210
228
|
if (!isAuthError) throw err
|
|
211
229
|
await this.testAuthInternal()
|
|
212
|
-
return {
|
|
230
|
+
return normalizeSdkPerson({
|
|
231
|
+
id: '',
|
|
232
|
+
ref: '',
|
|
233
|
+
emails: [],
|
|
234
|
+
displayName: '',
|
|
235
|
+
orgId: '',
|
|
236
|
+
orgRef: '',
|
|
237
|
+
type: 'person',
|
|
238
|
+
created: '',
|
|
239
|
+
})
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
|
-
return this.request<WebexPerson>('GET', '/people/me')
|
|
242
|
+
return normalizeSdkPerson(await this.request<WebexPerson>('GET', '/people/me'))
|
|
216
243
|
}
|
|
217
244
|
|
|
218
245
|
private async testAuthInternal(): Promise<void> {
|
|
@@ -245,8 +272,55 @@ export class WebexClient {
|
|
|
245
272
|
}
|
|
246
273
|
}
|
|
247
274
|
|
|
275
|
+
async resolveRoomId(roomId: string): Promise<string> {
|
|
276
|
+
const decoded = decodeWebexId(roomId)
|
|
277
|
+
let uuid: string
|
|
278
|
+
let fallback: string
|
|
279
|
+
|
|
280
|
+
if (decoded) {
|
|
281
|
+
if (decoded.type !== 'ROOM' || decoded.cluster.startsWith('urn:')) return roomId
|
|
282
|
+
uuid = decoded.uuid
|
|
283
|
+
fallback = roomId
|
|
284
|
+
} else if (looksLikeUuid(roomId)) {
|
|
285
|
+
uuid = roomId
|
|
286
|
+
fallback = toRestId(roomId, 'ROOM')
|
|
287
|
+
} else {
|
|
288
|
+
return roomId
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const cached = this.clusteredRoomIds.get(uuid)
|
|
292
|
+
if (cached) return cached
|
|
293
|
+
|
|
294
|
+
const inFlight = this.roomIdLookups.get(uuid)
|
|
295
|
+
if (inFlight) return inFlight
|
|
296
|
+
|
|
297
|
+
const lookup = this.lookupRoomId(uuid, fallback)
|
|
298
|
+
this.roomIdLookups.set(uuid, lookup)
|
|
299
|
+
try {
|
|
300
|
+
return await lookup
|
|
301
|
+
} finally {
|
|
302
|
+
this.roomIdLookups.delete(uuid)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async resolvePersonId(personId: string): Promise<string> {
|
|
307
|
+
if (!personId || decodeWebexId(personId)) return personId
|
|
308
|
+
|
|
309
|
+
if (looksLikeEmail(personId)) {
|
|
310
|
+
const [person] = await this.listPeople({ email: personId, max: 1 })
|
|
311
|
+
if (!person) {
|
|
312
|
+
throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
|
|
313
|
+
}
|
|
314
|
+
return person.id
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (looksLikeUuid(personId)) return toRestId(personId, 'PEOPLE')
|
|
318
|
+
return personId
|
|
319
|
+
}
|
|
320
|
+
|
|
248
321
|
async getSpace(spaceId: string): Promise<WebexSpace> {
|
|
249
|
-
|
|
322
|
+
const resolvedSpaceId = await this.resolveRoomId(spaceId)
|
|
323
|
+
return this.request<WebexSpace>('GET', `/rooms/${resolvedSpaceId}`)
|
|
250
324
|
}
|
|
251
325
|
|
|
252
326
|
async sendMessage(
|
|
@@ -254,15 +328,18 @@ export class WebexClient {
|
|
|
254
328
|
text: string,
|
|
255
329
|
options?: { markdown?: boolean; parentId?: string; files?: string[] },
|
|
256
330
|
): Promise<WebexMessage> {
|
|
331
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
332
|
+
const resolvedOptions = this.resolveMessageOptions(options)
|
|
333
|
+
|
|
257
334
|
if (this.useInternalAPI) {
|
|
258
|
-
return this.sendMessageInternal(
|
|
335
|
+
return this.sendMessageInternal(resolvedRoomId, text, resolvedOptions)
|
|
259
336
|
}
|
|
260
|
-
const body: Record<string, unknown> = { roomId }
|
|
261
|
-
if (
|
|
337
|
+
const body: Record<string, unknown> = { roomId: resolvedRoomId }
|
|
338
|
+
if (resolvedOptions?.markdown) body.markdown = text
|
|
262
339
|
else body.text = text
|
|
263
|
-
if (
|
|
264
|
-
if (
|
|
265
|
-
return this.request<WebexMessage>('POST', '/messages', body)
|
|
340
|
+
if (resolvedOptions?.parentId) body.parentId = resolvedOptions.parentId
|
|
341
|
+
if (resolvedOptions?.files?.length) body.files = resolvedOptions.files
|
|
342
|
+
return normalizeSdkMessage(await this.request<WebexMessage>('POST', '/messages', body))
|
|
266
343
|
}
|
|
267
344
|
|
|
268
345
|
private get useInternalAPI(): boolean {
|
|
@@ -283,7 +360,7 @@ export class WebexClient {
|
|
|
283
360
|
}
|
|
284
361
|
|
|
285
362
|
private decodeConvUuid(roomId: string): string {
|
|
286
|
-
return
|
|
363
|
+
return decodeWebexId(roomId)?.uuid ?? roomId
|
|
287
364
|
}
|
|
288
365
|
|
|
289
366
|
private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
|
@@ -314,15 +391,18 @@ export class WebexClient {
|
|
|
314
391
|
}
|
|
315
392
|
}
|
|
316
393
|
|
|
317
|
-
return {
|
|
318
|
-
id: a.id,
|
|
394
|
+
return normalizeSdkMessage({
|
|
395
|
+
id: this.normalizeMessageId(a.id),
|
|
396
|
+
ref: '',
|
|
319
397
|
roomId,
|
|
398
|
+
roomRef: '',
|
|
320
399
|
roomType: 'group' as const,
|
|
321
400
|
text,
|
|
322
|
-
personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
|
|
401
|
+
personId: this.normalizePersonId(a.actor?.entryUUID ?? a.actor?.id ?? ''),
|
|
402
|
+
personRef: '',
|
|
323
403
|
personEmail: a.actor?.emailAddress ?? '',
|
|
324
404
|
created: a.published,
|
|
325
|
-
}
|
|
405
|
+
})
|
|
326
406
|
}
|
|
327
407
|
|
|
328
408
|
private async buildEncryptedObject(
|
|
@@ -407,7 +487,7 @@ export class WebexClient {
|
|
|
407
487
|
const body = options?.markdown
|
|
408
488
|
? { toPersonEmail: personEmail, markdown: text }
|
|
409
489
|
: { toPersonEmail: personEmail, text }
|
|
410
|
-
return this.request<WebexMessage>('POST', '/messages', body)
|
|
490
|
+
return normalizeSdkMessage(await this.request<WebexMessage>('POST', '/messages', body))
|
|
411
491
|
}
|
|
412
492
|
|
|
413
493
|
private async findDirectRoomByEmail(email: string): Promise<string | null> {
|
|
@@ -425,27 +505,31 @@ export class WebexClient {
|
|
|
425
505
|
roomId: string,
|
|
426
506
|
options?: { max?: number; mentionedPeople?: string; parentId?: string },
|
|
427
507
|
): Promise<WebexMessage[]> {
|
|
508
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
509
|
+
const resolvedOptions = await this.resolveListMessageOptions(options)
|
|
510
|
+
|
|
428
511
|
if (this.useInternalAPI) {
|
|
429
|
-
const convUuid = this.decodeConvUuid(
|
|
430
|
-
const max =
|
|
512
|
+
const convUuid = this.decodeConvUuid(resolvedRoomId)
|
|
513
|
+
const max = resolvedOptions?.max ?? 50
|
|
431
514
|
const conv = await this.internalRequest<InternalConversation>(
|
|
432
515
|
`/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
|
|
433
516
|
)
|
|
434
517
|
const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
|
|
435
|
-
return Promise.all(activities.map((a) => this.activityToMessage(a,
|
|
518
|
+
return Promise.all(activities.map((a) => this.activityToMessage(a, resolvedRoomId)))
|
|
436
519
|
}
|
|
437
520
|
const params = new URLSearchParams()
|
|
438
|
-
params.set('roomId',
|
|
439
|
-
params.set('max', String(
|
|
440
|
-
if (
|
|
441
|
-
if (
|
|
521
|
+
params.set('roomId', resolvedRoomId)
|
|
522
|
+
params.set('max', String(resolvedOptions?.max ?? 50))
|
|
523
|
+
if (resolvedOptions?.mentionedPeople) params.set('mentionedPeople', resolvedOptions.mentionedPeople)
|
|
524
|
+
if (resolvedOptions?.parentId) params.set('parentId', resolvedOptions.parentId)
|
|
442
525
|
const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
|
|
443
|
-
return data.items
|
|
526
|
+
return data.items.map(normalizeSdkMessage)
|
|
444
527
|
}
|
|
445
528
|
|
|
446
529
|
async getMessage(messageId: string): Promise<WebexMessage> {
|
|
447
530
|
if (this.useInternalAPI) {
|
|
448
|
-
const
|
|
531
|
+
const activityId = this.toMessageRef(messageId)
|
|
532
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
|
|
449
533
|
const convId = activity.target?.id ?? ''
|
|
450
534
|
// Internal API responses don't carry the cluster shard (e.g. `us-west-2_r`) the
|
|
451
535
|
// public roomId encoding requires. The `unknown` placeholder is a sentinel — it
|
|
@@ -455,25 +539,26 @@ export class WebexClient {
|
|
|
455
539
|
const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
|
|
456
540
|
return this.activityToMessage(activity, roomId)
|
|
457
541
|
}
|
|
458
|
-
return this.request<WebexMessage>('GET', `/messages/${messageId}`)
|
|
542
|
+
return normalizeSdkMessage(await this.request<WebexMessage>('GET', `/messages/${this.resolveMessageId(messageId)}`))
|
|
459
543
|
}
|
|
460
544
|
|
|
461
545
|
async deleteMessage(messageId: string): Promise<void> {
|
|
462
546
|
if (this.useInternalAPI) {
|
|
463
|
-
const
|
|
547
|
+
const activityId = this.toMessageRef(messageId)
|
|
548
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
|
|
464
549
|
const convId = activity.target?.id
|
|
465
550
|
if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
|
|
466
551
|
await this.internalRequest<unknown>('/activities', {
|
|
467
552
|
method: 'POST',
|
|
468
553
|
body: JSON.stringify({
|
|
469
554
|
verb: 'delete',
|
|
470
|
-
object: { id:
|
|
555
|
+
object: { id: activityId, objectType: 'activity' },
|
|
471
556
|
target: { id: convId, objectType: 'conversation' },
|
|
472
557
|
}),
|
|
473
558
|
})
|
|
474
559
|
return
|
|
475
560
|
}
|
|
476
|
-
return this.request<void>('DELETE', `/messages/${messageId}`)
|
|
561
|
+
return this.request<void>('DELETE', `/messages/${this.resolveMessageId(messageId)}`)
|
|
477
562
|
}
|
|
478
563
|
|
|
479
564
|
async editMessage(
|
|
@@ -482,8 +567,11 @@ export class WebexClient {
|
|
|
482
567
|
text: string,
|
|
483
568
|
options?: { markdown?: boolean },
|
|
484
569
|
): Promise<WebexMessage> {
|
|
570
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
571
|
+
|
|
485
572
|
if (this.useInternalAPI) {
|
|
486
|
-
const
|
|
573
|
+
const activityId = this.toMessageRef(messageId)
|
|
574
|
+
const convUuid = this.decodeConvUuid(resolvedRoomId)
|
|
487
575
|
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, {
|
|
488
576
|
...options,
|
|
489
577
|
forEdit: true,
|
|
@@ -493,7 +581,7 @@ export class WebexClient {
|
|
|
493
581
|
verb: 'post',
|
|
494
582
|
object,
|
|
495
583
|
target: { id: convUuid, objectType: 'conversation' },
|
|
496
|
-
parent: { id:
|
|
584
|
+
parent: { id: activityId, type: 'edit' },
|
|
497
585
|
clientTempId: `tmp-${Date.now()}-edit`,
|
|
498
586
|
}
|
|
499
587
|
|
|
@@ -508,17 +596,19 @@ export class WebexClient {
|
|
|
508
596
|
|
|
509
597
|
// Tolerate responses that omit `parent` (server may return minimal shape) —
|
|
510
598
|
// only fail on an explicit mismatch between the echoed parent and the edited id.
|
|
511
|
-
if (result.parent && result.parent.id !==
|
|
599
|
+
if (result.parent && result.parent.id !== activityId) {
|
|
512
600
|
throw new WebexError(
|
|
513
|
-
`Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${
|
|
601
|
+
`Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${activityId}.`,
|
|
514
602
|
'edit_failed',
|
|
515
603
|
)
|
|
516
604
|
}
|
|
517
605
|
|
|
518
|
-
return this.activityToMessage(result,
|
|
606
|
+
return this.activityToMessage(result, resolvedRoomId)
|
|
519
607
|
}
|
|
520
|
-
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
521
|
-
return
|
|
608
|
+
const body = options?.markdown ? { roomId: resolvedRoomId, markdown: text } : { roomId: resolvedRoomId, text }
|
|
609
|
+
return normalizeSdkMessage(
|
|
610
|
+
await this.request<WebexMessage>('PUT', `/messages/${this.resolveMessageId(messageId)}`, body),
|
|
611
|
+
)
|
|
522
612
|
}
|
|
523
613
|
|
|
524
614
|
async listPeople(options?: { email?: string; displayName?: string; max?: number }): Promise<WebexPerson[]> {
|
|
@@ -529,26 +619,34 @@ export class WebexClient {
|
|
|
529
619
|
const query = params.toString()
|
|
530
620
|
const path = query ? `/people?${query}` : '/people'
|
|
531
621
|
const data = await this.request<{ items: WebexPerson[] }>('GET', path)
|
|
532
|
-
return data.items
|
|
622
|
+
return data.items.map(normalizeSdkPerson)
|
|
533
623
|
}
|
|
534
624
|
|
|
535
625
|
async getPerson(personId: string): Promise<WebexPerson> {
|
|
536
|
-
|
|
626
|
+
if (!decodeWebexId(personId) && looksLikeEmail(personId)) {
|
|
627
|
+
const [person] = await this.listPeople({ email: personId, max: 1 })
|
|
628
|
+
if (!person) {
|
|
629
|
+
throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
|
|
630
|
+
}
|
|
631
|
+
return person
|
|
632
|
+
}
|
|
633
|
+
return normalizeSdkPerson(await this.request<WebexPerson>('GET', `/people/${await this.resolvePersonId(personId)}`))
|
|
537
634
|
}
|
|
538
635
|
|
|
539
636
|
async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
|
|
540
637
|
const params = new URLSearchParams()
|
|
541
638
|
params.set('max', String(options?.max ?? 100))
|
|
542
639
|
const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
|
|
543
|
-
return data.items
|
|
640
|
+
return data.items.map(normalizeSdkMembership)
|
|
544
641
|
}
|
|
545
642
|
|
|
546
643
|
async listMemberships(roomId: string, options?: { max?: number }): Promise<WebexMembership[]> {
|
|
644
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
547
645
|
const params = new URLSearchParams()
|
|
548
|
-
params.set('roomId',
|
|
646
|
+
params.set('roomId', resolvedRoomId)
|
|
549
647
|
if (options?.max) params.set('max', String(options.max))
|
|
550
648
|
const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
|
|
551
|
-
return data.items
|
|
649
|
+
return data.items.map(normalizeSdkMembership)
|
|
552
650
|
}
|
|
553
651
|
|
|
554
652
|
async uploadFile(
|
|
@@ -556,12 +654,14 @@ export class WebexClient {
|
|
|
556
654
|
file: { content: Blob; filename: string },
|
|
557
655
|
options?: { text?: string; markdown?: boolean; parentId?: string },
|
|
558
656
|
): Promise<WebexMessage> {
|
|
657
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
658
|
+
const resolvedParentId = options?.parentId ? this.resolveMessageId(options.parentId) : undefined
|
|
559
659
|
const form = new FormData()
|
|
560
|
-
form.set('roomId',
|
|
660
|
+
form.set('roomId', resolvedRoomId)
|
|
561
661
|
if (options?.text) {
|
|
562
662
|
form.set(options.markdown ? 'markdown' : 'text', options.text)
|
|
563
663
|
}
|
|
564
|
-
if (
|
|
664
|
+
if (resolvedParentId) form.set('parentId', resolvedParentId)
|
|
565
665
|
form.set('files', file.content, file.filename)
|
|
566
666
|
|
|
567
667
|
const response = await fetch(`${BASE_URL}/messages`, {
|
|
@@ -574,7 +674,81 @@ export class WebexClient {
|
|
|
574
674
|
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
575
675
|
throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
|
|
576
676
|
}
|
|
577
|
-
return response.json() as
|
|
677
|
+
return normalizeSdkMessage((await response.json()) as WebexMessage)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private async lookupRoomId(uuid: string, fallback: string): Promise<string> {
|
|
681
|
+
try {
|
|
682
|
+
// Page through every room the account belongs to, stopping as soon as the
|
|
683
|
+
// trailing UUID matches because room titles are not stable identifiers.
|
|
684
|
+
for await (const room of this.iterateSpaces({ max: 1000 })) {
|
|
685
|
+
if (decodeWebexId(room.id)?.uuid === uuid) {
|
|
686
|
+
this.clusteredRoomIds.set(uuid, room.id)
|
|
687
|
+
return room.id
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
// Network/auth failure: fail open to the un-corrected id rather than block the call.
|
|
692
|
+
return fallback
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
console.warn(
|
|
696
|
+
`${this.roomResolutionWarningPrefix} Could not resolve clustered room id for ${uuid}; falling back to the un-clustered id. ` +
|
|
697
|
+
'Room-scoped calls may fail if this room lives on a non-default Webex cluster.',
|
|
698
|
+
)
|
|
699
|
+
return fallback
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private resolveMessageOptions(options?: {
|
|
703
|
+
markdown?: boolean
|
|
704
|
+
parentId?: string
|
|
705
|
+
files?: string[]
|
|
706
|
+
}): { markdown?: boolean; parentId?: string; files?: string[] } | undefined {
|
|
707
|
+
if (!options?.parentId) return options
|
|
708
|
+
return { ...options, parentId: this.resolveMessageId(options.parentId) }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private async resolveListMessageOptions(options?: {
|
|
712
|
+
max?: number
|
|
713
|
+
mentionedPeople?: string
|
|
714
|
+
parentId?: string
|
|
715
|
+
}): Promise<{ max?: number; mentionedPeople?: string; parentId?: string } | undefined> {
|
|
716
|
+
if (!options) return undefined
|
|
717
|
+
const resolved = { ...options }
|
|
718
|
+
if (options.mentionedPeople) {
|
|
719
|
+
resolved.mentionedPeople = await this.resolveMentionedPeople(options.mentionedPeople)
|
|
720
|
+
}
|
|
721
|
+
if (options.parentId) {
|
|
722
|
+
resolved.parentId = this.resolveMessageId(options.parentId)
|
|
723
|
+
}
|
|
724
|
+
return resolved
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private async resolveMentionedPeople(mentionedPeople: string): Promise<string> {
|
|
728
|
+
if (mentionedPeople === 'me') return mentionedPeople
|
|
729
|
+
return this.resolvePersonId(mentionedPeople)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private resolveMessageId(messageId: string): string {
|
|
733
|
+
if (!messageId || decodeWebexId(messageId)) return messageId
|
|
734
|
+
// A lone message UUID does not identify its room cluster, so cluster correction
|
|
735
|
+
// is not possible without the room context.
|
|
736
|
+
if (looksLikeUuid(messageId)) return toRestId(messageId, 'MESSAGE')
|
|
737
|
+
return messageId
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private toMessageRef(messageId: string): string {
|
|
741
|
+
return decodeWebexId(messageId)?.uuid ?? messageId
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private normalizeMessageId(messageId: string): string {
|
|
745
|
+
if (!messageId || decodeWebexId(messageId)) return messageId
|
|
746
|
+
return toRestId(messageId, 'MESSAGE')
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private normalizePersonId(personId: string): string {
|
|
750
|
+
if (!personId || decodeWebexId(personId)) return personId
|
|
751
|
+
return toRestId(personId, 'PEOPLE')
|
|
578
752
|
}
|
|
579
753
|
|
|
580
754
|
async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
|
|
@@ -638,6 +812,14 @@ function sanitizeFilename(name: string | undefined): string | undefined {
|
|
|
638
812
|
return base
|
|
639
813
|
}
|
|
640
814
|
|
|
815
|
+
function looksLikeUuid(value: string): boolean {
|
|
816
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function looksLikeEmail(value: string): boolean {
|
|
820
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
821
|
+
}
|
|
822
|
+
|
|
641
823
|
interface InternalActivity {
|
|
642
824
|
id: string
|
|
643
825
|
verb: string
|
|
@@ -9,6 +9,8 @@ import { WebexTokenExtractor } from '../token-extractor'
|
|
|
9
9
|
import { WebexError } from '../types'
|
|
10
10
|
import { extractAction, loginAction, logoutAction, oauthAction, statusAction } from './auth'
|
|
11
11
|
|
|
12
|
+
const personId = Buffer.from('ciscospark://us/PEOPLE/person-1').toString('base64url')
|
|
13
|
+
|
|
12
14
|
let promptQueue: string[] = []
|
|
13
15
|
mock.module('node:readline/promises', () => ({
|
|
14
16
|
createInterface: () => ({
|
|
@@ -33,7 +35,7 @@ describe('auth commands', () => {
|
|
|
33
35
|
let originalStdinTTY: boolean | undefined
|
|
34
36
|
let originalStdoutTTY: boolean | undefined
|
|
35
37
|
const mockPerson = {
|
|
36
|
-
id:
|
|
38
|
+
id: personId,
|
|
37
39
|
displayName: 'Test User',
|
|
38
40
|
emails: ['test@example.com'],
|
|
39
41
|
orgId: 'org-1',
|