@jimiford/webex 0.1.1
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/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/channel-plugin.d.ts +18 -0
- package/dist/channel-plugin.js +410 -0
- package/dist/channel.d.ts +98 -0
- package/dist/channel.js +224 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +32 -0
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +23 -0
- package/dist/send.d.ts +92 -0
- package/dist/send.js +304 -0
- package/dist/types.d.ts +223 -0
- package/dist/types.js +6 -0
- package/dist/webhook.d.ts +64 -0
- package/dist/webhook.js +297 -0
- package/openclaw.plugin.json +33 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jimi Ford
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# @jimiford/webex
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for Cisco Webex messaging. Enables your OpenClaw gateway to send and receive messages via Webex bots.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Direct Messages (1:1)**: Send and receive private messages with users
|
|
8
|
+
- **Space/Room Messages**: Communicate in Webex spaces and rooms
|
|
9
|
+
- **Attachments**: Send files via public URLs
|
|
10
|
+
- **Adaptive Cards**: Rich interactive message cards
|
|
11
|
+
- **Threaded Replies**: Support for message threading
|
|
12
|
+
- **Webhook Integration**: Real-time message reception
|
|
13
|
+
- **Automatic Retries**: Configurable retry logic with exponential backoff
|
|
14
|
+
- **Message Normalization**: Converts Webex messages to OpenClaw's envelope format
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @jimiford/webex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
### 1. Create a Webex Bot
|
|
25
|
+
|
|
26
|
+
1. Go to [Webex Developer Portal](https://developer.webex.com)
|
|
27
|
+
2. Sign in with your Webex account
|
|
28
|
+
3. Navigate to **My Webex Apps** → **Create a New App**
|
|
29
|
+
4. Select **Create a Bot**
|
|
30
|
+
5. Fill in the bot details:
|
|
31
|
+
- Bot Name: Your bot's display name
|
|
32
|
+
- Bot Username: Unique identifier (e.g., `mybot@webex.bot`)
|
|
33
|
+
- Icon: Upload or select an icon
|
|
34
|
+
- Description: Brief description of your bot
|
|
35
|
+
6. Click **Add Bot**
|
|
36
|
+
7. **Important**: Copy the **Bot Access Token** - you'll only see it once!
|
|
37
|
+
|
|
38
|
+
### 2. Set Up a Public Webhook URL
|
|
39
|
+
|
|
40
|
+
Your webhook endpoint must be publicly accessible. Options:
|
|
41
|
+
|
|
42
|
+
- **Production**: Deploy to a cloud provider with HTTPS
|
|
43
|
+
- **Development**: Use [ngrok](https://ngrok.com) to expose localhost:
|
|
44
|
+
```bash
|
|
45
|
+
ngrok http 3000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createWebexChannel, WebexChannelConfig } from '@jimiford/webex';
|
|
52
|
+
|
|
53
|
+
const config: WebexChannelConfig = {
|
|
54
|
+
// Required: Your Webex bot access token
|
|
55
|
+
token: 'YOUR_BOT_ACCESS_TOKEN',
|
|
56
|
+
|
|
57
|
+
// Required: Public URL for receiving webhooks
|
|
58
|
+
webhookUrl: 'https://your-domain.com/webhooks/webex',
|
|
59
|
+
|
|
60
|
+
// Required: Policy for handling direct messages
|
|
61
|
+
// - 'allow': Accept DMs from anyone
|
|
62
|
+
// - 'deny': Reject all DMs
|
|
63
|
+
// - 'allowlisted': Only accept from specified users
|
|
64
|
+
dmPolicy: 'allow',
|
|
65
|
+
|
|
66
|
+
// Optional: List of allowed person IDs or emails (when dmPolicy is 'allowlisted')
|
|
67
|
+
allowFrom: ['user@example.com', 'Y2lzY29zcGFyazov...'],
|
|
68
|
+
|
|
69
|
+
// Optional: Secret for webhook signature verification
|
|
70
|
+
webhookSecret: 'your-webhook-secret',
|
|
71
|
+
|
|
72
|
+
// Optional: Custom API base URL (default: https://webexapis.com/v1)
|
|
73
|
+
apiBaseUrl: 'https://webexapis.com/v1',
|
|
74
|
+
|
|
75
|
+
// Optional: Maximum retry attempts (default: 3)
|
|
76
|
+
maxRetries: 3,
|
|
77
|
+
|
|
78
|
+
// Optional: Retry delay in ms (default: 1000)
|
|
79
|
+
retryDelayMs: 1000,
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Basic Setup
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createWebexChannel } from '@jimiford/webex';
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
// Create and initialize the channel
|
|
92
|
+
const channel = createWebexChannel();
|
|
93
|
+
await channel.initialize({
|
|
94
|
+
token: process.env.WEBEX_BOT_TOKEN!,
|
|
95
|
+
webhookUrl: process.env.WEBHOOK_URL!,
|
|
96
|
+
dmPolicy: 'allow',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Register webhooks with Webex
|
|
100
|
+
await channel.registerWebhooks();
|
|
101
|
+
|
|
102
|
+
// Register a message handler
|
|
103
|
+
channel.onMessage(async (envelope) => {
|
|
104
|
+
console.log('Received message:', envelope);
|
|
105
|
+
|
|
106
|
+
// Echo the message back
|
|
107
|
+
await channel.send({
|
|
108
|
+
to: envelope.conversationId,
|
|
109
|
+
content: { text: `You said: ${envelope.content.text}` },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log('Webex channel ready!');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch(console.error);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Sending Messages
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Send to a room
|
|
123
|
+
await channel.sendText('roomId', 'Hello, room!');
|
|
124
|
+
|
|
125
|
+
// Send markdown
|
|
126
|
+
await channel.sendMarkdown('roomId', '**Bold** and _italic_');
|
|
127
|
+
|
|
128
|
+
// Send direct message
|
|
129
|
+
await channel.sendDirect('user@example.com', 'Hello!');
|
|
130
|
+
|
|
131
|
+
// Reply in a thread
|
|
132
|
+
await channel.reply('roomId', 'parentMessageId', 'This is a reply');
|
|
133
|
+
|
|
134
|
+
// Send with full options
|
|
135
|
+
await channel.send({
|
|
136
|
+
to: 'roomId',
|
|
137
|
+
content: {
|
|
138
|
+
text: 'Plain text fallback',
|
|
139
|
+
markdown: '**Rich** content',
|
|
140
|
+
files: ['https://example.com/image.png'],
|
|
141
|
+
},
|
|
142
|
+
parentId: 'threadParentId',
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Handling Webhooks
|
|
147
|
+
|
|
148
|
+
Set up an HTTP endpoint to receive webhooks:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import express from 'express';
|
|
152
|
+
import { createWebexChannel } from '@jimiford/webex';
|
|
153
|
+
|
|
154
|
+
const app = express();
|
|
155
|
+
app.use(express.json());
|
|
156
|
+
|
|
157
|
+
const channel = createWebexChannel();
|
|
158
|
+
|
|
159
|
+
// Initialize channel (do this on startup)
|
|
160
|
+
await channel.initialize({
|
|
161
|
+
token: process.env.WEBEX_BOT_TOKEN!,
|
|
162
|
+
webhookUrl: 'https://your-domain.com/webhooks/webex',
|
|
163
|
+
dmPolicy: 'allow',
|
|
164
|
+
webhookSecret: process.env.WEBHOOK_SECRET,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Webhook endpoint
|
|
168
|
+
app.post('/webhooks/webex', async (req, res) => {
|
|
169
|
+
try {
|
|
170
|
+
const signature = req.headers['x-spark-signature'] as string;
|
|
171
|
+
const envelope = await channel.handleWebhook(req.body, signature);
|
|
172
|
+
|
|
173
|
+
if (envelope) {
|
|
174
|
+
// Process the message
|
|
175
|
+
console.log('Message from:', envelope.author.email);
|
|
176
|
+
console.log('Content:', envelope.content.text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.status(200).send('OK');
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Webhook error:', error);
|
|
182
|
+
res.status(500).send('Error');
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
app.listen(3000);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### OpenClaw Envelope Format
|
|
190
|
+
|
|
191
|
+
Incoming messages are normalized to this format:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface OpenClawEnvelope {
|
|
195
|
+
id: string; // Webex message ID
|
|
196
|
+
channel: 'webex'; // Channel identifier
|
|
197
|
+
conversationId: string; // Room ID
|
|
198
|
+
author: {
|
|
199
|
+
id: string; // Person ID
|
|
200
|
+
email?: string; // Email address
|
|
201
|
+
displayName?: string; // Display name
|
|
202
|
+
isBot: boolean; // Always false (bot messages filtered)
|
|
203
|
+
};
|
|
204
|
+
content: {
|
|
205
|
+
text?: string; // Plain text content
|
|
206
|
+
markdown?: string; // Markdown content
|
|
207
|
+
attachments?: Array<{
|
|
208
|
+
type: 'file' | 'card';
|
|
209
|
+
url?: string; // File URL
|
|
210
|
+
content?: unknown; // Card content
|
|
211
|
+
}>;
|
|
212
|
+
};
|
|
213
|
+
metadata: {
|
|
214
|
+
roomType: 'direct' | 'group';
|
|
215
|
+
roomId: string;
|
|
216
|
+
timestamp: string; // ISO 8601
|
|
217
|
+
mentions?: string[]; // Mentioned person IDs
|
|
218
|
+
parentId?: string; // Thread parent message ID
|
|
219
|
+
raw: WebexMessage; // Original Webex message
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Advanced Usage
|
|
225
|
+
|
|
226
|
+
### Direct Sender Access
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const sender = channel.getSender();
|
|
230
|
+
|
|
231
|
+
// Get message details
|
|
232
|
+
const message = await sender.getMessage('messageId');
|
|
233
|
+
|
|
234
|
+
// Delete a message
|
|
235
|
+
await sender.deleteMessage('messageId');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Direct Webhook Handler Access
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const webhookHandler = channel.getWebhookHandler();
|
|
242
|
+
|
|
243
|
+
// List existing webhooks
|
|
244
|
+
const webhooks = await webhookHandler.listWebhooks();
|
|
245
|
+
|
|
246
|
+
// Delete a webhook
|
|
247
|
+
await webhookHandler.deleteWebhook('webhookId');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Error Handling
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { WebexApiRequestError } from '@jimiford/webex';
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await channel.send({ to: 'invalid', content: { text: 'test' } });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof WebexApiRequestError) {
|
|
259
|
+
console.error('API Error:', error.message);
|
|
260
|
+
console.error('Status:', error.statusCode);
|
|
261
|
+
console.error('Tracking ID:', error.trackingId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## API Reference
|
|
267
|
+
|
|
268
|
+
### WebexChannel
|
|
269
|
+
|
|
270
|
+
| Method | Description |
|
|
271
|
+
|--------|-------------|
|
|
272
|
+
| `initialize(config)` | Initialize with configuration |
|
|
273
|
+
| `send(message)` | Send a message |
|
|
274
|
+
| `sendText(roomId, text)` | Send plain text to a room |
|
|
275
|
+
| `sendMarkdown(roomId, md)` | Send markdown to a room |
|
|
276
|
+
| `sendDirect(to, text)` | Send direct message |
|
|
277
|
+
| `reply(roomId, parentId, text)` | Send threaded reply |
|
|
278
|
+
| `handleWebhook(payload, sig?)` | Process incoming webhook |
|
|
279
|
+
| `onMessage(handler)` | Register message handler |
|
|
280
|
+
| `offMessage(handler)` | Remove message handler |
|
|
281
|
+
| `registerWebhooks()` | Register webhooks with Webex |
|
|
282
|
+
| `shutdown()` | Cleanup and shutdown |
|
|
283
|
+
|
|
284
|
+
## Environment Variables
|
|
285
|
+
|
|
286
|
+
Recommended environment variables:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
WEBEX_BOT_TOKEN=your_bot_access_token
|
|
290
|
+
WEBHOOK_URL=https://your-domain.com/webhooks/webex
|
|
291
|
+
WEBHOOK_SECRET=your_webhook_secret
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Troubleshooting
|
|
295
|
+
|
|
296
|
+
### Bot not receiving messages
|
|
297
|
+
|
|
298
|
+
1. Ensure webhooks are registered: `await channel.registerWebhooks()`
|
|
299
|
+
2. Verify your webhook URL is publicly accessible
|
|
300
|
+
3. Check that the bot is added to the room/space
|
|
301
|
+
4. For DMs, the user must message the bot first
|
|
302
|
+
|
|
303
|
+
### "Invalid webhook signature" errors
|
|
304
|
+
|
|
305
|
+
1. Ensure `webhookSecret` matches the secret used when creating webhooks
|
|
306
|
+
2. Verify the signature header name: `x-spark-signature`
|
|
307
|
+
|
|
308
|
+
### Rate limiting
|
|
309
|
+
|
|
310
|
+
The plugin includes automatic retry with exponential backoff for rate-limited requests. Adjust `maxRetries` and `retryDelayMs` in config if needed.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Channel Plugin for Webex
|
|
3
|
+
*
|
|
4
|
+
* Implements the ChannelPlugin interface for OpenClaw's plugin system.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { WebexChannelConfig } from "./types";
|
|
8
|
+
/** Resolved account configuration */
|
|
9
|
+
export interface ResolvedWebexAccount {
|
|
10
|
+
accountId: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
configured: boolean;
|
|
14
|
+
config: WebexChannelConfig;
|
|
15
|
+
token?: string;
|
|
16
|
+
webhookUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const webexPlugin: ChannelPlugin<ResolvedWebexAccount>;
|