openclaw-groupme 0.4.4 → 0.5.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/README.md +147 -45
- package/channel-plugin-api.ts +3 -0
- package/dist/channel-plugin-api.js +3 -0
- package/dist/index.js +15 -9
- package/dist/runtime-setter-api.js +1 -0
- package/dist/secret-contract-api.js +1 -0
- package/dist/setup-entry.js +16 -0
- package/dist/setup-plugin-api.js +3 -0
- package/dist/src/accounts.js +24 -48
- package/dist/src/channel.js +63 -29
- package/dist/src/config-schema.js +10 -11
- package/dist/src/groupme-api.js +9 -5
- package/dist/src/inbound.js +18 -10
- package/dist/src/monitor.js +25 -27
- package/dist/src/normalize.js +6 -0
- package/dist/src/onboarding.js +364 -337
- package/dist/src/parse.js +4 -14
- package/dist/src/policy.js +1 -1
- package/dist/src/rate-limit.js +12 -7
- package/dist/src/replay-cache.js +0 -3
- package/dist/src/secret-contract.js +49 -0
- package/dist/src/security.js +17 -34
- package/dist/src/send.js +19 -13
- package/index.ts +15 -10
- package/openclaw.plugin.json +14 -15
- package/package.json +43 -9
- package/runtime-setter-api.ts +1 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +17 -0
- package/setup-plugin-api.ts +3 -0
- package/src/accounts.ts +29 -68
- package/src/channel.ts +74 -64
- package/src/config-schema.ts +10 -11
- package/src/groupme-api.ts +21 -5
- package/src/history.ts +1 -1
- package/src/inbound.ts +45 -75
- package/src/monitor.ts +37 -52
- package/src/normalize.ts +7 -1
- package/src/onboarding.ts +449 -409
- package/src/parse.ts +6 -23
- package/src/policy.ts +1 -4
- package/src/rate-limit.ts +15 -12
- package/src/replay-cache.ts +1 -4
- package/src/runtime.ts +1 -1
- package/src/secret-contract.ts +66 -0
- package/src/security.ts +28 -66
- package/src/send.ts +32 -38
- package/src/types.ts +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# openclaw-groupme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/openclaw-groupme)
|
|
4
|
+
[](https://www.npmjs.com/package/openclaw-groupme)
|
|
5
|
+
[](https://github.com/oddrationale/openclaw-groupme/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/oddrationale/openclaw-groupme/actions/workflows/codeql.yml)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://github.com/openclaw/openclaw)
|
|
10
|
+
|
|
11
|
+
An [OpenClaw](https://github.com/openclaw/openclaw) channel plugin that brings your AI agent into GroupMe group chats. It hooks into GroupMe's Bot API via webhooks so your agent can receive messages, understand context, and reply — all within the group conversations your team (or friends) are already having. Group chats only; DMs are not supported by the GroupMe Bot API.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- **OpenClaw** `>= 2026.6.1` — this plugin targets the 2026.6.1 plugin SDK.
|
|
16
|
+
- **Node.js** `>= 22.19.0` — matches OpenClaw's runtime.
|
|
17
|
+
- A reachable, public **HTTPS** endpoint for the GroupMe callback (see [Prerequisites](#prerequisites)).
|
|
18
|
+
|
|
19
|
+
> New to OpenClaw? Start with the [OpenClaw docs](https://docs.openclaw.ai) and the [channels overview](https://docs.openclaw.ai/channels).
|
|
4
20
|
|
|
5
21
|
## Install
|
|
6
22
|
|
|
@@ -20,7 +36,7 @@ After installing, restart the gateway so it picks up the new plugin:
|
|
|
20
36
|
openclaw gateway restart
|
|
21
37
|
```
|
|
22
38
|
|
|
23
|
-
That's it — the plugin is loaded and ready to configure.
|
|
39
|
+
That's it — the plugin is loaded and ready to configure. (It ships compiled, so there's no build step on your end.)
|
|
24
40
|
|
|
25
41
|
## Prerequisites
|
|
26
42
|
|
|
@@ -66,31 +82,33 @@ The interactive wizard is the easiest way to get started. It creates a new Group
|
|
|
66
82
|
openclaw channels add
|
|
67
83
|
```
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
> You can also reach the same GroupMe wizard from the broader setup flows: `openclaw configure --section channels` or the first-run `openclaw onboard`.
|
|
70
86
|
|
|
71
|
-
**
|
|
87
|
+
**2.** Choose **GroupMe** from the channel list (use the arrow keys to select it).
|
|
72
88
|
|
|
73
|
-
**
|
|
89
|
+
**3.** Enter a **bot name**. This is the display name your bot will use in the group (e.g., `openclaw`). This name is also used for mention detection.
|
|
74
90
|
|
|
75
|
-
**
|
|
91
|
+
**4.** Paste your **GroupMe access token** when prompted.
|
|
76
92
|
|
|
77
|
-
**
|
|
93
|
+
**5.** The wizard fetches your groups from GroupMe. **Select the group** you want the bot to live in.
|
|
78
94
|
|
|
79
|
-
**
|
|
95
|
+
**6.** Choose whether to **require an @mention**:
|
|
80
96
|
- **Yes** — The bot only responds when someone mentions it by name (e.g., "hey @openclaw, what's the weather?"). This is great for groups with multiple people where you don't want the bot jumping into every conversation.
|
|
81
97
|
- **No** — The bot responds to every message in the group. Perfect if you're the only human in the group and want a direct chat experience.
|
|
82
98
|
|
|
83
|
-
**
|
|
99
|
+
**7.** Enter the **public domain** that will host your callback URL. This should be the domain name (or public IP) that can reach your OpenClaw gateway — for example, `bot.example.com`. The wizard generates a secure callback URL with a random path and secret token, then registers the bot with GroupMe.
|
|
84
100
|
|
|
85
|
-
**
|
|
101
|
+
**8.** The wizard writes the config and shows a summary of what was created.
|
|
86
102
|
|
|
87
|
-
**
|
|
103
|
+
**9.** Restart the gateway:
|
|
88
104
|
|
|
89
105
|
```bash
|
|
90
106
|
openclaw gateway restart
|
|
91
107
|
```
|
|
92
108
|
|
|
93
|
-
**
|
|
109
|
+
**10.** Send a test message in the GroupMe group. If everything is wired up correctly, your bot should respond. Make sure your reverse proxy or port forwarding is configured to route the callback URL to your gateway.
|
|
110
|
+
|
|
111
|
+
> **After the wizard — hardening secrets (recommended for production):** the wizard writes your `accessToken`, `botId`, and `callbackToken` as plaintext literals in `openclaw.json`. That's fine for local testing, but for production you should migrate them to [SecretRefs](#secrets) so no plaintext credentials live in your config. See [Secrets](#secrets) below.
|
|
94
112
|
|
|
95
113
|
## Setup: Non-Interactive CLI
|
|
96
114
|
|
|
@@ -102,7 +120,7 @@ If you already have a GroupMe bot created (maybe from the [Bots page](https://de
|
|
|
102
120
|
openclaw channels add --channel groupme \
|
|
103
121
|
--token "YOUR_GROUPME_BOT_ID" \
|
|
104
122
|
--access-token "YOUR_GROUPME_ACCESS_TOKEN" \
|
|
105
|
-
--webhook-
|
|
123
|
+
--webhook-path "/groupme/callback"
|
|
106
124
|
```
|
|
107
125
|
|
|
108
126
|
For named accounts (useful if you're running multiple bots):
|
|
@@ -113,26 +131,28 @@ openclaw channels add --channel groupme \
|
|
|
113
131
|
--name "Work Bot" \
|
|
114
132
|
--token "YOUR_GROUPME_BOT_ID" \
|
|
115
133
|
--access-token "YOUR_GROUPME_ACCESS_TOKEN" \
|
|
116
|
-
--webhook-
|
|
134
|
+
--webhook-path "/groupme/callback"
|
|
117
135
|
```
|
|
118
136
|
|
|
119
137
|
| Flag | Maps to config | Description |
|
|
120
138
|
| ---- | -------------- | ----------- |
|
|
121
139
|
| `--token` | `botId` | Your GroupMe Bot ID |
|
|
122
140
|
| `--access-token` | `accessToken` | Your GroupMe access token |
|
|
123
|
-
| `--webhook-url` | `
|
|
124
|
-
| `--webhook-path` | `
|
|
141
|
+
| `--webhook-url` | `webhookPath` (+ `callbackToken` if a `?k=` is present) | Full webhook URL; the path and `k` token are extracted |
|
|
142
|
+
| `--webhook-path` | `webhookPath` (+ `callbackToken` if a `?k=` is present) | Relative webhook route path |
|
|
125
143
|
| `--account` | account ID | Named account identifier |
|
|
126
144
|
| `--name` | `name` | Display name for the account |
|
|
127
145
|
|
|
128
|
-
> **Note:** The non-interactive CLI does not prompt for `botName`, `groupId`, `requireMention`, or `
|
|
146
|
+
> **Note:** The non-interactive CLI does not prompt for `botName`, `groupId`, `requireMention`, `publicDomain`, or `callbackToken`. Add those manually afterward (a `?k=<token>` on `--webhook-path`/`--webhook-url` is the one exception — it's parsed into `callbackToken`), or use the interactive wizard to generate complete webhook settings.
|
|
129
147
|
|
|
130
|
-
After adding the channel, make sure the callback URL you gave GroupMe
|
|
148
|
+
After adding the channel, make sure the callback URL you gave GroupMe uses the configured `webhookPath` and `callbackToken`. Then restart the gateway:
|
|
131
149
|
|
|
132
150
|
```bash
|
|
133
151
|
openclaw gateway restart
|
|
134
152
|
```
|
|
135
153
|
|
|
154
|
+
Run `openclaw channels add --help` to see the full list of per-channel flags.
|
|
155
|
+
|
|
136
156
|
## Manual Config Example
|
|
137
157
|
|
|
138
158
|
If you prefer editing config files directly, here's what a complete setup looks like:
|
|
@@ -147,21 +167,62 @@ If you prefer editing config files directly, here's what a complete setup looks
|
|
|
147
167
|
"botId": "YOUR_GROUPME_BOT_ID",
|
|
148
168
|
"groupId": "YOUR_GROUPME_GROUP_ID",
|
|
149
169
|
"publicDomain": "bot.example.com",
|
|
150
|
-
"
|
|
170
|
+
"webhookPath": "/groupme/e60b3e59da98950f",
|
|
171
|
+
"callbackToken": "YOUR_SECRET_TOKEN",
|
|
151
172
|
"requireMention": true
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
```
|
|
156
177
|
|
|
178
|
+
## Multiple Accounts
|
|
179
|
+
|
|
180
|
+
Each GroupMe bot is bound to a single group, so running the bot in more than one group means configuring more than one account. Top-level fields act as the `default` account, and additional accounts live under `accounts.<id>`. Named accounts inherit any fields you don't override.
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"channels": {
|
|
185
|
+
"groupme": {
|
|
186
|
+
"enabled": true,
|
|
187
|
+
"defaultAccount": "family",
|
|
188
|
+
"botName": "openclaw",
|
|
189
|
+
"accounts": {
|
|
190
|
+
"family": {
|
|
191
|
+
"name": "Family Bot",
|
|
192
|
+
"accessToken": "FAMILY_ACCESS_TOKEN",
|
|
193
|
+
"botId": "FAMILY_BOT_ID",
|
|
194
|
+
"groupId": "FAMILY_GROUP_ID",
|
|
195
|
+
"publicDomain": "bot.example.com",
|
|
196
|
+
"webhookPath": "/groupme/family-a1b2c3",
|
|
197
|
+
"callbackToken": "FAMILY_SECRET_TOKEN",
|
|
198
|
+
"requireMention": false
|
|
199
|
+
},
|
|
200
|
+
"work": {
|
|
201
|
+
"name": "Work Bot",
|
|
202
|
+
"accessToken": "WORK_ACCESS_TOKEN",
|
|
203
|
+
"botId": "WORK_BOT_ID",
|
|
204
|
+
"groupId": "WORK_GROUP_ID",
|
|
205
|
+
"publicDomain": "bot.example.com",
|
|
206
|
+
"webhookPath": "/groupme/work-d4e5f6",
|
|
207
|
+
"callbackToken": "WORK_SECRET_TOKEN",
|
|
208
|
+
"requireMention": true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Each account needs its **own** `webhookPath` and `callbackToken`, since each registered bot has its own callback URL.
|
|
217
|
+
|
|
157
218
|
## Reconfiguring an Existing Setup
|
|
158
219
|
|
|
159
|
-
If GroupMe is already configured and you run `openclaw configure` again, you'll get a menu of targeted actions instead of repeating the full wizard:
|
|
220
|
+
If GroupMe is already configured and you run `openclaw configure --section channels` (or `openclaw channels add`) again, you'll get a menu of targeted actions instead of repeating the full wizard:
|
|
160
221
|
|
|
161
222
|
- **Skip** — leave everything as-is
|
|
162
223
|
- **Rotate access token** — replace the stored access token (validates the new token by fetching your groups)
|
|
163
224
|
- **Change group** — pick a different GroupMe group and optionally register a new bot in it
|
|
164
|
-
- **Regenerate
|
|
225
|
+
- **Regenerate webhook settings** — create a new random webhook path and callback token (you'll need to update the bot's callback URL in GroupMe afterward)
|
|
165
226
|
- **Toggle requireMention** — flip mention-required mode on or off
|
|
166
227
|
- **Update public domain** — change the public domain used for callback URLs
|
|
167
228
|
- **Full re-setup** — start from scratch with the interactive wizard
|
|
@@ -231,7 +292,7 @@ The plugin ships with some security defaults out of the box. Replay protection a
|
|
|
231
292
|
Every incoming webhook request goes through this gauntlet before your agent ever sees it:
|
|
232
293
|
|
|
233
294
|
1. **Method check** — Only `POST` requests are accepted (405 for everything else)
|
|
234
|
-
2. **Callback token auth** — The `k` query parameter in the callback URL is verified using timing-safe comparison
|
|
295
|
+
2. **Callback token auth** — The `k` query parameter in the callback URL is verified against `callbackToken` using timing-safe comparison
|
|
235
296
|
3. **Proxy validation** — If configured, validates trusted proxy headers, allowed hosts, and HTTPS protocol
|
|
236
297
|
4. **Body parsing** — 64KB size limit, 15-second timeout
|
|
237
298
|
5. **Payload parsing** — Extracts the GroupMe callback data and filters out bot messages, system messages, and empty messages
|
|
@@ -239,6 +300,8 @@ Every incoming webhook request goes through this gauntlet before your agent ever
|
|
|
239
300
|
7. **Replay protection** — SHA-256 keyed deduplication with a sliding TTL window
|
|
240
301
|
8. **Rate limiting** — Per-IP, per-sender, and global concurrency caps
|
|
241
302
|
|
|
303
|
+
Accepted requests get an immediate `200 ok` and the message is processed asynchronously, so GroupMe never waits on your agent.
|
|
304
|
+
|
|
242
305
|
### Outbound Media Security
|
|
243
306
|
|
|
244
307
|
When the bot sends image replies, outbound media fetches are hardened with:
|
|
@@ -296,6 +359,8 @@ You only need a `security` block if you want to override the defaults. Just incl
|
|
|
296
359
|
| `security.media.requestTimeoutMs` | number | `10000` | Timeout for outbound media fetch requests (ms) |
|
|
297
360
|
| `security.media.allowedMimePrefixes` | string[] | `["image/"]` | Allowed MIME type prefixes for outbound media |
|
|
298
361
|
|
|
362
|
+
> **Inbound vs. outbound media limits:** the `security.media.*` settings above govern **outbound** media — images the bot downloads from a URL and re-uploads to GroupMe. The separate top-level `mediaMaxMb` field governs **inbound** media the OpenClaw runtime fetches from GroupMe callbacks. They are independent knobs.
|
|
363
|
+
|
|
299
364
|
#### Logging
|
|
300
365
|
|
|
301
366
|
| Field | Type | Default | Description |
|
|
@@ -321,47 +386,78 @@ Include a `proxy` block to enable trusted-proxy validation. This is useful when
|
|
|
321
386
|
| `security.proxy.requireHttpsProto` | boolean | `false` | Require the effective protocol to be HTTPS |
|
|
322
387
|
| `security.proxy.rejectStatus` | number | `403` | HTTP status for proxy-policy rejections (`400`, `403`, or `404`) |
|
|
323
388
|
|
|
324
|
-
##
|
|
389
|
+
## Secrets
|
|
390
|
+
|
|
391
|
+
`botId`, `accessToken`, and `callbackToken` are OpenClaw secret inputs. You can store literal values, but **for production the recommended approach is SecretRefs** so no plaintext credentials live in your config — this matches OpenClaw's [secrets guidance](https://docs.openclaw.ai/gateway/secrets) and how official channels like Slack and Discord document setup. Plaintext is fully supported and fine for quick local testing.
|
|
325
392
|
|
|
326
|
-
|
|
393
|
+
The plugin does **not** read environment variables on its own. The `GROUPME_BOT_ID`, `GROUPME_ACCESS_TOKEN`, and `GROUPME_CALLBACK_TOKEN` names it declares (as `channelEnvVars`) are what OpenClaw surfaces as **env-backed SecretRefs** and for setup tooling — to actually use them, reference them from config with a SecretRef (shown below), or let the setup wizard write the config for you. Setting those env vars alone, without a SecretRef in config, is not enough to configure the channel.
|
|
327
394
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"channels": {
|
|
398
|
+
"groupme": {
|
|
399
|
+
"botId": { "source": "env", "provider": "default", "id": "GROUPME_BOT_ID" },
|
|
400
|
+
"accessToken": { "source": "env", "provider": "default", "id": "GROUPME_ACCESS_TOKEN" },
|
|
401
|
+
"callbackToken": { "source": "env", "provider": "default", "id": "GROUPME_CALLBACK_TOKEN" },
|
|
402
|
+
"groupId": "YOUR_GROUPME_GROUP_ID",
|
|
403
|
+
"webhookPath": "/groupme/callback"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
336
408
|
|
|
337
|
-
|
|
409
|
+
SecretRefs also support `file`- and `exec`-backed providers, and work per account (under `accounts.<id>`), not just the default account.
|
|
410
|
+
|
|
411
|
+
### Migrating wizard-written plaintext to SecretRefs
|
|
412
|
+
|
|
413
|
+
The setup wizard writes plaintext credentials. To convert them to SecretRefs (and scrub the plaintext residue), use OpenClaw's secrets workflow — GroupMe's `botId`, `accessToken`, and `callbackToken` are registered targets, so they show up in the planner automatically:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
openclaw secrets configure # plan the migration (interactive)
|
|
417
|
+
openclaw secrets apply --from <plan> # write SecretRefs and scrub plaintext
|
|
418
|
+
openclaw secrets audit --check # verify no plaintext residue remains
|
|
419
|
+
openclaw secrets reload # re-resolve refs into the runtime snapshot
|
|
420
|
+
```
|
|
338
421
|
|
|
339
422
|
## Config Reference
|
|
340
423
|
|
|
341
424
|
| Field | Type | Default | Description |
|
|
342
425
|
| ----- | ---- | ------- | ----------- |
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
426
|
+
| `enabled` | boolean | `true` | Whether the GroupMe channel/account is active; only an explicit `false` disables it. An account still needs a `botId` before it will actually run. |
|
|
427
|
+
| `name` | string | — | Display name for the account |
|
|
428
|
+
| `botId` | secret input | — | GroupMe Bot ID |
|
|
429
|
+
| `accessToken` | secret input | — | GroupMe access token (required for image uploads and the interactive wizard) |
|
|
430
|
+
| `callbackToken` | secret input | — | Secret expected in the inbound `k` query parameter |
|
|
345
431
|
| `botName` | string | — | Bot display name, used for mention detection |
|
|
346
432
|
| `groupId` | string | — | Expected GroupMe `group_id` for inbound binding |
|
|
347
433
|
| `publicDomain` | string | — | Public domain where the gateway is reachable (e.g., `bot.example.com`) |
|
|
348
|
-
| `
|
|
434
|
+
| `webhookPath` | string | `/groupme` | Relative webhook route path |
|
|
349
435
|
| `requireMention` | boolean | `true` | Only respond when mentioned by name |
|
|
350
|
-
| `historyLimit` | number | `20` | Max buffered messages per group (when `requireMention: true`) |
|
|
436
|
+
| `historyLimit` | number | `20` | Max buffered messages per group (when `requireMention: true`); `0` disables buffering |
|
|
351
437
|
| `mentionPatterns` | string[] | — | Custom regex patterns for mention detection |
|
|
352
438
|
| `allowFrom` | array | — | Sender allowlist (`"*"` allows everyone) |
|
|
353
|
-
| `textChunkLimit` | number | `1000` | Max characters per outbound text chunk |
|
|
439
|
+
| `textChunkLimit` | number | `1000` | Max characters per outbound text chunk (capped at GroupMe's 1000-char limit) |
|
|
440
|
+
| `responsePrefix` | string | — | Text prepended to each outbound reply |
|
|
441
|
+
| `blockStreaming` | boolean | unset (OpenClaw default) | Override block streaming for this channel. When unset, OpenClaw's dispatcher default applies; set `true` to stream completed assistant blocks as separate messages, or `false` to send a single final reply |
|
|
442
|
+
| `blockStreamingCoalesce` | object | — | Fine-tunes how streamed blocks are coalesced (see OpenClaw docs) |
|
|
443
|
+
| `markdown` | object | — | Markdown rendering overrides for outbound messages |
|
|
444
|
+
| `mediaMaxMb` | number | — | Max size (MB) for **inbound** media the OpenClaw runtime fetches from GroupMe. Distinct from `security.media.maxDownloadBytes`, which caps **outbound** media the bot downloads before re-uploading. |
|
|
354
445
|
| `security` | object | — | Security overrides (see [Security](#security) section above) |
|
|
446
|
+
| `accounts` | object | — | Named accounts (`accounts.<id>`), each accepting the fields above |
|
|
447
|
+
| `defaultAccount` | string | — | Which named account is the default for outbound routing |
|
|
355
448
|
|
|
356
|
-
##
|
|
449
|
+
## Webhook URL Format
|
|
357
450
|
|
|
358
|
-
The
|
|
451
|
+
The plugin stores the route path and secret separately:
|
|
359
452
|
|
|
360
|
-
```
|
|
361
|
-
|
|
453
|
+
```json
|
|
454
|
+
{
|
|
455
|
+
"webhookPath": "/groupme/e60b3e59da98950f",
|
|
456
|
+
"callbackToken": "775c9958da544c73e6d97c04f884957caa174c8570889bbaa0900d6253f20bbc"
|
|
457
|
+
}
|
|
362
458
|
```
|
|
363
459
|
|
|
364
|
-
The full URL you register with GroupMe is your public domain
|
|
460
|
+
The full URL you register with GroupMe is your public domain plus the path and `k` token:
|
|
365
461
|
|
|
366
462
|
```
|
|
367
463
|
https://bot.example.com/groupme/e60b3e59da98950f?k=775c9958da544c73e6d97c04f884957caa174c8570889bbaa0900d6253f20bbc
|
|
@@ -390,14 +486,14 @@ GroupMe bots are intentionally limited compared to full user accounts. These con
|
|
|
390
486
|
## Troubleshooting
|
|
391
487
|
|
|
392
488
|
- **Bot doesn't respond:**
|
|
393
|
-
- Is your webhook URL public, HTTPS, and matching
|
|
489
|
+
- Is your webhook URL public, HTTPS, and matching `webhookPath` plus `callbackToken`?
|
|
394
490
|
- Does `groupId` match the actual GroupMe group ID?
|
|
395
491
|
- Is `botId` correct?
|
|
396
492
|
- If `requireMention: true`, are you mentioning the bot by name?
|
|
397
493
|
- Check `allowFrom` if you have a sender allowlist configured
|
|
398
494
|
|
|
399
495
|
- **Webhook returns 404 or 403:**
|
|
400
|
-
- Verify the `k` token in the URL matches
|
|
496
|
+
- Verify the `k` token in the URL matches `callbackToken`
|
|
401
497
|
- Check `groupId` binding
|
|
402
498
|
- If using proxy validation, check your `security.proxy` settings
|
|
403
499
|
|
|
@@ -425,6 +521,12 @@ openclaw channels logs --channel groupme
|
|
|
425
521
|
openclaw channels status --probe
|
|
426
522
|
```
|
|
427
523
|
|
|
524
|
+
## Resources
|
|
525
|
+
|
|
526
|
+
- [OpenClaw documentation](https://docs.openclaw.ai) · [Channels](https://docs.openclaw.ai/channels) · [Plugins](https://docs.openclaw.ai/plugins)
|
|
527
|
+
- [GroupMe developer portal](https://dev.groupme.com) · [Bots](https://dev.groupme.com/bots)
|
|
528
|
+
- [Contributing guide](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Changelog](CHANGELOG.md)
|
|
529
|
+
|
|
428
530
|
## License
|
|
429
531
|
|
|
430
532
|
[MIT](LICENSE)
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { setGroupMeRuntime } from "./src/runtime.js";
|
|
4
|
-
const plugin = {
|
|
1
|
+
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
2
|
+
const plugin = defineBundledChannelEntry({
|
|
5
3
|
id: "groupme",
|
|
6
4
|
name: "GroupMe",
|
|
7
5
|
description: "GroupMe channel plugin",
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
importMetaUrl: import.meta.url,
|
|
7
|
+
plugin: {
|
|
8
|
+
specifier: "./channel-plugin-api.js",
|
|
9
|
+
exportName: "groupmePlugin",
|
|
12
10
|
},
|
|
13
|
-
|
|
11
|
+
secrets: {
|
|
12
|
+
specifier: "./secret-contract-api.js",
|
|
13
|
+
exportName: "channelSecrets",
|
|
14
|
+
},
|
|
15
|
+
runtime: {
|
|
16
|
+
specifier: "./runtime-setter-api.js",
|
|
17
|
+
exportName: "setGroupMeRuntime",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
14
20
|
export default plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { setGroupMeRuntime } from "./src/runtime.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries, } from "./src/secret-contract.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
2
|
+
export default defineBundledChannelSetupEntry({
|
|
3
|
+
importMetaUrl: import.meta.url,
|
|
4
|
+
plugin: {
|
|
5
|
+
specifier: "./setup-plugin-api.js",
|
|
6
|
+
exportName: "groupmeSetupPlugin",
|
|
7
|
+
},
|
|
8
|
+
secrets: {
|
|
9
|
+
specifier: "./secret-contract-api.js",
|
|
10
|
+
exportName: "channelSecrets",
|
|
11
|
+
},
|
|
12
|
+
runtime: {
|
|
13
|
+
specifier: "./runtime-setter-api.js",
|
|
14
|
+
exportName: "setGroupMeRuntime",
|
|
15
|
+
},
|
|
16
|
+
});
|
package/dist/src/accounts.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId
|
|
2
|
-
const ENV_BOT_ID = "GROUPME_BOT_ID";
|
|
3
|
-
const ENV_ACCESS_TOKEN = "GROUPME_ACCESS_TOKEN";
|
|
4
|
-
const ENV_BOT_NAME = "GROUPME_BOT_NAME";
|
|
5
|
-
const ENV_CALLBACK_URL = "GROUPME_CALLBACK_URL";
|
|
6
|
-
const ENV_GROUP_ID = "GROUPME_GROUP_ID";
|
|
7
|
-
const ENV_PUBLIC_DOMAIN = "GROUPME_PUBLIC_DOMAIN";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
8
2
|
export function readTrimmed(value) {
|
|
9
3
|
if (typeof value !== "string") {
|
|
10
4
|
return undefined;
|
|
@@ -12,6 +6,15 @@ export function readTrimmed(value) {
|
|
|
12
6
|
const trimmed = value.trim();
|
|
13
7
|
return trimmed || undefined;
|
|
14
8
|
}
|
|
9
|
+
export function hasSecretInput(value) {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
return Boolean(value.trim());
|
|
12
|
+
}
|
|
13
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
14
|
+
}
|
|
15
|
+
function trimSecretInput(value) {
|
|
16
|
+
return typeof value === "string" ? readTrimmed(value) : value;
|
|
17
|
+
}
|
|
15
18
|
function listConfiguredAccountIds(cfg) {
|
|
16
19
|
const accounts = cfg.channels?.groupme?.accounts;
|
|
17
20
|
if (!accounts || typeof accounts !== "object") {
|
|
@@ -40,9 +43,7 @@ function resolveAccountConfig(cfg, accountId) {
|
|
|
40
43
|
function mergeAccountConfig(cfg, accountId) {
|
|
41
44
|
const raw = (cfg.channels?.groupme ?? {});
|
|
42
45
|
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
|
43
|
-
const account = accountId === DEFAULT_ACCOUNT_ID
|
|
44
|
-
? {}
|
|
45
|
-
: (resolveAccountConfig(cfg, accountId) ?? {});
|
|
46
|
+
const account = accountId === DEFAULT_ACCOUNT_ID ? {} : (resolveAccountConfig(cfg, accountId) ?? {});
|
|
46
47
|
return {
|
|
47
48
|
...base,
|
|
48
49
|
...account,
|
|
@@ -50,10 +51,7 @@ function mergeAccountConfig(cfg, accountId) {
|
|
|
50
51
|
}
|
|
51
52
|
export function listGroupMeAccountIds(cfg) {
|
|
52
53
|
const sorted = listConfiguredAccountIds(cfg).toSorted((a, b) => a.localeCompare(b));
|
|
53
|
-
return [
|
|
54
|
-
DEFAULT_ACCOUNT_ID,
|
|
55
|
-
...sorted.filter((id) => id !== DEFAULT_ACCOUNT_ID),
|
|
56
|
-
];
|
|
54
|
+
return [DEFAULT_ACCOUNT_ID, ...sorted.filter((id) => id !== DEFAULT_ACCOUNT_ID)];
|
|
57
55
|
}
|
|
58
56
|
export function resolveDefaultGroupMeAccountId(cfg) {
|
|
59
57
|
const configuredDefault = readTrimmed(cfg.channels?.groupme?.defaultAccount);
|
|
@@ -64,54 +62,32 @@ export function resolveDefaultGroupMeAccountId(cfg) {
|
|
|
64
62
|
}
|
|
65
63
|
export function resolveGroupMeAccount(params) {
|
|
66
64
|
const normalizedRequested = normalizeAccountId(params.accountId);
|
|
67
|
-
const accountId = normalizedRequested ||
|
|
68
|
-
resolveDefaultGroupMeAccountId(params.cfg) ||
|
|
69
|
-
DEFAULT_ACCOUNT_ID;
|
|
65
|
+
const accountId = normalizedRequested || resolveDefaultGroupMeAccountId(params.cfg) || DEFAULT_ACCOUNT_ID;
|
|
70
66
|
const merged = mergeAccountConfig(params.cfg, accountId);
|
|
71
67
|
const baseEnabled = params.cfg.channels?.groupme?.enabled !== false;
|
|
72
68
|
const accountEnabled = merged.enabled !== false;
|
|
73
69
|
const enabled = baseEnabled && accountEnabled;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
? readTrimmed(process.env[ENV_ACCESS_TOKEN])
|
|
81
|
-
: undefined) ||
|
|
82
|
-
"";
|
|
83
|
-
const botName = readTrimmed(merged.botName) ||
|
|
84
|
-
(isDefaultAccount ? readTrimmed(process.env[ENV_BOT_NAME]) : undefined) ||
|
|
85
|
-
undefined;
|
|
86
|
-
const groupId = readTrimmed(merged.groupId) ||
|
|
87
|
-
(isDefaultAccount
|
|
88
|
-
? readTrimmed(process.env[ENV_GROUP_ID])
|
|
89
|
-
: undefined) ||
|
|
90
|
-
undefined;
|
|
91
|
-
const callbackUrl = readTrimmed(merged.callbackUrl) ||
|
|
92
|
-
(isDefaultAccount
|
|
93
|
-
? readTrimmed(process.env[ENV_CALLBACK_URL])
|
|
94
|
-
: undefined) ||
|
|
95
|
-
undefined;
|
|
96
|
-
const publicDomain = readTrimmed(merged.publicDomain) ||
|
|
97
|
-
(isDefaultAccount
|
|
98
|
-
? readTrimmed(process.env[ENV_PUBLIC_DOMAIN])
|
|
99
|
-
: undefined) ||
|
|
100
|
-
undefined;
|
|
70
|
+
const botId = readTrimmed(merged.botId) ?? "";
|
|
71
|
+
const accessToken = readTrimmed(merged.accessToken) ?? "";
|
|
72
|
+
const botName = readTrimmed(merged.botName);
|
|
73
|
+
const groupId = readTrimmed(merged.groupId);
|
|
74
|
+
const webhookPath = readTrimmed(merged.webhookPath);
|
|
75
|
+
const publicDomain = readTrimmed(merged.publicDomain);
|
|
101
76
|
const config = {
|
|
102
77
|
...merged,
|
|
103
|
-
botId,
|
|
104
|
-
accessToken,
|
|
78
|
+
botId: trimSecretInput(merged.botId),
|
|
79
|
+
accessToken: trimSecretInput(merged.accessToken),
|
|
80
|
+
callbackToken: trimSecretInput(merged.callbackToken),
|
|
105
81
|
botName,
|
|
106
82
|
groupId,
|
|
107
83
|
publicDomain,
|
|
108
|
-
|
|
84
|
+
webhookPath,
|
|
109
85
|
};
|
|
110
86
|
return {
|
|
111
87
|
accountId,
|
|
112
88
|
name: readTrimmed(merged.name),
|
|
113
89
|
enabled,
|
|
114
|
-
configured:
|
|
90
|
+
configured: hasSecretInput(merged.botId),
|
|
115
91
|
botId,
|
|
116
92
|
accessToken,
|
|
117
93
|
config,
|