agent-messenger 2.20.5 → 2.22.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/README.md +8 -5
- package/dist/package.json +9 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +19 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +81 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webexbot/cli.d.ts +5 -0
- package/dist/src/platforms/webexbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/cli.js +33 -0
- package/dist/src/platforms/webexbot/cli.js.map +1 -0
- package/dist/src/platforms/webexbot/client.d.ts +61 -0
- package/dist/src/platforms/webexbot/client.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/client.js +80 -0
- package/dist/src/platforms/webexbot/client.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/auth.d.ts +28 -0
- package/dist/src/platforms/webexbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/auth.js +166 -0
- package/dist/src/platforms/webexbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts +22 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/file.js +64 -0
- package/dist/src/platforms/webexbot/commands/file.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts +10 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/index.js +10 -0
- package/dist/src/platforms/webexbot/commands/index.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/listen.d.ts +12 -0
- package/dist/src/platforms/webexbot/commands/listen.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/listen.js +85 -0
- package/dist/src/platforms/webexbot/commands/listen.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/member.d.ts +19 -0
- package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/member.js +33 -0
- package/dist/src/platforms/webexbot/commands/member.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts +44 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/message.js +193 -0
- package/dist/src/platforms/webexbot/commands/message.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/webexbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/shared.js +13 -0
- package/dist/src/platforms/webexbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts +24 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js +37 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/space.d.ts +28 -0
- package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/space.js +61 -0
- package/dist/src/platforms/webexbot/commands/space.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts +30 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.js +66 -0
- package/dist/src/platforms/webexbot/commands/user.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/whoami.d.ts +16 -0
- package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/whoami.js +29 -0
- package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -0
- package/dist/src/platforms/webexbot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/credential-manager.js +120 -0
- package/dist/src/platforms/webexbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/webexbot/index.d.ts +9 -0
- package/dist/src/platforms/webexbot/index.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/index.js +6 -0
- package/dist/src/platforms/webexbot/index.js.map +1 -0
- package/dist/src/platforms/webexbot/listener.d.ts +44 -0
- package/dist/src/platforms/webexbot/listener.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/listener.js +214 -0
- package/dist/src/platforms/webexbot/listener.js.map +1 -0
- package/dist/src/platforms/webexbot/types.d.ts +60 -0
- package/dist/src/platforms/webexbot/types.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/types.js +28 -0
- package/dist/src/platforms/webexbot/types.js.map +1 -0
- package/dist/src/platforms/webexbot/wdm-discovery.d.ts +4 -0
- package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/wdm-discovery.js +36 -0
- package/dist/src/platforms/webexbot/wdm-discovery.js.map +1 -0
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/webexbot.mdx +292 -0
- package/docs/content/docs/sdk/meta.json +1 -0
- package/docs/content/docs/sdk/webexbot.mdx +342 -0
- package/docs/src/app/page.tsx +115 -19
- package/package.json +9 -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 +1 -1
- package/skills/agent-webexbot/SKILL.md +414 -0
- package/skills/agent-webexbot/references/authentication.md +225 -0
- package/skills/agent-webexbot/references/common-patterns.md +708 -0
- 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/cli.ts +4 -0
- package/src/platforms/webex/client.test.ts +10 -0
- package/src/platforms/webex/client.ts +97 -3
- package/src/platforms/webex/typings/webex-message-handler.d.ts +360 -29
- package/src/platforms/webexbot/cli.ts +48 -0
- package/src/platforms/webexbot/client.test.ts +198 -0
- package/src/platforms/webexbot/client.ts +113 -0
- package/src/platforms/webexbot/commands/auth.test.ts +185 -0
- package/src/platforms/webexbot/commands/auth.ts +210 -0
- package/src/platforms/webexbot/commands/file.ts +104 -0
- package/src/platforms/webexbot/commands/index.ts +9 -0
- package/src/platforms/webexbot/commands/listen.test.ts +20 -0
- package/src/platforms/webexbot/commands/listen.ts +104 -0
- package/src/platforms/webexbot/commands/member.ts +51 -0
- package/src/platforms/webexbot/commands/message.ts +263 -0
- package/src/platforms/webexbot/commands/shared.ts +22 -0
- package/src/platforms/webexbot/commands/snapshot.ts +60 -0
- package/src/platforms/webexbot/commands/space.ts +88 -0
- package/src/platforms/webexbot/commands/user.test.ts +77 -0
- package/src/platforms/webexbot/commands/user.ts +98 -0
- package/src/platforms/webexbot/commands/whoami.ts +43 -0
- package/src/platforms/webexbot/credential-manager.test.ts +182 -0
- package/src/platforms/webexbot/credential-manager.ts +149 -0
- package/src/platforms/webexbot/index.ts +8 -0
- package/src/platforms/webexbot/listener.test.ts +234 -0
- package/src/platforms/webexbot/listener.ts +255 -0
- package/src/platforms/webexbot/types.test.ts +87 -0
- package/src/platforms/webexbot/types.ts +72 -0
- package/src/platforms/webexbot/wdm-discovery.test.ts +97 -0
- package/src/platforms/webexbot/wdm-discovery.ts +43 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
3
|
+
import { mkdir, stat } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { WebexBotCredentialManager } from './credential-manager'
|
|
8
|
+
|
|
9
|
+
const CREDS_A = {
|
|
10
|
+
token: 'bot-token-a',
|
|
11
|
+
bot_id: 'bot-123',
|
|
12
|
+
bot_name: 'Bot A',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CREDS_B = {
|
|
16
|
+
token: 'bot-token-b',
|
|
17
|
+
bot_id: 'bot-456',
|
|
18
|
+
bot_name: 'Bot B',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('WebexBotCredentialManager', () => {
|
|
22
|
+
let tempDir: string
|
|
23
|
+
let manager: WebexBotCredentialManager
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
tempDir = join(tmpdir(), `webexbot-cred-test-${Date.now()}`)
|
|
27
|
+
await mkdir(tempDir, { recursive: true })
|
|
28
|
+
manager = new WebexBotCredentialManager(tempDir)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (existsSync(tempDir)) {
|
|
33
|
+
rmSync(tempDir, { recursive: true })
|
|
34
|
+
}
|
|
35
|
+
delete process.env.E2E_WEBEXBOT_TOKEN
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns empty config when no file exists', async () => {
|
|
39
|
+
const config = await manager.load()
|
|
40
|
+
|
|
41
|
+
expect(config.current).toBeNull()
|
|
42
|
+
expect(config.bots).toEqual({})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('persists config to file', async () => {
|
|
46
|
+
const config = {
|
|
47
|
+
current: { bot_id: 'bot-123' },
|
|
48
|
+
bots: {
|
|
49
|
+
'bot-123': { bot_id: 'bot-123', bot_name: 'Test Bot', token: 'test-token' },
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await manager.save(config)
|
|
54
|
+
const loaded = await manager.load()
|
|
55
|
+
|
|
56
|
+
expect(loaded).toEqual(config)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns null when no credentials exist', async () => {
|
|
60
|
+
expect(await manager.getCredentials()).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns current bot credentials', async () => {
|
|
64
|
+
await manager.setCredentials(CREDS_A)
|
|
65
|
+
|
|
66
|
+
const creds = await manager.getCredentials()
|
|
67
|
+
|
|
68
|
+
expect(creds).toEqual(CREDS_A)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns specific bot by id', async () => {
|
|
72
|
+
await manager.setCredentials(CREDS_A)
|
|
73
|
+
await manager.setCredentials(CREDS_B)
|
|
74
|
+
|
|
75
|
+
const creds = await manager.getCredentials('bot-123')
|
|
76
|
+
|
|
77
|
+
expect(creds).toEqual(CREDS_A)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns null for non-existent bot id', async () => {
|
|
81
|
+
await manager.setCredentials(CREDS_A)
|
|
82
|
+
|
|
83
|
+
const creds = await manager.getCredentials('nonexistent')
|
|
84
|
+
|
|
85
|
+
expect(creds).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('env var takes precedence when no botId specified', async () => {
|
|
89
|
+
await manager.setCredentials(CREDS_A)
|
|
90
|
+
|
|
91
|
+
process.env.E2E_WEBEXBOT_TOKEN = 'env-token'
|
|
92
|
+
|
|
93
|
+
const creds = await manager.getCredentials()
|
|
94
|
+
|
|
95
|
+
expect(creds).toEqual({ token: 'env-token', bot_id: 'env', bot_name: 'env' })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('env var is ignored when botId explicitly provided', async () => {
|
|
99
|
+
await manager.setCredentials(CREDS_A)
|
|
100
|
+
|
|
101
|
+
process.env.E2E_WEBEXBOT_TOKEN = 'env-token'
|
|
102
|
+
|
|
103
|
+
const creds = await manager.getCredentials('bot-123')
|
|
104
|
+
|
|
105
|
+
expect(creds).toEqual(CREDS_A)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('stores multiple bots and sets latest as current', async () => {
|
|
109
|
+
await manager.setCredentials(CREDS_A)
|
|
110
|
+
await manager.setCredentials(CREDS_B)
|
|
111
|
+
|
|
112
|
+
const config = await manager.load()
|
|
113
|
+
expect(Object.keys(config.bots)).toEqual(['bot-123', 'bot-456'])
|
|
114
|
+
expect(config.current).toEqual({ bot_id: 'bot-456' })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('returns all bots with current flag', async () => {
|
|
118
|
+
await manager.setCredentials(CREDS_A)
|
|
119
|
+
await manager.setCredentials(CREDS_B)
|
|
120
|
+
|
|
121
|
+
const all = await manager.listAll()
|
|
122
|
+
|
|
123
|
+
expect(all).toHaveLength(2)
|
|
124
|
+
expect(all.find((b) => b.bot_id === 'bot-123')?.is_current).toBe(false)
|
|
125
|
+
expect(all.find((b) => b.bot_id === 'bot-456')?.is_current).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('switches current bot', async () => {
|
|
129
|
+
await manager.setCredentials(CREDS_A)
|
|
130
|
+
await manager.setCredentials(CREDS_B)
|
|
131
|
+
|
|
132
|
+
const switched = await manager.setCurrent('bot-123')
|
|
133
|
+
|
|
134
|
+
expect(switched).toBe(true)
|
|
135
|
+
const creds = await manager.getCredentials()
|
|
136
|
+
expect(creds?.bot_id).toBe('bot-123')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('returns false when switching to unknown bot', async () => {
|
|
140
|
+
expect(await manager.setCurrent('nonexistent')).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('removes a bot by id', async () => {
|
|
144
|
+
await manager.setCredentials(CREDS_A)
|
|
145
|
+
await manager.setCredentials(CREDS_B)
|
|
146
|
+
|
|
147
|
+
const removed = await manager.removeBot('bot-123')
|
|
148
|
+
|
|
149
|
+
expect(removed).toBe(true)
|
|
150
|
+
const config = await manager.load()
|
|
151
|
+
expect(Object.keys(config.bots)).toEqual(['bot-456'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('clears current when current bot is removed', async () => {
|
|
155
|
+
await manager.setCredentials(CREDS_A)
|
|
156
|
+
|
|
157
|
+
await manager.removeBot('bot-123')
|
|
158
|
+
|
|
159
|
+
const config = await manager.load()
|
|
160
|
+
expect(config.current).toBeNull()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('clears all credentials', async () => {
|
|
164
|
+
await manager.setCredentials(CREDS_A)
|
|
165
|
+
await manager.setCredentials(CREDS_B)
|
|
166
|
+
|
|
167
|
+
await manager.clearCredentials()
|
|
168
|
+
|
|
169
|
+
const config = await manager.load()
|
|
170
|
+
expect(config.current).toBeNull()
|
|
171
|
+
expect(config.bots).toEqual({})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('saves file with secure permissions', async () => {
|
|
175
|
+
await manager.setCredentials(CREDS_A)
|
|
176
|
+
|
|
177
|
+
const credPath = join(tempDir, 'webexbot-credentials.json')
|
|
178
|
+
const stats = await stat(credPath)
|
|
179
|
+
|
|
180
|
+
expect(stats.mode & 0o777).toBe(0o600)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { getConfigDir } from '../../shared/utils/config-dir'
|
|
6
|
+
import type { WebexBotConfig, WebexBotCredentials } from './types'
|
|
7
|
+
import { WebexBotConfigSchema } from './types'
|
|
8
|
+
|
|
9
|
+
export class WebexBotCredentialManager {
|
|
10
|
+
private configDir: string
|
|
11
|
+
private credentialsPath: string
|
|
12
|
+
|
|
13
|
+
constructor(configDir?: string) {
|
|
14
|
+
this.configDir = configDir ?? getConfigDir()
|
|
15
|
+
this.credentialsPath = join(this.configDir, 'webexbot-credentials.json')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async load(): Promise<WebexBotConfig> {
|
|
19
|
+
if (!existsSync(this.credentialsPath)) {
|
|
20
|
+
return { current: null, bots: {} }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = await readFile(this.credentialsPath, 'utf-8')
|
|
24
|
+
|
|
25
|
+
let json: unknown
|
|
26
|
+
try {
|
|
27
|
+
json = JSON.parse(content)
|
|
28
|
+
} catch {
|
|
29
|
+
return { current: null, bots: {} }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parsed = WebexBotConfigSchema.safeParse(json)
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
return { current: null, bots: {} }
|
|
35
|
+
}
|
|
36
|
+
return parsed.data
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async save(config: WebexBotConfig): Promise<void> {
|
|
40
|
+
await mkdir(this.configDir, { recursive: true })
|
|
41
|
+
await writeFile(this.credentialsPath, JSON.stringify(config, null, 2), { mode: 0o600 })
|
|
42
|
+
await chmod(this.credentialsPath, 0o600)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getCredentials(botId?: string): Promise<WebexBotCredentials | null> {
|
|
46
|
+
const envToken = process.env.E2E_WEBEXBOT_TOKEN
|
|
47
|
+
|
|
48
|
+
if (envToken && !botId) {
|
|
49
|
+
return {
|
|
50
|
+
token: envToken,
|
|
51
|
+
bot_id: 'env',
|
|
52
|
+
bot_name: 'env',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const config = await this.load()
|
|
57
|
+
|
|
58
|
+
if (botId) {
|
|
59
|
+
const bot = config.bots[botId]
|
|
60
|
+
if (!bot) return null
|
|
61
|
+
return {
|
|
62
|
+
token: bot.token,
|
|
63
|
+
bot_id: bot.bot_id,
|
|
64
|
+
bot_name: bot.bot_name,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!config.current) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bot = config.bots[config.current.bot_id]
|
|
73
|
+
if (!bot) return null
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
token: bot.token,
|
|
77
|
+
bot_id: bot.bot_id,
|
|
78
|
+
bot_name: bot.bot_name,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async setCredentials(creds: WebexBotCredentials): Promise<void> {
|
|
83
|
+
const config = await this.load()
|
|
84
|
+
|
|
85
|
+
config.bots[creds.bot_id] = {
|
|
86
|
+
bot_id: creds.bot_id,
|
|
87
|
+
bot_name: creds.bot_name,
|
|
88
|
+
token: creds.token,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
config.current = {
|
|
92
|
+
bot_id: creds.bot_id,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await this.save(config)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async removeBot(botId: string): Promise<boolean> {
|
|
99
|
+
const config = await this.load()
|
|
100
|
+
|
|
101
|
+
if (!config.bots[botId]) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
delete config.bots[botId]
|
|
106
|
+
|
|
107
|
+
if (config.current?.bot_id === botId) {
|
|
108
|
+
config.current = null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await this.save(config)
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async setCurrent(botId: string): Promise<boolean> {
|
|
116
|
+
const config = await this.load()
|
|
117
|
+
|
|
118
|
+
if (!config.bots[botId]) {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
config.current = {
|
|
123
|
+
bot_id: botId,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.save(config)
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async listAll(): Promise<Array<WebexBotCredentials & { is_current: boolean }>> {
|
|
131
|
+
const config = await this.load()
|
|
132
|
+
const results: Array<WebexBotCredentials & { is_current: boolean }> = []
|
|
133
|
+
|
|
134
|
+
for (const bot of Object.values(config.bots)) {
|
|
135
|
+
results.push({
|
|
136
|
+
token: bot.token,
|
|
137
|
+
bot_id: bot.bot_id,
|
|
138
|
+
bot_name: bot.bot_name,
|
|
139
|
+
is_current: config.current?.bot_id === bot.bot_id,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return results
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async clearCredentials(): Promise<void> {
|
|
147
|
+
await this.save({ current: null, bots: {} })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { WebexBotClient } from './client'
|
|
2
|
+
export { WebexBotCredentialManager } from './credential-manager'
|
|
3
|
+
export { WebexBotListener } from './listener'
|
|
4
|
+
export type { WebexBotListenerOptions } from './listener'
|
|
5
|
+
export type { WebexBotConfig, WebexBotCredentials, WebexBotEntry, WebexBotListenerEventMap } from './types'
|
|
6
|
+
export { WebexBotConfigSchema, WebexBotCredentialsSchema, WebexBotEntrySchema, WebexBotError } from './types'
|
|
7
|
+
export type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from '../webex/types'
|
|
8
|
+
export { WebexMembershipSchema, WebexMessageSchema, WebexPersonSchema, WebexSpaceSchema } from '../webex/types'
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
DecryptedMessage,
|
|
6
|
+
HandlerStatus,
|
|
7
|
+
MercuryActivity,
|
|
8
|
+
WebexMessageHandlerConfig,
|
|
9
|
+
WebexMessageHandlerEvents,
|
|
10
|
+
} from 'webex-message-handler'
|
|
11
|
+
|
|
12
|
+
import { WebexBotListener } from './listener'
|
|
13
|
+
|
|
14
|
+
const STATUS: HandlerStatus = {
|
|
15
|
+
status: 'connected',
|
|
16
|
+
webSocketOpen: true,
|
|
17
|
+
kmsInitialized: true,
|
|
18
|
+
deviceRegistered: true,
|
|
19
|
+
reconnectAttempt: 0,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RAW_ACTIVITY: MercuryActivity = {
|
|
23
|
+
id: 'activity-123',
|
|
24
|
+
verb: 'post',
|
|
25
|
+
actor: { id: 'person-123', objectType: 'person', emailAddress: 'user@example.com' },
|
|
26
|
+
object: { id: 'object-123', objectType: 'comment', displayName: 'hello' },
|
|
27
|
+
target: { id: 'room-123', objectType: 'conversation' },
|
|
28
|
+
published: '2024-01-01T00:00:00Z',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MESSAGE: DecryptedMessage = {
|
|
32
|
+
id: 'message-123',
|
|
33
|
+
roomId: 'room-123',
|
|
34
|
+
personId: 'person-123',
|
|
35
|
+
personEmail: 'user@example.com',
|
|
36
|
+
text: 'hello',
|
|
37
|
+
created: '2024-01-01T00:00:00Z',
|
|
38
|
+
mentionedPeople: [],
|
|
39
|
+
mentionedGroups: [],
|
|
40
|
+
files: [],
|
|
41
|
+
raw: RAW_ACTIVITY,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class FakeWebexMessageHandler extends EventEmitter {
|
|
45
|
+
connect = mock(() => Promise.resolve())
|
|
46
|
+
disconnect = mock(() => Promise.resolve())
|
|
47
|
+
connected = true
|
|
48
|
+
|
|
49
|
+
status(): HandlerStatus {
|
|
50
|
+
return STATUS
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override on<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
|
|
54
|
+
return super.on(event, listener)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override off<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
|
|
58
|
+
return super.off(event, listener)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override once<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
|
|
62
|
+
return super.once(event, listener)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('WebexBotListener', () => {
|
|
67
|
+
it('bridges handler message events and webex_event', async () => {
|
|
68
|
+
const handler = new FakeWebexMessageHandler()
|
|
69
|
+
const client = { getToken: () => 'token123' }
|
|
70
|
+
const listener = new WebexBotListener(client, {
|
|
71
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
72
|
+
})
|
|
73
|
+
const messageCreated = mock((_event: DecryptedMessage) => undefined)
|
|
74
|
+
const webexEvent = mock((_event: DecryptedMessage) => undefined)
|
|
75
|
+
listener.on('message_created', messageCreated)
|
|
76
|
+
listener.on('webex_event', webexEvent)
|
|
77
|
+
|
|
78
|
+
await listener.start()
|
|
79
|
+
handler.emit('message:created', MESSAGE)
|
|
80
|
+
|
|
81
|
+
expect(messageCreated).toHaveBeenCalledWith(MESSAGE)
|
|
82
|
+
expect(webexEvent).toHaveBeenCalledWith(MESSAGE)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('stop calls handler disconnect', async () => {
|
|
86
|
+
const handler = new FakeWebexMessageHandler()
|
|
87
|
+
const client = { getToken: () => 'token123' }
|
|
88
|
+
const listener = new WebexBotListener(client, {
|
|
89
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await listener.start()
|
|
93
|
+
await listener.stop()
|
|
94
|
+
|
|
95
|
+
expect(handler.disconnect).toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('start is idempotent', async () => {
|
|
99
|
+
const handler = new FakeWebexMessageHandler()
|
|
100
|
+
const client = { getToken: () => 'token123' }
|
|
101
|
+
const listener = new WebexBotListener(client, {
|
|
102
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await listener.start()
|
|
106
|
+
await listener.start()
|
|
107
|
+
|
|
108
|
+
expect(handler.connect).toHaveBeenCalledTimes(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('start rethrows and resets state when connect fails, allowing retry', async () => {
|
|
112
|
+
const failing = new FakeWebexMessageHandler()
|
|
113
|
+
failing.connect = mock(() => Promise.reject(new Error('device registration failed')))
|
|
114
|
+
const ok = new FakeWebexMessageHandler()
|
|
115
|
+
const handlers = [failing, ok]
|
|
116
|
+
const client = { getToken: () => 'token123' }
|
|
117
|
+
const listener = new WebexBotListener(client, {
|
|
118
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handlers.shift()!,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
await expect(listener.start()).rejects.toThrow('device registration failed')
|
|
122
|
+
expect(failing.disconnect).toHaveBeenCalled()
|
|
123
|
+
|
|
124
|
+
await listener.start()
|
|
125
|
+
expect(ok.connect).toHaveBeenCalledTimes(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('does not throw when handler emits error with no error listener', async () => {
|
|
129
|
+
const handler = new FakeWebexMessageHandler()
|
|
130
|
+
const client = { getToken: () => 'token123' }
|
|
131
|
+
const listener = new WebexBotListener(client, {
|
|
132
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
133
|
+
})
|
|
134
|
+
listener.on('message_created', () => undefined)
|
|
135
|
+
|
|
136
|
+
await listener.start()
|
|
137
|
+
|
|
138
|
+
expect(() => handler.emit('error', new Error('boom'))).not.toThrow()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('ignores stale handler events after stop', async () => {
|
|
142
|
+
const handler = new FakeWebexMessageHandler()
|
|
143
|
+
const client = { getToken: () => 'token123' }
|
|
144
|
+
const listener = new WebexBotListener(client, {
|
|
145
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
146
|
+
})
|
|
147
|
+
const messageCreated = mock((_event: DecryptedMessage) => undefined)
|
|
148
|
+
listener.on('message_created', messageCreated)
|
|
149
|
+
|
|
150
|
+
await listener.start()
|
|
151
|
+
await listener.stop()
|
|
152
|
+
handler.emit('message:created', MESSAGE)
|
|
153
|
+
|
|
154
|
+
expect(messageCreated).not.toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('start-stop-start does not cross-talk between handlers', async () => {
|
|
158
|
+
const first = new FakeWebexMessageHandler()
|
|
159
|
+
const second = new FakeWebexMessageHandler()
|
|
160
|
+
const handlers = [first, second]
|
|
161
|
+
const client = { getToken: () => 'token123' }
|
|
162
|
+
const listener = new WebexBotListener(client, {
|
|
163
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handlers.shift()!,
|
|
164
|
+
})
|
|
165
|
+
const messageCreated = mock((_event: DecryptedMessage) => undefined)
|
|
166
|
+
listener.on('message_created', messageCreated)
|
|
167
|
+
|
|
168
|
+
await listener.start()
|
|
169
|
+
await listener.stop()
|
|
170
|
+
await listener.start()
|
|
171
|
+
|
|
172
|
+
first.emit('message:created', MESSAGE)
|
|
173
|
+
expect(messageCreated).not.toHaveBeenCalled()
|
|
174
|
+
|
|
175
|
+
second.emit('message:created', MESSAGE)
|
|
176
|
+
expect(messageCreated).toHaveBeenCalledTimes(1)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('preserves disconnected reason', async () => {
|
|
180
|
+
const handler = new FakeWebexMessageHandler()
|
|
181
|
+
const client = { getToken: () => 'token123' }
|
|
182
|
+
const listener = new WebexBotListener(client, {
|
|
183
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
184
|
+
})
|
|
185
|
+
const disconnected = mock((_reason: string) => undefined)
|
|
186
|
+
listener.on('disconnected', disconnected)
|
|
187
|
+
|
|
188
|
+
await listener.start()
|
|
189
|
+
handler.emit('disconnected', 'network lost')
|
|
190
|
+
|
|
191
|
+
expect(disconnected).toHaveBeenCalledWith('network lost')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('concurrent start() calls share the same connect failure', async () => {
|
|
195
|
+
const handler = new FakeWebexMessageHandler()
|
|
196
|
+
handler.connect = mock(() => Promise.reject(new Error('connect failed')))
|
|
197
|
+
const client = { getToken: () => 'token123' }
|
|
198
|
+
const listener = new WebexBotListener(client, {
|
|
199
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const first = listener.start()
|
|
203
|
+
const second = listener.start()
|
|
204
|
+
const firstResult = first.then(
|
|
205
|
+
() => 'ok',
|
|
206
|
+
(e: Error) => e.message,
|
|
207
|
+
)
|
|
208
|
+
const secondResult = second.then(
|
|
209
|
+
() => 'ok',
|
|
210
|
+
(e: Error) => e.message,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
expect(await firstResult).toBe('connect failed')
|
|
214
|
+
expect(await secondResult).toBe('connect failed')
|
|
215
|
+
expect(handler.connect).toHaveBeenCalledTimes(1)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('disconnects a handler whose connect resolves after stop', async () => {
|
|
219
|
+
const handler = new FakeWebexMessageHandler()
|
|
220
|
+
let resolveConnect: () => void = () => undefined
|
|
221
|
+
handler.connect = mock(() => new Promise<void>((resolve) => (resolveConnect = resolve)))
|
|
222
|
+
const client = { getToken: () => 'token123' }
|
|
223
|
+
const listener = new WebexBotListener(client, {
|
|
224
|
+
_handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const starting = listener.start()
|
|
228
|
+
const stopping = listener.stop()
|
|
229
|
+
resolveConnect()
|
|
230
|
+
await Promise.all([starting, stopping])
|
|
231
|
+
|
|
232
|
+
expect(handler.disconnect).toHaveBeenCalled()
|
|
233
|
+
})
|
|
234
|
+
})
|