agent-messenger 2.23.1 → 2.23.2
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 +178 -37
- 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 +10 -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 +3 -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 +3 -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 +3 -0
- package/dist/src/platforms/webex/commands/whoami.js.map +1 -1
- package/dist/src/platforms/webex/id-normalizer.d.ts +7 -0
- package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -1
- package/dist/src/platforms/webex/id-normalizer.js +16 -0
- 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/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 +3 -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 +3 -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 +7 -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 +4 -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 +3 -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 +194 -32
- 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 +18 -8
- package/src/platforms/webex/commands/member.ts +3 -0
- package/src/platforms/webex/commands/message.test.ts +31 -23
- package/src/platforms/webex/commands/message.ts +3 -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 +14 -6
- package/src/platforms/webex/commands/whoami.ts +3 -0
- package/src/platforms/webex/id-normalizer.test.ts +37 -0
- package/src/platforms/webex/id-normalizer.ts +21 -0
- package/src/platforms/webex/index.ts +2 -2
- package/src/platforms/webexbot/client.ts +8 -74
- package/src/platforms/webexbot/commands/file.ts +5 -0
- package/src/platforms/webexbot/commands/member.ts +5 -0
- package/src/platforms/webexbot/commands/message.ts +11 -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 +11 -5
- package/src/platforms/webexbot/commands/user.ts +7 -0
- package/src/platforms/webexbot/commands/whoami.ts +5 -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,5 +1,6 @@
|
|
|
1
1
|
import { WebexCredentialManager } from './credential-manager'
|
|
2
2
|
import { WebexEncryptionService } from './encryption'
|
|
3
|
+
import { decodeWebexId, toRestId } from './id-normalizer'
|
|
3
4
|
import { KmsKeyProvider } from './kms-key-provider'
|
|
4
5
|
import { escapeHtml, markdownToHtml, stripMarkdown } from './markdown-to-html'
|
|
5
6
|
import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
@@ -15,6 +16,10 @@ interface RateLimitBucket {
|
|
|
15
16
|
resetAt: number
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
interface WebexClientOptions {
|
|
20
|
+
roomResolutionWarningPrefix?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export class WebexClient {
|
|
19
24
|
private token: string | null = null
|
|
20
25
|
private deviceUrl: string | null = null
|
|
@@ -22,6 +27,13 @@ export class WebexClient {
|
|
|
22
27
|
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
23
28
|
private globalRateLimitUntil: number = 0
|
|
24
29
|
private encryption: WebexEncryptionService | null = null
|
|
30
|
+
private clusteredRoomIds = new Map<string, string>()
|
|
31
|
+
private roomIdLookups = new Map<string, Promise<string>>()
|
|
32
|
+
private roomResolutionWarningPrefix: string
|
|
33
|
+
|
|
34
|
+
constructor(options: WebexClientOptions = {}) {
|
|
35
|
+
this.roomResolutionWarningPrefix = options.roomResolutionWarningPrefix ?? '[webex]'
|
|
36
|
+
}
|
|
25
37
|
|
|
26
38
|
async login(credentials?: { token: string; deviceUrl?: string; tokenType?: string }): Promise<this> {
|
|
27
39
|
if (credentials) {
|
|
@@ -245,8 +257,55 @@ export class WebexClient {
|
|
|
245
257
|
}
|
|
246
258
|
}
|
|
247
259
|
|
|
260
|
+
async resolveRoomId(roomId: string): Promise<string> {
|
|
261
|
+
const decoded = decodeWebexId(roomId)
|
|
262
|
+
let uuid: string
|
|
263
|
+
let fallback: string
|
|
264
|
+
|
|
265
|
+
if (decoded) {
|
|
266
|
+
if (decoded.type !== 'ROOM' || decoded.cluster.startsWith('urn:')) return roomId
|
|
267
|
+
uuid = decoded.uuid
|
|
268
|
+
fallback = roomId
|
|
269
|
+
} else if (looksLikeUuid(roomId)) {
|
|
270
|
+
uuid = roomId
|
|
271
|
+
fallback = toRestId(roomId, 'ROOM')
|
|
272
|
+
} else {
|
|
273
|
+
return roomId
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const cached = this.clusteredRoomIds.get(uuid)
|
|
277
|
+
if (cached) return cached
|
|
278
|
+
|
|
279
|
+
const inFlight = this.roomIdLookups.get(uuid)
|
|
280
|
+
if (inFlight) return inFlight
|
|
281
|
+
|
|
282
|
+
const lookup = this.lookupRoomId(uuid, fallback)
|
|
283
|
+
this.roomIdLookups.set(uuid, lookup)
|
|
284
|
+
try {
|
|
285
|
+
return await lookup
|
|
286
|
+
} finally {
|
|
287
|
+
this.roomIdLookups.delete(uuid)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async resolvePersonId(personId: string): Promise<string> {
|
|
292
|
+
if (!personId || decodeWebexId(personId)) return personId
|
|
293
|
+
|
|
294
|
+
if (looksLikeEmail(personId)) {
|
|
295
|
+
const [person] = await this.listPeople({ email: personId, max: 1 })
|
|
296
|
+
if (!person) {
|
|
297
|
+
throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
|
|
298
|
+
}
|
|
299
|
+
return person.id
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (looksLikeUuid(personId)) return toRestId(personId, 'PEOPLE')
|
|
303
|
+
return personId
|
|
304
|
+
}
|
|
305
|
+
|
|
248
306
|
async getSpace(spaceId: string): Promise<WebexSpace> {
|
|
249
|
-
|
|
307
|
+
const resolvedSpaceId = await this.resolveRoomId(spaceId)
|
|
308
|
+
return this.request<WebexSpace>('GET', `/rooms/${resolvedSpaceId}`)
|
|
250
309
|
}
|
|
251
310
|
|
|
252
311
|
async sendMessage(
|
|
@@ -254,14 +313,17 @@ export class WebexClient {
|
|
|
254
313
|
text: string,
|
|
255
314
|
options?: { markdown?: boolean; parentId?: string; files?: string[] },
|
|
256
315
|
): Promise<WebexMessage> {
|
|
316
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
317
|
+
const resolvedOptions = this.resolveMessageOptions(options)
|
|
318
|
+
|
|
257
319
|
if (this.useInternalAPI) {
|
|
258
|
-
return this.sendMessageInternal(
|
|
320
|
+
return this.sendMessageInternal(resolvedRoomId, text, resolvedOptions)
|
|
259
321
|
}
|
|
260
|
-
const body: Record<string, unknown> = { roomId }
|
|
261
|
-
if (
|
|
322
|
+
const body: Record<string, unknown> = { roomId: resolvedRoomId }
|
|
323
|
+
if (resolvedOptions?.markdown) body.markdown = text
|
|
262
324
|
else body.text = text
|
|
263
|
-
if (
|
|
264
|
-
if (
|
|
325
|
+
if (resolvedOptions?.parentId) body.parentId = resolvedOptions.parentId
|
|
326
|
+
if (resolvedOptions?.files?.length) body.files = resolvedOptions.files
|
|
265
327
|
return this.request<WebexMessage>('POST', '/messages', body)
|
|
266
328
|
}
|
|
267
329
|
|
|
@@ -283,7 +345,7 @@ export class WebexClient {
|
|
|
283
345
|
}
|
|
284
346
|
|
|
285
347
|
private decodeConvUuid(roomId: string): string {
|
|
286
|
-
return
|
|
348
|
+
return decodeWebexId(roomId)?.uuid ?? roomId
|
|
287
349
|
}
|
|
288
350
|
|
|
289
351
|
private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
|
@@ -315,11 +377,11 @@ export class WebexClient {
|
|
|
315
377
|
}
|
|
316
378
|
|
|
317
379
|
return {
|
|
318
|
-
id: a.id,
|
|
380
|
+
id: this.normalizeMessageId(a.id),
|
|
319
381
|
roomId,
|
|
320
382
|
roomType: 'group' as const,
|
|
321
383
|
text,
|
|
322
|
-
personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
|
|
384
|
+
personId: this.normalizePersonId(a.actor?.entryUUID ?? a.actor?.id ?? ''),
|
|
323
385
|
personEmail: a.actor?.emailAddress ?? '',
|
|
324
386
|
created: a.published,
|
|
325
387
|
}
|
|
@@ -425,27 +487,31 @@ export class WebexClient {
|
|
|
425
487
|
roomId: string,
|
|
426
488
|
options?: { max?: number; mentionedPeople?: string; parentId?: string },
|
|
427
489
|
): Promise<WebexMessage[]> {
|
|
490
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
491
|
+
const resolvedOptions = await this.resolveListMessageOptions(options)
|
|
492
|
+
|
|
428
493
|
if (this.useInternalAPI) {
|
|
429
|
-
const convUuid = this.decodeConvUuid(
|
|
430
|
-
const max =
|
|
494
|
+
const convUuid = this.decodeConvUuid(resolvedRoomId)
|
|
495
|
+
const max = resolvedOptions?.max ?? 50
|
|
431
496
|
const conv = await this.internalRequest<InternalConversation>(
|
|
432
497
|
`/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
|
|
433
498
|
)
|
|
434
499
|
const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
|
|
435
|
-
return Promise.all(activities.map((a) => this.activityToMessage(a,
|
|
500
|
+
return Promise.all(activities.map((a) => this.activityToMessage(a, resolvedRoomId)))
|
|
436
501
|
}
|
|
437
502
|
const params = new URLSearchParams()
|
|
438
|
-
params.set('roomId',
|
|
439
|
-
params.set('max', String(
|
|
440
|
-
if (
|
|
441
|
-
if (
|
|
503
|
+
params.set('roomId', resolvedRoomId)
|
|
504
|
+
params.set('max', String(resolvedOptions?.max ?? 50))
|
|
505
|
+
if (resolvedOptions?.mentionedPeople) params.set('mentionedPeople', resolvedOptions.mentionedPeople)
|
|
506
|
+
if (resolvedOptions?.parentId) params.set('parentId', resolvedOptions.parentId)
|
|
442
507
|
const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
|
|
443
508
|
return data.items
|
|
444
509
|
}
|
|
445
510
|
|
|
446
511
|
async getMessage(messageId: string): Promise<WebexMessage> {
|
|
447
512
|
if (this.useInternalAPI) {
|
|
448
|
-
const
|
|
513
|
+
const activityId = this.toMessageRef(messageId)
|
|
514
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
|
|
449
515
|
const convId = activity.target?.id ?? ''
|
|
450
516
|
// Internal API responses don't carry the cluster shard (e.g. `us-west-2_r`) the
|
|
451
517
|
// public roomId encoding requires. The `unknown` placeholder is a sentinel — it
|
|
@@ -455,25 +521,26 @@ export class WebexClient {
|
|
|
455
521
|
const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
|
|
456
522
|
return this.activityToMessage(activity, roomId)
|
|
457
523
|
}
|
|
458
|
-
return this.request<WebexMessage>('GET', `/messages/${messageId}`)
|
|
524
|
+
return this.request<WebexMessage>('GET', `/messages/${this.resolveMessageId(messageId)}`)
|
|
459
525
|
}
|
|
460
526
|
|
|
461
527
|
async deleteMessage(messageId: string): Promise<void> {
|
|
462
528
|
if (this.useInternalAPI) {
|
|
463
|
-
const
|
|
529
|
+
const activityId = this.toMessageRef(messageId)
|
|
530
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
|
|
464
531
|
const convId = activity.target?.id
|
|
465
532
|
if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
|
|
466
533
|
await this.internalRequest<unknown>('/activities', {
|
|
467
534
|
method: 'POST',
|
|
468
535
|
body: JSON.stringify({
|
|
469
536
|
verb: 'delete',
|
|
470
|
-
object: { id:
|
|
537
|
+
object: { id: activityId, objectType: 'activity' },
|
|
471
538
|
target: { id: convId, objectType: 'conversation' },
|
|
472
539
|
}),
|
|
473
540
|
})
|
|
474
541
|
return
|
|
475
542
|
}
|
|
476
|
-
return this.request<void>('DELETE', `/messages/${messageId}`)
|
|
543
|
+
return this.request<void>('DELETE', `/messages/${this.resolveMessageId(messageId)}`)
|
|
477
544
|
}
|
|
478
545
|
|
|
479
546
|
async editMessage(
|
|
@@ -482,8 +549,11 @@ export class WebexClient {
|
|
|
482
549
|
text: string,
|
|
483
550
|
options?: { markdown?: boolean },
|
|
484
551
|
): Promise<WebexMessage> {
|
|
552
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
553
|
+
|
|
485
554
|
if (this.useInternalAPI) {
|
|
486
|
-
const
|
|
555
|
+
const activityId = this.toMessageRef(messageId)
|
|
556
|
+
const convUuid = this.decodeConvUuid(resolvedRoomId)
|
|
487
557
|
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, {
|
|
488
558
|
...options,
|
|
489
559
|
forEdit: true,
|
|
@@ -493,7 +563,7 @@ export class WebexClient {
|
|
|
493
563
|
verb: 'post',
|
|
494
564
|
object,
|
|
495
565
|
target: { id: convUuid, objectType: 'conversation' },
|
|
496
|
-
parent: { id:
|
|
566
|
+
parent: { id: activityId, type: 'edit' },
|
|
497
567
|
clientTempId: `tmp-${Date.now()}-edit`,
|
|
498
568
|
}
|
|
499
569
|
|
|
@@ -508,17 +578,17 @@ export class WebexClient {
|
|
|
508
578
|
|
|
509
579
|
// Tolerate responses that omit `parent` (server may return minimal shape) —
|
|
510
580
|
// only fail on an explicit mismatch between the echoed parent and the edited id.
|
|
511
|
-
if (result.parent && result.parent.id !==
|
|
581
|
+
if (result.parent && result.parent.id !== activityId) {
|
|
512
582
|
throw new WebexError(
|
|
513
|
-
`Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${
|
|
583
|
+
`Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${activityId}.`,
|
|
514
584
|
'edit_failed',
|
|
515
585
|
)
|
|
516
586
|
}
|
|
517
587
|
|
|
518
|
-
return this.activityToMessage(result,
|
|
588
|
+
return this.activityToMessage(result, resolvedRoomId)
|
|
519
589
|
}
|
|
520
|
-
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
521
|
-
return this.request<WebexMessage>('PUT', `/messages/${messageId}`, body)
|
|
590
|
+
const body = options?.markdown ? { roomId: resolvedRoomId, markdown: text } : { roomId: resolvedRoomId, text }
|
|
591
|
+
return this.request<WebexMessage>('PUT', `/messages/${this.resolveMessageId(messageId)}`, body)
|
|
522
592
|
}
|
|
523
593
|
|
|
524
594
|
async listPeople(options?: { email?: string; displayName?: string; max?: number }): Promise<WebexPerson[]> {
|
|
@@ -533,7 +603,14 @@ export class WebexClient {
|
|
|
533
603
|
}
|
|
534
604
|
|
|
535
605
|
async getPerson(personId: string): Promise<WebexPerson> {
|
|
536
|
-
|
|
606
|
+
if (!decodeWebexId(personId) && looksLikeEmail(personId)) {
|
|
607
|
+
const [person] = await this.listPeople({ email: personId, max: 1 })
|
|
608
|
+
if (!person) {
|
|
609
|
+
throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
|
|
610
|
+
}
|
|
611
|
+
return person
|
|
612
|
+
}
|
|
613
|
+
return this.request<WebexPerson>('GET', `/people/${await this.resolvePersonId(personId)}`)
|
|
537
614
|
}
|
|
538
615
|
|
|
539
616
|
async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
|
|
@@ -544,8 +621,9 @@ export class WebexClient {
|
|
|
544
621
|
}
|
|
545
622
|
|
|
546
623
|
async listMemberships(roomId: string, options?: { max?: number }): Promise<WebexMembership[]> {
|
|
624
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
547
625
|
const params = new URLSearchParams()
|
|
548
|
-
params.set('roomId',
|
|
626
|
+
params.set('roomId', resolvedRoomId)
|
|
549
627
|
if (options?.max) params.set('max', String(options.max))
|
|
550
628
|
const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
|
|
551
629
|
return data.items
|
|
@@ -556,12 +634,14 @@ export class WebexClient {
|
|
|
556
634
|
file: { content: Blob; filename: string },
|
|
557
635
|
options?: { text?: string; markdown?: boolean; parentId?: string },
|
|
558
636
|
): Promise<WebexMessage> {
|
|
637
|
+
const resolvedRoomId = await this.resolveRoomId(roomId)
|
|
638
|
+
const resolvedParentId = options?.parentId ? this.resolveMessageId(options.parentId) : undefined
|
|
559
639
|
const form = new FormData()
|
|
560
|
-
form.set('roomId',
|
|
640
|
+
form.set('roomId', resolvedRoomId)
|
|
561
641
|
if (options?.text) {
|
|
562
642
|
form.set(options.markdown ? 'markdown' : 'text', options.text)
|
|
563
643
|
}
|
|
564
|
-
if (
|
|
644
|
+
if (resolvedParentId) form.set('parentId', resolvedParentId)
|
|
565
645
|
form.set('files', file.content, file.filename)
|
|
566
646
|
|
|
567
647
|
const response = await fetch(`${BASE_URL}/messages`, {
|
|
@@ -577,6 +657,80 @@ export class WebexClient {
|
|
|
577
657
|
return response.json() as Promise<WebexMessage>
|
|
578
658
|
}
|
|
579
659
|
|
|
660
|
+
private async lookupRoomId(uuid: string, fallback: string): Promise<string> {
|
|
661
|
+
try {
|
|
662
|
+
// Page through every room the account belongs to, stopping as soon as the
|
|
663
|
+
// trailing UUID matches because room titles are not stable identifiers.
|
|
664
|
+
for await (const room of this.iterateSpaces({ max: 1000 })) {
|
|
665
|
+
if (decodeWebexId(room.id)?.uuid === uuid) {
|
|
666
|
+
this.clusteredRoomIds.set(uuid, room.id)
|
|
667
|
+
return room.id
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch {
|
|
671
|
+
// Network/auth failure: fail open to the un-corrected id rather than block the call.
|
|
672
|
+
return fallback
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
console.warn(
|
|
676
|
+
`${this.roomResolutionWarningPrefix} Could not resolve clustered room id for ${uuid}; falling back to the un-clustered id. ` +
|
|
677
|
+
'Room-scoped calls may fail if this room lives on a non-default Webex cluster.',
|
|
678
|
+
)
|
|
679
|
+
return fallback
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private resolveMessageOptions(options?: {
|
|
683
|
+
markdown?: boolean
|
|
684
|
+
parentId?: string
|
|
685
|
+
files?: string[]
|
|
686
|
+
}): { markdown?: boolean; parentId?: string; files?: string[] } | undefined {
|
|
687
|
+
if (!options?.parentId) return options
|
|
688
|
+
return { ...options, parentId: this.resolveMessageId(options.parentId) }
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private async resolveListMessageOptions(options?: {
|
|
692
|
+
max?: number
|
|
693
|
+
mentionedPeople?: string
|
|
694
|
+
parentId?: string
|
|
695
|
+
}): Promise<{ max?: number; mentionedPeople?: string; parentId?: string } | undefined> {
|
|
696
|
+
if (!options) return undefined
|
|
697
|
+
const resolved = { ...options }
|
|
698
|
+
if (options.mentionedPeople) {
|
|
699
|
+
resolved.mentionedPeople = await this.resolveMentionedPeople(options.mentionedPeople)
|
|
700
|
+
}
|
|
701
|
+
if (options.parentId) {
|
|
702
|
+
resolved.parentId = this.resolveMessageId(options.parentId)
|
|
703
|
+
}
|
|
704
|
+
return resolved
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private async resolveMentionedPeople(mentionedPeople: string): Promise<string> {
|
|
708
|
+
if (mentionedPeople === 'me') return mentionedPeople
|
|
709
|
+
return this.resolvePersonId(mentionedPeople)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private resolveMessageId(messageId: string): string {
|
|
713
|
+
if (!messageId || decodeWebexId(messageId)) return messageId
|
|
714
|
+
// A lone message UUID does not identify its room cluster, so cluster correction
|
|
715
|
+
// is not possible without the room context.
|
|
716
|
+
if (looksLikeUuid(messageId)) return toRestId(messageId, 'MESSAGE')
|
|
717
|
+
return messageId
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private toMessageRef(messageId: string): string {
|
|
721
|
+
return decodeWebexId(messageId)?.uuid ?? messageId
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private normalizeMessageId(messageId: string): string {
|
|
725
|
+
if (!messageId || decodeWebexId(messageId)) return messageId
|
|
726
|
+
return toRestId(messageId, 'MESSAGE')
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private normalizePersonId(personId: string): string {
|
|
730
|
+
if (!personId || decodeWebexId(personId)) return personId
|
|
731
|
+
return toRestId(personId, 'PEOPLE')
|
|
732
|
+
}
|
|
733
|
+
|
|
580
734
|
async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
|
|
581
735
|
const url = this.resolveContentUrl(contentRef)
|
|
582
736
|
const response = await fetch(url, {
|
|
@@ -638,6 +792,14 @@ function sanitizeFilename(name: string | undefined): string | undefined {
|
|
|
638
792
|
return base
|
|
639
793
|
}
|
|
640
794
|
|
|
795
|
+
function looksLikeUuid(value: string): boolean {
|
|
796
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function looksLikeEmail(value: string): boolean {
|
|
800
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
801
|
+
}
|
|
802
|
+
|
|
641
803
|
interface InternalActivity {
|
|
642
804
|
id: string
|
|
643
805
|
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',
|
|
@@ -12,6 +12,7 @@ import { info, debug, error as stderrError } from '@/shared/utils/stderr'
|
|
|
12
12
|
import { getWebexAppCredentials } from '../app-config'
|
|
13
13
|
import { WebexClient } from '../client'
|
|
14
14
|
import { WebexCredentialManager } from '../credential-manager'
|
|
15
|
+
import { toRef } from '../id-normalizer'
|
|
15
16
|
import { loginWithPassword, WEB_CLIENT_ID, WEB_CLIENT_SECRET } from '../password-login'
|
|
16
17
|
import { WebexTokenExtractor } from '../token-extractor'
|
|
17
18
|
import { WebexError } from '../types'
|
|
@@ -21,6 +22,10 @@ interface ResolvedCredentials {
|
|
|
21
22
|
clientSecret: string
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function formatAuthUser(person: { id: string; displayName: string; emails: string[] }) {
|
|
26
|
+
return { id: person.id, ref: toRef(person.id), displayName: person.displayName, emails: person.emails }
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
async function openBrowser(url: string): Promise<void> {
|
|
25
30
|
const { exec } = await import('node:child_process')
|
|
26
31
|
const command =
|
|
@@ -170,7 +175,7 @@ async function loginWithToken(credManager: WebexCredentialManager, token: string
|
|
|
170
175
|
console.log(
|
|
171
176
|
formatOutput(
|
|
172
177
|
{
|
|
173
|
-
user:
|
|
178
|
+
user: formatAuthUser(person),
|
|
174
179
|
authenticated: true,
|
|
175
180
|
},
|
|
176
181
|
pretty,
|
|
@@ -205,7 +210,7 @@ async function loginWithEmailPassword(
|
|
|
205
210
|
formatOutput(
|
|
206
211
|
{
|
|
207
212
|
authenticated: true,
|
|
208
|
-
user:
|
|
213
|
+
user: formatAuthUser(person),
|
|
209
214
|
},
|
|
210
215
|
options.pretty,
|
|
211
216
|
),
|
|
@@ -259,7 +264,7 @@ export async function oauthAction(options: OAuthOptions): Promise<void> {
|
|
|
259
264
|
console.log(
|
|
260
265
|
formatOutput(
|
|
261
266
|
{
|
|
262
|
-
user:
|
|
267
|
+
user: formatAuthUser(person),
|
|
263
268
|
authenticated: true,
|
|
264
269
|
},
|
|
265
270
|
options.pretty,
|
|
@@ -308,7 +313,7 @@ async function finishDeviceGrant(credManager: WebexCredentialManager, options: O
|
|
|
308
313
|
formatOutput(
|
|
309
314
|
{
|
|
310
315
|
authenticated: true,
|
|
311
|
-
user:
|
|
316
|
+
user: formatAuthUser(person),
|
|
312
317
|
},
|
|
313
318
|
options.pretty,
|
|
314
319
|
),
|
|
@@ -364,7 +369,7 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
|
|
|
364
369
|
formatOutput(
|
|
365
370
|
{
|
|
366
371
|
authenticated: true,
|
|
367
|
-
user:
|
|
372
|
+
user: formatAuthUser(person),
|
|
368
373
|
},
|
|
369
374
|
options.pretty,
|
|
370
375
|
),
|
|
@@ -436,11 +441,11 @@ export async function extractAction(options: {
|
|
|
436
441
|
tokenType: 'extracted',
|
|
437
442
|
})
|
|
438
443
|
|
|
439
|
-
let person:
|
|
444
|
+
let person: ReturnType<typeof formatAuthUser> | null = null
|
|
440
445
|
try {
|
|
441
446
|
const result = await client.testAuth()
|
|
442
447
|
if (result.id) {
|
|
443
|
-
person =
|
|
448
|
+
person = formatAuthUser(result)
|
|
444
449
|
}
|
|
445
450
|
} catch (authError) {
|
|
446
451
|
const isAuthFailure =
|
|
@@ -3,20 +3,26 @@ import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
|
|
|
3
3
|
import { WebexClient } from '../client'
|
|
4
4
|
import { WebexError } from '../types'
|
|
5
5
|
|
|
6
|
+
const restId = (type: string, ref: string) => Buffer.from(`ciscospark://us/${type}/${ref}`).toString('base64url')
|
|
7
|
+
const member1Id = restId('MEMBERSHIP', 'person-1:room-1')
|
|
8
|
+
const member2Id = restId('MEMBERSHIP', 'person-2:room-1')
|
|
9
|
+
const person1Id = restId('PEOPLE', 'person-1')
|
|
10
|
+
const person2Id = restId('PEOPLE', 'person-2')
|
|
11
|
+
|
|
6
12
|
const mockMembers = [
|
|
7
13
|
{
|
|
8
|
-
id:
|
|
14
|
+
id: member1Id,
|
|
9
15
|
roomId: 'room-1',
|
|
10
|
-
personId:
|
|
16
|
+
personId: person1Id,
|
|
11
17
|
personEmail: 'alice@example.com',
|
|
12
18
|
personDisplayName: 'Alice',
|
|
13
19
|
isModerator: true,
|
|
14
20
|
created: '2024-01-01T00:00:00.000Z',
|
|
15
21
|
},
|
|
16
22
|
{
|
|
17
|
-
id:
|
|
23
|
+
id: member2Id,
|
|
18
24
|
roomId: 'room-1',
|
|
19
|
-
personId:
|
|
25
|
+
personId: person2Id,
|
|
20
26
|
personEmail: 'bob@example.com',
|
|
21
27
|
personDisplayName: 'Bob',
|
|
22
28
|
isModerator: false,
|
|
@@ -66,16 +72,20 @@ describe('member commands', () => {
|
|
|
66
72
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
67
73
|
JSON.stringify([
|
|
68
74
|
{
|
|
69
|
-
id:
|
|
70
|
-
|
|
75
|
+
id: member1Id,
|
|
76
|
+
ref: 'person-1:room-1',
|
|
77
|
+
personId: person1Id,
|
|
78
|
+
personRef: 'person-1',
|
|
71
79
|
personEmail: 'alice@example.com',
|
|
72
80
|
personDisplayName: 'Alice',
|
|
73
81
|
isModerator: true,
|
|
74
82
|
created: '2024-01-01T00:00:00.000Z',
|
|
75
83
|
},
|
|
76
84
|
{
|
|
77
|
-
id:
|
|
78
|
-
|
|
85
|
+
id: member2Id,
|
|
86
|
+
ref: 'person-2:room-1',
|
|
87
|
+
personId: person2Id,
|
|
88
|
+
personRef: 'person-2',
|
|
79
89
|
personEmail: 'bob@example.com',
|
|
80
90
|
personDisplayName: 'Bob',
|
|
81
91
|
isModerator: false,
|
|
@@ -4,6 +4,7 @@ import { handleError } from '@/shared/utils/error-handler'
|
|
|
4
4
|
import { formatOutput } from '@/shared/utils/output'
|
|
5
5
|
|
|
6
6
|
import { WebexClient } from '../client'
|
|
7
|
+
import { toRef } from '../id-normalizer'
|
|
7
8
|
|
|
8
9
|
export async function listAction(spaceId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
|
|
9
10
|
try {
|
|
@@ -12,7 +13,9 @@ export async function listAction(spaceId: string, options: { limit?: number; pre
|
|
|
12
13
|
|
|
13
14
|
const output = members.map((m) => ({
|
|
14
15
|
id: m.id,
|
|
16
|
+
ref: toRef(m.id),
|
|
15
17
|
personId: m.personId,
|
|
18
|
+
personRef: toRef(m.personId),
|
|
16
19
|
personEmail: m.personEmail,
|
|
17
20
|
personDisplayName: m.personDisplayName,
|
|
18
21
|
isModerator: m.isModerator,
|
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import { WebexClient } from '../client'
|
|
4
|
+
import { toRestId } from '../id-normalizer'
|
|
4
5
|
import { WebexError } from '../types'
|
|
5
6
|
|
|
7
|
+
const messageId = toRestId('msg_123', 'MESSAGE')
|
|
8
|
+
const message2Id = toRestId('msg_124', 'MESSAGE')
|
|
9
|
+
const roomId = toRestId('space_456', 'ROOM')
|
|
10
|
+
const personId = toRestId('person_789', 'PEOPLE')
|
|
11
|
+
|
|
6
12
|
const mockMessage = {
|
|
7
|
-
id:
|
|
8
|
-
roomId
|
|
13
|
+
id: messageId,
|
|
14
|
+
roomId,
|
|
9
15
|
roomType: 'group' as const,
|
|
10
16
|
text: 'Hello world',
|
|
11
17
|
html: '<p>Hello <a href="https://example.com">world</a></p>',
|
|
12
|
-
personId
|
|
18
|
+
personId,
|
|
13
19
|
personEmail: 'user@example.com',
|
|
14
20
|
created: '2025-01-29T10:00:00.000Z',
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const mockMessage2 = {
|
|
18
|
-
id:
|
|
19
|
-
roomId
|
|
24
|
+
id: message2Id,
|
|
25
|
+
roomId,
|
|
20
26
|
roomType: 'group' as const,
|
|
21
27
|
text: 'Second message',
|
|
22
28
|
html: '<p>Second message</p>',
|
|
23
|
-
personId
|
|
29
|
+
personId,
|
|
24
30
|
personEmail: 'user@example.com',
|
|
25
31
|
created: '2025-01-29T10:01:00.000Z',
|
|
26
32
|
}
|
|
@@ -78,10 +84,12 @@ it('calls sendMessage with correct args and outputs result', async () => {
|
|
|
78
84
|
markdown: undefined,
|
|
79
85
|
})
|
|
80
86
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
81
|
-
const output = consoleLogSpy.mock.calls[0][0]
|
|
82
|
-
expect(output).
|
|
83
|
-
expect(output).
|
|
84
|
-
expect(output).
|
|
87
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
|
88
|
+
expect(output.id).toBe(messageId)
|
|
89
|
+
expect(output.ref).toBe('msg_123')
|
|
90
|
+
expect(output.roomId).toBe(roomId)
|
|
91
|
+
expect(output.roomRef).toBe('space_456')
|
|
92
|
+
expect(output.personEmail).toBe('user@example.com')
|
|
85
93
|
expect(mockDispose).toHaveBeenCalled()
|
|
86
94
|
})
|
|
87
95
|
|
|
@@ -115,8 +123,8 @@ it('calls sendDirectMessage with email and text', async () => {
|
|
|
115
123
|
markdown: undefined,
|
|
116
124
|
})
|
|
117
125
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
118
|
-
const output = consoleLogSpy.mock.calls[0][0]
|
|
119
|
-
expect(output).
|
|
126
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
|
127
|
+
expect(output.ref).toBe('msg_123')
|
|
120
128
|
})
|
|
121
129
|
|
|
122
130
|
it('passes markdown option to sendDirectMessage when --markdown flag is set', async () => {
|
|
@@ -132,10 +140,10 @@ it('calls listMessages with limit and outputs array', async () => {
|
|
|
132
140
|
|
|
133
141
|
expect(mockListMessages).toHaveBeenCalledWith('space_456', { max: 50 })
|
|
134
142
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
135
|
-
const output = consoleLogSpy.mock.calls[0][0]
|
|
136
|
-
expect(output).
|
|
137
|
-
expect(output).
|
|
138
|
-
expect(output).toContain('https://example.com')
|
|
143
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
|
144
|
+
expect(output[0].ref).toBe('msg_123')
|
|
145
|
+
expect(output[1].ref).toBe('msg_124')
|
|
146
|
+
expect(output[0].html).toContain('https://example.com')
|
|
139
147
|
})
|
|
140
148
|
|
|
141
149
|
it('calls getMessage with correct id and outputs result', async () => {
|
|
@@ -143,10 +151,10 @@ it('calls getMessage with correct id and outputs result', async () => {
|
|
|
143
151
|
|
|
144
152
|
expect(mockGetMessage).toHaveBeenCalledWith('msg_123')
|
|
145
153
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
146
|
-
const output = consoleLogSpy.mock.calls[0][0]
|
|
147
|
-
expect(output).
|
|
148
|
-
expect(output).
|
|
149
|
-
expect(output).toContain('https://example.com')
|
|
154
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
|
155
|
+
expect(output.ref).toBe('msg_123')
|
|
156
|
+
expect(output.personEmail).toBe('user@example.com')
|
|
157
|
+
expect(output.html).toContain('https://example.com')
|
|
150
158
|
})
|
|
151
159
|
|
|
152
160
|
it('calls deleteMessage and outputs deleted id when --force flag is set', async () => {
|
|
@@ -178,9 +186,9 @@ it('calls editMessage with roomId in args and outputs result', async () => {
|
|
|
178
186
|
markdown: undefined,
|
|
179
187
|
})
|
|
180
188
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
181
|
-
const output = consoleLogSpy.mock.calls[0][0]
|
|
182
|
-
expect(output).
|
|
183
|
-
expect(output).
|
|
189
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
|
190
|
+
expect(output.ref).toBe('msg_123')
|
|
191
|
+
expect(output.text).toBe('Updated message')
|
|
184
192
|
expect(mockDispose).toHaveBeenCalled()
|
|
185
193
|
})
|
|
186
194
|
|