telegram-approval-buttons 4.0.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/index.ts +195 -0
- package/lib/approval-parser.ts +84 -0
- package/lib/approval-store.ts +143 -0
- package/lib/diagnostics.ts +164 -0
- package/lib/message-formatter.ts +152 -0
- package/lib/telegram-api.ts +165 -0
- package/openclaw.plugin.json +52 -0
- package/package.json +44 -0
- package/types.ts +98 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [4.0.1] - 2026-02-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Composite banner image showing the full approval workflow
|
|
12
|
+
- `tsconfig.json` for IDE TypeScript support
|
|
13
|
+
- `uiHints` in manifest for better config UI labels
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Rewritten README with simplified 3-step Quick Start for beginners
|
|
17
|
+
- FAQ section with 5 common questions
|
|
18
|
+
- Cleaner troubleshooting table with less jargon
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `/approvalstatus` command showing raw HTML `<b>` tags instead of plain text
|
|
22
|
+
- Plugin ID mismatch warning on gateway startup (aligned `package.json` name with manifest `id`)
|
|
23
|
+
|
|
24
|
+
## [4.0.0] - 2026-02-14
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Initial public release
|
|
28
|
+
- One-tap inline keyboard buttons: โ
Allow Once ยท ๐ Always ยท โ Deny
|
|
29
|
+
- Auto-resolve: edits message after decision, removes buttons
|
|
30
|
+
- Expiry handling: stale approvals auto-cleaned after configurable timeout
|
|
31
|
+
- `/approvalstatus` diagnostic command with health check
|
|
32
|
+
- Auto-detection of `chatId` and `botToken` from `channels.telegram` config
|
|
33
|
+
- Modular architecture: `index.ts` orchestration + 5 lib modules
|
|
34
|
+
- Full `configSchema` with JSON Schema validation
|
|
35
|
+
- MIT license
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jair Franco
|
|
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,183 @@
|
|
|
1
|
+
# ๐ Telegram Approval Buttons for OpenClaw
|
|
2
|
+
|
|
3
|
+
> One-tap `exec` approvals in Telegram โ no more typing `/approve <uuid> allow-once`.
|
|
4
|
+
|
|
5
|
+
## What does this look like?
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="docs/banner.png" alt="Plugin workflow: approval request โ allowed โ health check" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## What does this do?
|
|
12
|
+
|
|
13
|
+
OpenClaw's Discord has built-in approval buttons. **Telegram doesn't** โ you're stuck typing long `/approve` commands. This plugin fixes that.
|
|
14
|
+
|
|
15
|
+
**Features:**
|
|
16
|
+
- โ
**One-tap approvals** โ Allow Once ยท ๐ Always ยท โ Deny
|
|
17
|
+
- ๐ **Auto-resolve** โ edits the message after decision (removes buttons, shows result)
|
|
18
|
+
- โฐ **Expiry handling** โ stale approvals auto-cleaned and marked as expired
|
|
19
|
+
- ๐ฉบ **Self-diagnostics** โ `/approvalstatus` checks health and stats
|
|
20
|
+
- ๐ก๏ธ **Graceful fallback** โ if buttons fail, the original text goes through
|
|
21
|
+
- ๐ฆ **Zero dependencies** โ uses only Node.js built-in `fetch`
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Step 1: Download the plugin
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/JairFC/openclaw-telegram-approval-buttons.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Step 2: Add to your config
|
|
32
|
+
|
|
33
|
+
Open your `openclaw.json` and add this block. If the `"plugins"` section already exists, just merge the contents:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"plugins": {
|
|
38
|
+
"load": {
|
|
39
|
+
"paths": ["/path/to/openclaw-telegram-approval-buttons"]
|
|
40
|
+
},
|
|
41
|
+
"entries": {
|
|
42
|
+
"telegram-approval-buttons": {
|
|
43
|
+
"enabled": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> ๐ก **Replace `/path/to/`** with the actual path where you cloned the repo.
|
|
51
|
+
> Example: `"/home/jair/Projects/openclaw-telegram-approval-buttons"`
|
|
52
|
+
|
|
53
|
+
### Step 3: Restart and verify
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
openclaw gateway restart
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then send `/approvalstatus` in your Telegram chat. You should see:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
๐ข Approval Buttons Status
|
|
63
|
+
|
|
64
|
+
Config: chatId=โ ยท token=โ
|
|
65
|
+
Telegram: โ connected (@your_bot)
|
|
66
|
+
Pending: 0 ยท Processed: 0
|
|
67
|
+
Uptime: 1m
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**That's it!** Next time the AI triggers an `exec` approval, you'll get buttons.
|
|
71
|
+
|
|
72
|
+
## Prerequisites
|
|
73
|
+
|
|
74
|
+
- **OpenClaw โฅ 2026.2.9** installed and running
|
|
75
|
+
- **Node.js โฅ 22** (uses built-in `fetch`)
|
|
76
|
+
- **Telegram configured** in your `openclaw.json`
|
|
77
|
+
- **Exec approvals enabled** โ `tools.exec.ask` must NOT be `"off"`
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
โโโโโโโโโโโโโโโ message_sending โโโโโโโโโโโโโโโโโโโโ
|
|
83
|
+
โ OpenClaw โ โโ approval text โโโ โ Plugin โ
|
|
84
|
+
โ Gateway โ โ โ
|
|
85
|
+
โ โ cancel original โ 1. Parse text โ
|
|
86
|
+
โ โ โโโโโโโโโโโโโโโโโโโโโ โ 2. Send buttons โ
|
|
87
|
+
โโโโโโโโโโโโโโโ โ 3. Track pending โ
|
|
88
|
+
โโโโโโโโโโฌโโโโโโโโโโ
|
|
89
|
+
โ
|
|
90
|
+
Telegram Bot API
|
|
91
|
+
โ
|
|
92
|
+
โโโโโโโโโโผโโโโโโโโโโ
|
|
93
|
+
โ Telegram Chat โ
|
|
94
|
+
โ โ
|
|
95
|
+
โ ๐ Exec Approval โ
|
|
96
|
+
โ [โ
Allow] [๐] โ
|
|
97
|
+
โ [โ Deny] โ
|
|
98
|
+
โโโโโโโโโโโโโโโโโโโโ
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When you tap a button, OpenClaw converts the `callback_data` into a synthetic text message โ **no webhook needed**.
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
The plugin **auto-detects** `botToken` and `chatId` from your Telegram channel config. Most setups need zero extra configuration.
|
|
106
|
+
|
|
107
|
+
### Config resolution order
|
|
108
|
+
|
|
109
|
+
| Setting | Priority 1 (explicit) | Priority 2 (shared config) | Priority 3 (env) |
|
|
110
|
+
|------------|-----------------------------|------------------------------------|---------------------------|
|
|
111
|
+
| `botToken` | `pluginConfig.botToken` | `channels.telegram.token` | `TELEGRAM_BOT_TOKEN` |
|
|
112
|
+
| `chatId` | `pluginConfig.chatId` | `channels.telegram.allowFrom[0]` | `TELEGRAM_CHAT_ID` |
|
|
113
|
+
|
|
114
|
+
### Advanced options
|
|
115
|
+
|
|
116
|
+
```jsonc
|
|
117
|
+
{
|
|
118
|
+
"plugins": {
|
|
119
|
+
"entries": {
|
|
120
|
+
"telegram-approval-buttons": {
|
|
121
|
+
"enabled": true,
|
|
122
|
+
"config": {
|
|
123
|
+
"chatId": "123456789", // Override auto-detected chat ID
|
|
124
|
+
"botToken": "123:ABC...", // Override auto-detected bot token
|
|
125
|
+
"staleMins": 10, // Minutes before stale cleanup (default: 10)
|
|
126
|
+
"verbose": false // Diagnostic logging (default: false)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## FAQ
|
|
135
|
+
|
|
136
|
+
**Q: I installed the plugin but no buttons appear.**
|
|
137
|
+
A: Make sure `tools.exec.ask` is NOT set to `"off"` in your config. If it's `"off"`, there are no approvals to buttonize. Set it to `"on-miss"` or `"always"`.
|
|
138
|
+
|
|
139
|
+
**Q: How do I find my Telegram Chat ID?**
|
|
140
|
+
A: Send `/start` to [@userinfobot](https://t.me/userinfobot) on Telegram โ it replies with your ID. Alternatively, check `https://api.telegram.org/bot<TOKEN>/getUpdates` after sending a message to your bot.
|
|
141
|
+
|
|
142
|
+
**Q: Do I need to set up a webhook?**
|
|
143
|
+
A: No! OpenClaw's Telegram integration automatically converts button taps into synthetic text messages. No extra setup needed.
|
|
144
|
+
|
|
145
|
+
**Q: What happens if the plugin fails to send buttons?**
|
|
146
|
+
A: The original plain-text approval message goes through normally. The plugin never blocks approvals.
|
|
147
|
+
|
|
148
|
+
**Q: Does this work in group chats?**
|
|
149
|
+
A: Yes, but the bot needs to be an admin or it needs permission to edit its own messages.
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
| Problem | Fix |
|
|
154
|
+
|---------|-----|
|
|
155
|
+
| No buttons appear | Check `tools.exec.ask` is not `"off"`. Run `/approvalstatus` to check config. |
|
|
156
|
+
| Buttons show but nothing happens | Bot needs message editing permission. Use a private chat or make bot admin. |
|
|
157
|
+
| `/approvalstatus` says "token=โ" | Set `botToken` in plugin config, or check `channels.telegram.token`. |
|
|
158
|
+
| `/approvalstatus` says "chatId=โ" | Set `chatId` in plugin config, or add your ID to `channels.telegram.allowFrom`. |
|
|
159
|
+
| Buttons say "expired" | Approval timed out before you tapped. Adjust `staleMins` if needed. |
|
|
160
|
+
|
|
161
|
+
## Architecture
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
telegram-approval-buttons/
|
|
165
|
+
โโโ index.ts # Entry point โ orchestration only
|
|
166
|
+
โโโ types.ts # Shared TypeScript interfaces
|
|
167
|
+
โโโ lib/
|
|
168
|
+
โ โโโ telegram-api.ts # Telegram Bot API client (isolated)
|
|
169
|
+
โ โโโ approval-parser.ts # Parse OpenClaw approval text format
|
|
170
|
+
โ โโโ message-formatter.ts # HTML formatting for Telegram messages
|
|
171
|
+
โ โโโ approval-store.ts # In-memory pending approval tracker
|
|
172
|
+
โ โโโ diagnostics.ts # Config resolution, health checks
|
|
173
|
+
โโโ openclaw.plugin.json # Plugin manifest
|
|
174
|
+
โโโ package.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
Issues and PRs welcome. Each file in `lib/` is self-contained with a single responsibility.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
[MIT](LICENSE)
|
package/index.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท index.ts (v4.0.0)
|
|
3
|
+
// Plugin entry point โ orchestration only, all logic lives in lib/
|
|
4
|
+
//
|
|
5
|
+
// Adds inline keyboard buttons to exec approval messages in Telegram.
|
|
6
|
+
// When a user taps a button, OpenClaw processes the /approve command
|
|
7
|
+
// automatically via its callback_query โ synthetic text message pipeline.
|
|
8
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
9
|
+
|
|
10
|
+
import type { PluginConfig } from "./types.js";
|
|
11
|
+
|
|
12
|
+
// โโ Modules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
|
|
14
|
+
import { TelegramApi } from "./lib/telegram-api.js";
|
|
15
|
+
import { ApprovalStore } from "./lib/approval-store.js";
|
|
16
|
+
import { parseApprovalText, detectApprovalResult } from "./lib/approval-parser.js";
|
|
17
|
+
import {
|
|
18
|
+
formatApprovalRequest,
|
|
19
|
+
formatApprovalResolved,
|
|
20
|
+
formatApprovalExpired,
|
|
21
|
+
buildApprovalKeyboard,
|
|
22
|
+
formatHealthCheck,
|
|
23
|
+
} from "./lib/message-formatter.js";
|
|
24
|
+
import {
|
|
25
|
+
resolveConfig,
|
|
26
|
+
runHealthCheck,
|
|
27
|
+
logStartupDiagnostics,
|
|
28
|
+
runStartupChecks,
|
|
29
|
+
} from "./lib/diagnostics.js";
|
|
30
|
+
|
|
31
|
+
// โโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
32
|
+
|
|
33
|
+
const PLUGIN_VERSION = "4.0.1";
|
|
34
|
+
const TAG = "telegram-approval-buttons";
|
|
35
|
+
|
|
36
|
+
// โโ Plugin registration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
37
|
+
|
|
38
|
+
function register(api: any): void {
|
|
39
|
+
const log = api.logger;
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
|
|
42
|
+
// โโโ 1. Resolve config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
43
|
+
|
|
44
|
+
const pluginCfg: PluginConfig = api.pluginConfig ?? {};
|
|
45
|
+
const telegramCfg = api.config?.channels?.telegram ?? {};
|
|
46
|
+
|
|
47
|
+
const config = resolveConfig(
|
|
48
|
+
{
|
|
49
|
+
pluginConfig: pluginCfg,
|
|
50
|
+
telegramChannelConfig: {
|
|
51
|
+
token: telegramCfg.token,
|
|
52
|
+
allowFrom: telegramCfg.allowFrom,
|
|
53
|
+
},
|
|
54
|
+
env: {
|
|
55
|
+
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN,
|
|
56
|
+
TELEGRAM_CHAT_ID: process.env.TELEGRAM_CHAT_ID,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
log,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!config) {
|
|
63
|
+
log.warn(`[${TAG}] v${PLUGIN_VERSION} loaded (DISABLED โ missing config)`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logStartupDiagnostics(config, log);
|
|
68
|
+
|
|
69
|
+
// โโโ 2. Initialize modules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
70
|
+
|
|
71
|
+
const tg = new TelegramApi(config.botToken, config.verbose ? log : undefined);
|
|
72
|
+
|
|
73
|
+
const store = new ApprovalStore(
|
|
74
|
+
config.staleMins * 60_000,
|
|
75
|
+
config.verbose ? log : undefined,
|
|
76
|
+
// onExpired: edit the Telegram message to show "expired"
|
|
77
|
+
(entry) => {
|
|
78
|
+
tg.editMessageText(
|
|
79
|
+
config.chatId,
|
|
80
|
+
entry.messageId,
|
|
81
|
+
formatApprovalExpired(entry.info),
|
|
82
|
+
).catch(() => { });
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// โโโ 3. Register background service (cleanup timer) โโโโโโโโโโโโโโโโโโ
|
|
87
|
+
|
|
88
|
+
api.registerService({
|
|
89
|
+
id: `${TAG}-cleanup`,
|
|
90
|
+
start: () => {
|
|
91
|
+
store.start();
|
|
92
|
+
// Non-blocking connectivity check
|
|
93
|
+
runStartupChecks(tg, log).catch(() => { });
|
|
94
|
+
},
|
|
95
|
+
stop: () => store.stop(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// โโโ 4. Register /approvalstatus command โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
99
|
+
|
|
100
|
+
api.registerCommand({
|
|
101
|
+
name: "approvalstatus",
|
|
102
|
+
description: "Show approval buttons plugin health and stats",
|
|
103
|
+
acceptsArgs: false,
|
|
104
|
+
requireAuth: true,
|
|
105
|
+
handler: async () => {
|
|
106
|
+
const health = await runHealthCheck(config, tg, store, startedAt);
|
|
107
|
+
return { text: formatHealthCheck(health) };
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// โโโ 5. Register message_sending hook โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
112
|
+
|
|
113
|
+
api.on(
|
|
114
|
+
"message_sending",
|
|
115
|
+
async (
|
|
116
|
+
event: { to: string; content: string; metadata?: Record<string, unknown> },
|
|
117
|
+
ctx: { channelId: string; accountId?: string },
|
|
118
|
+
) => {
|
|
119
|
+
// Only intercept Telegram messages
|
|
120
|
+
if (ctx.channelId !== "telegram") return;
|
|
121
|
+
|
|
122
|
+
// โโ 5a. Check for approval resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
const resolution = detectApprovalResult(event.content, store.entries());
|
|
124
|
+
if (resolution) {
|
|
125
|
+
const entry = store.resolve(resolution.id);
|
|
126
|
+
if (entry) {
|
|
127
|
+
log.info(
|
|
128
|
+
`[${TAG}] resolved ${resolution.id.slice(0, 8)}โฆ โ ${resolution.action}`,
|
|
129
|
+
);
|
|
130
|
+
const edited = await tg.editMessageText(
|
|
131
|
+
config.chatId,
|
|
132
|
+
entry.messageId,
|
|
133
|
+
formatApprovalResolved(entry.info, resolution.action),
|
|
134
|
+
);
|
|
135
|
+
if (edited && config.verbose) {
|
|
136
|
+
log.info(`[${TAG}] edited msg=${entry.messageId}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Don't cancel โ let resolution messages pass through
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// โโ 5b. Check for new approval request โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
144
|
+
const info = parseApprovalText(event.content);
|
|
145
|
+
if (!info) return;
|
|
146
|
+
|
|
147
|
+
// Duplicate guard
|
|
148
|
+
if (store.has(info.id)) {
|
|
149
|
+
if (config.verbose) {
|
|
150
|
+
log.info(`[${TAG}] skipping duplicate ${info.id.slice(0, 8)}โฆ`);
|
|
151
|
+
}
|
|
152
|
+
return { cancel: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log.info(`[${TAG}] intercepting ${info.id.slice(0, 8)}โฆ`);
|
|
156
|
+
|
|
157
|
+
// Send formatted message with inline buttons
|
|
158
|
+
const messageId = await tg.sendMessage(
|
|
159
|
+
config.chatId,
|
|
160
|
+
formatApprovalRequest(info),
|
|
161
|
+
buildApprovalKeyboard(info.id),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (messageId === null) {
|
|
165
|
+
log.warn(`[${TAG}] send failed for ${info.id.slice(0, 8)}โฆ โ falling back to plain text`);
|
|
166
|
+
// Don't cancel: let the plain-text message through as fallback
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Track it
|
|
171
|
+
store.add(info.id, messageId, info);
|
|
172
|
+
log.info(`[${TAG}] sent buttons for ${info.id.slice(0, 8)}โฆ (msg=${messageId})`);
|
|
173
|
+
|
|
174
|
+
// Cancel the original plain-text message
|
|
175
|
+
return { cancel: true };
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// โโโ Done โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
180
|
+
|
|
181
|
+
log.info(`[${TAG}] v${PLUGIN_VERSION} loaded โ`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// โโโ Plugin export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
185
|
+
|
|
186
|
+
export default {
|
|
187
|
+
id: "telegram-approval-buttons",
|
|
188
|
+
name: "Telegram Approval Buttons",
|
|
189
|
+
description:
|
|
190
|
+
"Adds inline keyboard buttons to exec approval messages in Telegram. " +
|
|
191
|
+
"Tap to approve/deny without typing commands.",
|
|
192
|
+
version: PLUGIN_VERSION,
|
|
193
|
+
kind: "extension" as const,
|
|
194
|
+
register,
|
|
195
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท lib/approval-parser.ts
|
|
3
|
+
// Parse OpenClaw's plain-text exec approval format into structured data
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
import type { ApprovalAction, ApprovalInfo, ApprovalResolution, SentApproval } from "../types.js";
|
|
7
|
+
|
|
8
|
+
// โโโ Regex patterns (compiled once) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
9
|
+
|
|
10
|
+
const RE_APPROVAL_MARKER = /Exec approval required/i;
|
|
11
|
+
const RE_ID = /ID:\s*([a-f0-9-]+)/i;
|
|
12
|
+
const RE_COMMAND_BLOCK = /Command:\s*`{0,3}\n?(.+?)\n?`{0,3}(?:\n|$)/is;
|
|
13
|
+
const RE_COMMAND_INLINE = /Command:\s*(.+)/i;
|
|
14
|
+
const RE_CWD = /CWD:\s*(.+)/i;
|
|
15
|
+
const RE_HOST = /Host:\s*(.+)/i;
|
|
16
|
+
const RE_AGENT = /Agent:\s*(.+)/i;
|
|
17
|
+
const RE_SECURITY = /Security:\s*(.+)/i;
|
|
18
|
+
const RE_ASK = /Ask:\s*(.+)/i;
|
|
19
|
+
const RE_EXPIRES = /Expires in:\s*(.+)/i;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse OpenClaw's plain-text approval message into an ApprovalInfo object.
|
|
23
|
+
*
|
|
24
|
+
* Returns null if the text doesn't match the approval format.
|
|
25
|
+
* This function is intentionally lenient โ it extracts what it can and
|
|
26
|
+
* falls back to sensible defaults for missing fields.
|
|
27
|
+
*/
|
|
28
|
+
export function parseApprovalText(text: string): ApprovalInfo | null {
|
|
29
|
+
if (!RE_APPROVAL_MARKER.test(text)) return null;
|
|
30
|
+
|
|
31
|
+
const id = text.match(RE_ID)?.[1]?.trim();
|
|
32
|
+
if (!id) return null;
|
|
33
|
+
|
|
34
|
+
// Try block format first (```command```), then inline
|
|
35
|
+
let command = text.match(RE_COMMAND_BLOCK)?.[1]?.trim();
|
|
36
|
+
if (!command) command = text.match(RE_COMMAND_INLINE)?.[1]?.trim() ?? "unknown";
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
command,
|
|
41
|
+
cwd: text.match(RE_CWD)?.[1]?.trim() ?? "unknown",
|
|
42
|
+
host: text.match(RE_HOST)?.[1]?.trim() ?? "gateway",
|
|
43
|
+
agent: text.match(RE_AGENT)?.[1]?.trim() ?? "main",
|
|
44
|
+
security: text.match(RE_SECURITY)?.[1]?.trim() ?? "allowlist",
|
|
45
|
+
ask: text.match(RE_ASK)?.[1]?.trim() ?? "on-miss",
|
|
46
|
+
expires: text.match(RE_EXPIRES)?.[1]?.trim() ?? "120s",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect if an outgoing message indicates an approval was resolved.
|
|
52
|
+
*
|
|
53
|
+
* Checks whether the message text references any pending approval ID
|
|
54
|
+
* (full UUID or first 8 chars) and attempts to determine the action.
|
|
55
|
+
*/
|
|
56
|
+
export function detectApprovalResult(
|
|
57
|
+
text: string,
|
|
58
|
+
pending: ReadonlyMap<string, SentApproval>,
|
|
59
|
+
): ApprovalResolution | null {
|
|
60
|
+
for (const [id] of pending) {
|
|
61
|
+
const shortId = id.slice(0, 8);
|
|
62
|
+
if (!text.includes(id) && !text.includes(shortId)) continue;
|
|
63
|
+
|
|
64
|
+
const action = inferAction(text);
|
|
65
|
+
return { id, action };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// โโโ Internal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Infer the approval action from message text.
|
|
74
|
+
* Order matters: check most specific patterns first.
|
|
75
|
+
*/
|
|
76
|
+
function inferAction(text: string): ApprovalAction {
|
|
77
|
+
const lower = text.toLowerCase();
|
|
78
|
+
if (lower.includes("allow-always") || lower.includes("always allow")) return "allow-always";
|
|
79
|
+
if (lower.includes("deny") || lower.includes("denied") || lower.includes("rejected")) return "deny";
|
|
80
|
+
if (lower.includes("allow-once") || lower.includes("allowed")) return "allow-once";
|
|
81
|
+
if (lower.includes("approved")) return "allow-once";
|
|
82
|
+
// Default when we see the ID but can't determine action
|
|
83
|
+
return "allow-once";
|
|
84
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท lib/approval-store.ts
|
|
3
|
+
// In-memory store for pending approvals with TTL-based cleanup
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
import type { ApprovalAction, ApprovalInfo, Logger, SentApproval } from "../types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages the lifecycle of pending approval requests.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Track sent approval messages (approval ID โ SentApproval)
|
|
13
|
+
* - Auto-purge stale entries after configurable TTL
|
|
14
|
+
* - Provide stats for diagnostics
|
|
15
|
+
*
|
|
16
|
+
* All state is in-memory โ approvals are ephemeral by nature
|
|
17
|
+
* and don't survive gateway restarts (by design).
|
|
18
|
+
*/
|
|
19
|
+
export class ApprovalStore {
|
|
20
|
+
private readonly pending = new Map<string, SentApproval>();
|
|
21
|
+
private totalProcessed = 0;
|
|
22
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly staleTtlMs: number,
|
|
26
|
+
private readonly log?: Logger,
|
|
27
|
+
private readonly onExpired?: (entry: SentApproval) => void,
|
|
28
|
+
) { }
|
|
29
|
+
|
|
30
|
+
// โโ Lifecycle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start periodic stale-entry cleanup (runs every staleTtlMs / 2).
|
|
34
|
+
*/
|
|
35
|
+
start(): void {
|
|
36
|
+
if (this.cleanupTimer) return;
|
|
37
|
+
const interval = Math.max(this.staleTtlMs / 2, 30_000);
|
|
38
|
+
this.cleanupTimer = setInterval(() => this.cleanStale(), interval);
|
|
39
|
+
// Prevent the timer from keeping the process alive
|
|
40
|
+
if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
|
|
41
|
+
(this.cleanupTimer as NodeJS.Timeout).unref();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stop the cleanup timer.
|
|
47
|
+
*/
|
|
48
|
+
stop(): void {
|
|
49
|
+
if (this.cleanupTimer) {
|
|
50
|
+
clearInterval(this.cleanupTimer);
|
|
51
|
+
this.cleanupTimer = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// โโ Core operations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Track a newly sent approval message.
|
|
59
|
+
*/
|
|
60
|
+
add(approvalId: string, messageId: number, info: ApprovalInfo): void {
|
|
61
|
+
this.pending.set(approvalId, {
|
|
62
|
+
messageId,
|
|
63
|
+
info,
|
|
64
|
+
sentAt: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if an approval is already being tracked.
|
|
70
|
+
*/
|
|
71
|
+
has(approvalId: string): boolean {
|
|
72
|
+
return this.pending.has(approvalId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get a pending approval entry.
|
|
77
|
+
*/
|
|
78
|
+
get(approvalId: string): SentApproval | undefined {
|
|
79
|
+
return this.pending.get(approvalId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve (remove) a pending approval and increment the processed counter.
|
|
84
|
+
* Returns the entry if it existed.
|
|
85
|
+
*/
|
|
86
|
+
resolve(approvalId: string): SentApproval | undefined {
|
|
87
|
+
const entry = this.pending.get(approvalId);
|
|
88
|
+
if (entry) {
|
|
89
|
+
this.pending.delete(approvalId);
|
|
90
|
+
this.totalProcessed++;
|
|
91
|
+
}
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get a read-only view of all pending approvals.
|
|
97
|
+
*/
|
|
98
|
+
entries(): ReadonlyMap<string, SentApproval> {
|
|
99
|
+
return this.pending;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// โโ Stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
103
|
+
|
|
104
|
+
get pendingCount(): number {
|
|
105
|
+
return this.pending.size;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get processedCount(): number {
|
|
109
|
+
return this.totalProcessed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// โโ Cleanup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove entries older than staleTtlMs.
|
|
116
|
+
* Calls onExpired callback for each removed entry (e.g., to edit the Telegram message).
|
|
117
|
+
*/
|
|
118
|
+
cleanStale(): number {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
let removed = 0;
|
|
121
|
+
|
|
122
|
+
for (const [id, entry] of this.pending) {
|
|
123
|
+
if (now - entry.sentAt > this.staleTtlMs) {
|
|
124
|
+
this.pending.delete(id);
|
|
125
|
+
removed++;
|
|
126
|
+
this.log?.debug?.(
|
|
127
|
+
`[approval-store] purged stale: ${id.slice(0, 8)}โฆ (age=${Math.floor((now - entry.sentAt) / 1000)}s)`,
|
|
128
|
+
);
|
|
129
|
+
try {
|
|
130
|
+
this.onExpired?.(entry);
|
|
131
|
+
} catch {
|
|
132
|
+
// Non-critical โ just log and continue
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (removed > 0) {
|
|
138
|
+
this.log?.info(`[approval-store] cleaned ${removed} stale entries`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return removed;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท lib/diagnostics.ts
|
|
3
|
+
// Self-diagnostics: config validation, connectivity check, auto-repair
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
import type { HealthCheck, Logger, PluginConfig, ResolvedConfig } from "../types.js";
|
|
7
|
+
import type { TelegramApi } from "./telegram-api.js";
|
|
8
|
+
import type { ApprovalStore } from "./approval-store.js";
|
|
9
|
+
|
|
10
|
+
// โโโ Config resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
|
|
12
|
+
export interface ConfigSources {
|
|
13
|
+
pluginConfig: PluginConfig;
|
|
14
|
+
telegramChannelConfig: { token?: string; allowFrom?: (string | number)[] };
|
|
15
|
+
env: { TELEGRAM_BOT_TOKEN?: string; TELEGRAM_CHAT_ID?: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve plugin configuration from multiple sources with priority:
|
|
20
|
+
* 1. pluginConfig (explicit config in openclaw.json)
|
|
21
|
+
* 2. channels.telegram (shared Telegram config)
|
|
22
|
+
* 3. Environment variables (fallback)
|
|
23
|
+
*
|
|
24
|
+
* Returns null with diagnostic messages if critical config is missing.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveConfig(
|
|
27
|
+
sources: ConfigSources,
|
|
28
|
+
log: Logger,
|
|
29
|
+
): ResolvedConfig | null {
|
|
30
|
+
const { pluginConfig, telegramChannelConfig, env } = sources;
|
|
31
|
+
|
|
32
|
+
// โโ Bot token resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
33
|
+
const botToken =
|
|
34
|
+
pluginConfig.botToken ||
|
|
35
|
+
telegramChannelConfig.token ||
|
|
36
|
+
env.TELEGRAM_BOT_TOKEN ||
|
|
37
|
+
"";
|
|
38
|
+
|
|
39
|
+
if (!botToken) {
|
|
40
|
+
log.warn(
|
|
41
|
+
"[diagnostics] No bot token found. Check: " +
|
|
42
|
+
"pluginConfig.botToken โ channels.telegram.token โ TELEGRAM_BOT_TOKEN env",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// โโ Chat ID resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
|
+
let chatId = pluginConfig.chatId || env.TELEGRAM_CHAT_ID || "";
|
|
48
|
+
|
|
49
|
+
// Auto-repair: extract from allowFrom if not explicitly set
|
|
50
|
+
if (!chatId && Array.isArray(telegramChannelConfig.allowFrom)) {
|
|
51
|
+
const first = telegramChannelConfig.allowFrom[0];
|
|
52
|
+
const candidate = String(first ?? "");
|
|
53
|
+
if (/^-?\d+$/.test(candidate)) {
|
|
54
|
+
chatId = candidate;
|
|
55
|
+
log.info(
|
|
56
|
+
`[diagnostics] Auto-resolved chatId from channels.telegram.allowFrom: ${chatId}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!chatId) {
|
|
62
|
+
log.warn(
|
|
63
|
+
"[diagnostics] No chatId found. Set pluginConfig.chatId, " +
|
|
64
|
+
"TELEGRAM_CHAT_ID env, or channels.telegram.allowFrom",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// โโ Missing critical config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
69
|
+
if (!botToken || !chatId) {
|
|
70
|
+
log.error("[diagnostics] Plugin disabled due to missing configuration");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// โโ Optional config with defaults โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
75
|
+
const staleMins =
|
|
76
|
+
typeof pluginConfig.staleMins === "number" && pluginConfig.staleMins > 0
|
|
77
|
+
? pluginConfig.staleMins
|
|
78
|
+
: 10;
|
|
79
|
+
|
|
80
|
+
const verbose = pluginConfig.verbose === true;
|
|
81
|
+
|
|
82
|
+
return { chatId, botToken, staleMins, verbose };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// โโโ Health check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run a full health check: config validation + Telegram connectivity + store stats.
|
|
89
|
+
*/
|
|
90
|
+
export async function runHealthCheck(
|
|
91
|
+
config: ResolvedConfig | null,
|
|
92
|
+
tg: TelegramApi | null,
|
|
93
|
+
store: ApprovalStore,
|
|
94
|
+
startedAt: number,
|
|
95
|
+
): Promise<HealthCheck> {
|
|
96
|
+
const health: HealthCheck = {
|
|
97
|
+
ok: false,
|
|
98
|
+
config: {
|
|
99
|
+
chatId: !!config?.chatId,
|
|
100
|
+
botToken: !!config?.botToken,
|
|
101
|
+
},
|
|
102
|
+
telegram: { reachable: false },
|
|
103
|
+
store: {
|
|
104
|
+
pending: store.pendingCount,
|
|
105
|
+
totalProcessed: store.processedCount,
|
|
106
|
+
},
|
|
107
|
+
uptime: Date.now() - startedAt,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Config check
|
|
111
|
+
if (!config || !tg) {
|
|
112
|
+
health.telegram.error = "not configured";
|
|
113
|
+
return health;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Telegram connectivity check
|
|
117
|
+
const me = await tg.getMe();
|
|
118
|
+
if (me.ok) {
|
|
119
|
+
health.telegram.reachable = true;
|
|
120
|
+
health.telegram.botUsername = me.username;
|
|
121
|
+
health.ok = true;
|
|
122
|
+
} else {
|
|
123
|
+
health.telegram.error = me.error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return health;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// โโโ Startup diagnostics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Log startup diagnostic summary.
|
|
133
|
+
*/
|
|
134
|
+
export function logStartupDiagnostics(
|
|
135
|
+
config: ResolvedConfig,
|
|
136
|
+
log: Logger,
|
|
137
|
+
): void {
|
|
138
|
+
const maskedToken = config.botToken.slice(0, 6) + "โฆ" + config.botToken.slice(-4);
|
|
139
|
+
const maskedChatId = config.chatId.slice(0, 3) + "โฆ" + config.chatId.slice(-2);
|
|
140
|
+
log.info(
|
|
141
|
+
`[diagnostics] Config OK โ chatId=${maskedChatId}, ` +
|
|
142
|
+
`token=${maskedToken}, staleMins=${config.staleMins}, ` +
|
|
143
|
+
`verbose=${config.verbose}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run async startup checks (non-blocking).
|
|
149
|
+
* Verifies Telegram API connectivity and logs the result.
|
|
150
|
+
*/
|
|
151
|
+
export async function runStartupChecks(
|
|
152
|
+
tg: TelegramApi,
|
|
153
|
+
log: Logger,
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const me = await tg.getMe();
|
|
156
|
+
if (me.ok) {
|
|
157
|
+
log.info(`[diagnostics] Telegram connected โ @${me.username}`);
|
|
158
|
+
} else {
|
|
159
|
+
log.warn(
|
|
160
|
+
`[diagnostics] Telegram unreachable: ${me.error}. ` +
|
|
161
|
+
"Plugin will still attempt to send messages.",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท lib/message-formatter.ts
|
|
3
|
+
// HTML message formatting for Telegram (approval requests & resolutions)
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
import type { ApprovalAction, ApprovalInfo } from "../types.js";
|
|
7
|
+
|
|
8
|
+
// โโโ HTML escaping โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
9
|
+
|
|
10
|
+
/** Escape text for Telegram HTML parse mode. */
|
|
11
|
+
export function escapeHtml(text: string): string {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// โโโ Approval request format โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format an approval request as a rich HTML message for Telegram.
|
|
22
|
+
*/
|
|
23
|
+
export function formatApprovalRequest(info: ApprovalInfo): string {
|
|
24
|
+
const e = escapeHtml;
|
|
25
|
+
return [
|
|
26
|
+
`๐ <b>Exec Approval Request</b>`,
|
|
27
|
+
``,
|
|
28
|
+
`๐ค Agent: <b>${e(info.agent)}</b>`,
|
|
29
|
+
`๐ฅ๏ธ Host: <b>${e(info.host)}</b>`,
|
|
30
|
+
`๐ CWD: <code>${e(info.cwd)}</code>`,
|
|
31
|
+
``,
|
|
32
|
+
`<pre>${e(info.command)}</pre>`,
|
|
33
|
+
``,
|
|
34
|
+
`๐ก๏ธ Security: ${e(info.security)}`,
|
|
35
|
+
`โ Ask: ${e(info.ask)}`,
|
|
36
|
+
`โฑ๏ธ Expires: ${e(info.expires)}`,
|
|
37
|
+
`๐ <code>${e(info.id)}</code>`,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// โโโ Resolved approval format โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
42
|
+
|
|
43
|
+
const ACTION_ICONS: Record<ApprovalAction, string> = {
|
|
44
|
+
"allow-once": "โ
",
|
|
45
|
+
"allow-always": "๐",
|
|
46
|
+
deny: "โ",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ACTION_LABELS: Record<ApprovalAction, string> = {
|
|
50
|
+
"allow-once": "Allowed (once)",
|
|
51
|
+
"allow-always": "Always allowed",
|
|
52
|
+
deny: "Denied",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a resolved approval (post-decision) as an HTML message.
|
|
57
|
+
* Buttons are removed and the header shows the resolution.
|
|
58
|
+
*/
|
|
59
|
+
export function formatApprovalResolved(
|
|
60
|
+
info: ApprovalInfo,
|
|
61
|
+
action: ApprovalAction,
|
|
62
|
+
): string {
|
|
63
|
+
const e = escapeHtml;
|
|
64
|
+
const icon = ACTION_ICONS[action] ?? "โ
";
|
|
65
|
+
const label = ACTION_LABELS[action] ?? action;
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
`${icon} <b>Exec ${label}</b>`,
|
|
69
|
+
``,
|
|
70
|
+
`๐ค Agent: <b>${e(info.agent)}</b>`,
|
|
71
|
+
`๐ฅ๏ธ Host: <b>${e(info.host)}</b>`,
|
|
72
|
+
`๐ CWD: <code>${e(info.cwd)}</code>`,
|
|
73
|
+
``,
|
|
74
|
+
`<pre>${e(info.command)}</pre>`,
|
|
75
|
+
``,
|
|
76
|
+
`๐ <code>${e(info.id)}</code>`,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// โโโ Inline keyboard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the inline keyboard markup for an approval request.
|
|
84
|
+
*
|
|
85
|
+
* Each button uses `/approve <id> <action>` as callback_data.
|
|
86
|
+
* OpenClaw's Telegram integration converts unknown callback_data
|
|
87
|
+
* into synthetic text messages, so these are processed as commands
|
|
88
|
+
* automatically โ no webhook needed.
|
|
89
|
+
*/
|
|
90
|
+
export function buildApprovalKeyboard(approvalId: string): object {
|
|
91
|
+
return {
|
|
92
|
+
inline_keyboard: [
|
|
93
|
+
[
|
|
94
|
+
{ text: "โ
Allow Once", callback_data: `/approve ${approvalId} allow-once` },
|
|
95
|
+
{ text: "๐ Always", callback_data: `/approve ${approvalId} allow-always` },
|
|
96
|
+
],
|
|
97
|
+
[{ text: "โ Deny", callback_data: `/approve ${approvalId} deny` }],
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// โโโ Stale approval format โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Format a stale/expired approval message.
|
|
106
|
+
*/
|
|
107
|
+
export function formatApprovalExpired(info: ApprovalInfo): string {
|
|
108
|
+
const e = escapeHtml;
|
|
109
|
+
return [
|
|
110
|
+
`โฐ <b>Exec Approval Expired</b>`,
|
|
111
|
+
``,
|
|
112
|
+
`๐ค Agent: <b>${e(info.agent)}</b>`,
|
|
113
|
+
`๐ฅ๏ธ Host: <b>${e(info.host)}</b>`,
|
|
114
|
+
``,
|
|
115
|
+
`<pre>${e(info.command)}</pre>`,
|
|
116
|
+
``,
|
|
117
|
+
`๐ <code>${e(info.id)}</code>`,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// โโโ Health / diagnostics format โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format a health check result for display in Telegram.
|
|
125
|
+
*/
|
|
126
|
+
export function formatHealthCheck(health: {
|
|
127
|
+
ok: boolean;
|
|
128
|
+
config: { chatId: boolean; botToken: boolean };
|
|
129
|
+
telegram: { reachable: boolean; botUsername?: string; error?: string };
|
|
130
|
+
store: { pending: number; totalProcessed: number };
|
|
131
|
+
uptime: number;
|
|
132
|
+
}): string {
|
|
133
|
+
const uptimeMin = Math.floor(health.uptime / 60_000);
|
|
134
|
+
const lines = [
|
|
135
|
+
`${health.ok ? "๐ข" : "๐ด"} Approval Buttons Status`,
|
|
136
|
+
``,
|
|
137
|
+
`Config: chatId=${health.config.chatId ? "โ" : "โ"} ยท token=${health.config.botToken ? "โ" : "โ"}`,
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
if (health.telegram.reachable) {
|
|
141
|
+
lines.push(`Telegram: โ connected (@${health.telegram.botUsername ?? "?"})`);
|
|
142
|
+
} else {
|
|
143
|
+
lines.push(`Telegram: โ ${health.telegram.error ?? "unreachable"}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lines.push(
|
|
147
|
+
`Pending: ${health.store.pending} ยท Processed: ${health.store.totalProcessed}`,
|
|
148
|
+
`Uptime: ${uptimeMin}m`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท lib/telegram-api.ts
|
|
3
|
+
// Isolated Telegram Bot API wrapper โ only depends on fetch (Node built-in)
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
import type { Logger } from "../types.js";
|
|
7
|
+
|
|
8
|
+
const API_BASE = "https://api.telegram.org/bot";
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
// โโโ Internal helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
12
|
+
|
|
13
|
+
interface TgResponse<T = unknown> {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
result?: T;
|
|
16
|
+
description?: string;
|
|
17
|
+
error_code?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function tgFetch<T = unknown>(
|
|
21
|
+
token: string,
|
|
22
|
+
method: string,
|
|
23
|
+
body: Record<string, unknown>,
|
|
24
|
+
log?: Logger,
|
|
25
|
+
): Promise<TgResponse<T>> {
|
|
26
|
+
const url = `${API_BASE}${token}/${method}`;
|
|
27
|
+
try {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
30
|
+
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
|
|
40
|
+
const data = (await res.json()) as TgResponse<T>;
|
|
41
|
+
if (!data.ok && log) {
|
|
42
|
+
log.warn(
|
|
43
|
+
`[telegram-api] ${method} failed: ${data.error_code} ${data.description}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
log?.error(`[telegram-api] ${method} network error: ${msg}`);
|
|
50
|
+
return { ok: false, description: msg };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// โโโ Public API โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Telegram Bot API client.
|
|
58
|
+
* Instantiate with a bot token; all methods are self-contained.
|
|
59
|
+
*/
|
|
60
|
+
export class TelegramApi {
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly token: string,
|
|
63
|
+
private readonly log?: Logger,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
// โโ Connectivity โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Call getMe to verify the bot token and get bot info.
|
|
70
|
+
* Useful for diagnostics.
|
|
71
|
+
*/
|
|
72
|
+
async getMe(): Promise<{ ok: true; username: string } | { ok: false; error: string }> {
|
|
73
|
+
const res = await tgFetch<{ username: string }>(
|
|
74
|
+
this.token,
|
|
75
|
+
"getMe",
|
|
76
|
+
{},
|
|
77
|
+
this.log,
|
|
78
|
+
);
|
|
79
|
+
if (res.ok && res.result?.username) {
|
|
80
|
+
return { ok: true, username: res.result.username };
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, error: res.description ?? "unknown error" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// โโ Messaging โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Send an HTML-formatted message, optionally with inline keyboard.
|
|
89
|
+
* Returns the message_id on success, null on failure.
|
|
90
|
+
*/
|
|
91
|
+
async sendMessage(
|
|
92
|
+
chatId: string,
|
|
93
|
+
text: string,
|
|
94
|
+
replyMarkup?: object,
|
|
95
|
+
): Promise<number | null> {
|
|
96
|
+
const body: Record<string, unknown> = {
|
|
97
|
+
chat_id: chatId,
|
|
98
|
+
text,
|
|
99
|
+
parse_mode: "HTML",
|
|
100
|
+
};
|
|
101
|
+
if (replyMarkup) body.reply_markup = replyMarkup;
|
|
102
|
+
|
|
103
|
+
const res = await tgFetch<{ message_id: number }>(
|
|
104
|
+
this.token,
|
|
105
|
+
"sendMessage",
|
|
106
|
+
body,
|
|
107
|
+
this.log,
|
|
108
|
+
);
|
|
109
|
+
return res.ok ? (res.result?.message_id ?? null) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Edit an existing message's text and remove inline keyboard.
|
|
114
|
+
* Returns true on success.
|
|
115
|
+
*/
|
|
116
|
+
async editMessageText(
|
|
117
|
+
chatId: string,
|
|
118
|
+
messageId: number,
|
|
119
|
+
text: string,
|
|
120
|
+
replyMarkup?: object,
|
|
121
|
+
): Promise<boolean> {
|
|
122
|
+
const body: Record<string, unknown> = {
|
|
123
|
+
chat_id: chatId,
|
|
124
|
+
message_id: messageId,
|
|
125
|
+
text,
|
|
126
|
+
parse_mode: "HTML",
|
|
127
|
+
};
|
|
128
|
+
// If replyMarkup is explicitly provided, include it; otherwise omit
|
|
129
|
+
// (omitting reply_markup removes all buttons)
|
|
130
|
+
if (replyMarkup) body.reply_markup = replyMarkup;
|
|
131
|
+
|
|
132
|
+
const res = await tgFetch(this.token, "editMessageText", body, this.log);
|
|
133
|
+
return res.ok;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Answer a callback query (acknowledges button press in Telegram UI).
|
|
138
|
+
* Optional text shows as a toast notification to the user.
|
|
139
|
+
*/
|
|
140
|
+
async answerCallbackQuery(
|
|
141
|
+
callbackQueryId: string,
|
|
142
|
+
text?: string,
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
const body: Record<string, unknown> = {
|
|
145
|
+
callback_query_id: callbackQueryId,
|
|
146
|
+
};
|
|
147
|
+
if (text) body.text = text;
|
|
148
|
+
|
|
149
|
+
const res = await tgFetch(this.token, "answerCallbackQuery", body, this.log);
|
|
150
|
+
return res.ok;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Delete a message from a chat.
|
|
155
|
+
*/
|
|
156
|
+
async deleteMessage(chatId: string, messageId: number): Promise<boolean> {
|
|
157
|
+
const res = await tgFetch(
|
|
158
|
+
this.token,
|
|
159
|
+
"deleteMessage",
|
|
160
|
+
{ chat_id: chatId, message_id: messageId },
|
|
161
|
+
this.log,
|
|
162
|
+
);
|
|
163
|
+
return res.ok;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "telegram-approval-buttons",
|
|
3
|
+
"name": "Telegram Approval Buttons",
|
|
4
|
+
"description": "Adds inline keyboard buttons to exec approval messages in Telegram. Tap to approve/deny without typing commands.",
|
|
5
|
+
"version": "4.0.1",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"chatId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Telegram chat ID where approval buttons are sent. Falls back to channels.telegram.allowFrom[0]."
|
|
13
|
+
},
|
|
14
|
+
"botToken": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Telegram bot token. Falls back to channels.telegram.token or TELEGRAM_BOT_TOKEN env."
|
|
17
|
+
},
|
|
18
|
+
"staleMins": {
|
|
19
|
+
"type": "number",
|
|
20
|
+
"description": "Minutes before a pending approval is considered stale and cleaned up.",
|
|
21
|
+
"default": 10
|
|
22
|
+
},
|
|
23
|
+
"verbose": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"description": "Enable verbose diagnostic logging.",
|
|
26
|
+
"default": false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"uiHints": {
|
|
31
|
+
"chatId": {
|
|
32
|
+
"label": "Telegram Chat ID",
|
|
33
|
+
"placeholder": "123456789",
|
|
34
|
+
"help": "Auto-detected from channels.telegram.allowFrom if not set."
|
|
35
|
+
},
|
|
36
|
+
"botToken": {
|
|
37
|
+
"label": "Bot Token",
|
|
38
|
+
"sensitive": true,
|
|
39
|
+
"placeholder": "Auto-detected from channels.telegram.token",
|
|
40
|
+
"help": "Only needed if different from the main Telegram channel token."
|
|
41
|
+
},
|
|
42
|
+
"staleMins": {
|
|
43
|
+
"label": "Stale Timeout (min)",
|
|
44
|
+
"placeholder": "10",
|
|
45
|
+
"advanced": true
|
|
46
|
+
},
|
|
47
|
+
"verbose": {
|
|
48
|
+
"label": "Verbose Logging",
|
|
49
|
+
"advanced": true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "telegram-approval-buttons",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "Inline keyboard buttons for exec approval messages in Telegram โ tap to approve/deny without typing commands",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:watch": "vitest"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"openclaw-plugin",
|
|
15
|
+
"telegram",
|
|
16
|
+
"approval",
|
|
17
|
+
"exec",
|
|
18
|
+
"inline-keyboard",
|
|
19
|
+
"buttons"
|
|
20
|
+
],
|
|
21
|
+
"openclaw": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"author": "Jair Franco (https://github.com/JairFC)",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/JairFC/openclaw-telegram-approval-buttons"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.ts",
|
|
33
|
+
"types.ts",
|
|
34
|
+
"lib/",
|
|
35
|
+
"openclaw.plugin.json",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.2.3",
|
|
41
|
+
"typescript": "^5.7.0",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2
|
+
// telegram-approval-buttons ยท types.ts
|
|
3
|
+
// Shared TypeScript interfaces for the plugin
|
|
4
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parsed representation of an OpenClaw exec approval request.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApprovalInfo {
|
|
10
|
+
/** UUID of the approval request */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Shell command requiring approval */
|
|
13
|
+
command: string;
|
|
14
|
+
/** Working directory where the command would execute */
|
|
15
|
+
cwd: string;
|
|
16
|
+
/** Host machine name */
|
|
17
|
+
host: string;
|
|
18
|
+
/** Agent that requested the command */
|
|
19
|
+
agent: string;
|
|
20
|
+
/** Security policy that triggered the approval */
|
|
21
|
+
security: string;
|
|
22
|
+
/** Ask mode (e.g., "on-miss", "always") */
|
|
23
|
+
ask: string;
|
|
24
|
+
/** Time until approval expires (e.g., "120s") */
|
|
25
|
+
expires: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A tracked approval that was sent with inline buttons.
|
|
30
|
+
*/
|
|
31
|
+
export interface SentApproval {
|
|
32
|
+
/** Telegram message_id of our formatted message */
|
|
33
|
+
messageId: number;
|
|
34
|
+
/** Parsed approval details */
|
|
35
|
+
info: ApprovalInfo;
|
|
36
|
+
/** Unix timestamp (ms) when the message was sent */
|
|
37
|
+
sentAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolution of an approval (allow-once, allow-always, deny).
|
|
42
|
+
*/
|
|
43
|
+
export type ApprovalAction = "allow-once" | "allow-always" | "deny";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Result of detecting an approval resolution in a message.
|
|
47
|
+
*/
|
|
48
|
+
export interface ApprovalResolution {
|
|
49
|
+
/** Approval UUID */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Action that was taken */
|
|
52
|
+
action: ApprovalAction;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Plugin configuration (from openclaw.json โ plugins.entries.<id>.config).
|
|
57
|
+
*/
|
|
58
|
+
export interface PluginConfig {
|
|
59
|
+
/** Telegram chat ID to send approval buttons to */
|
|
60
|
+
chatId?: string;
|
|
61
|
+
/** Telegram bot token (optional โ falls back to channels.telegram.token) */
|
|
62
|
+
botToken?: string;
|
|
63
|
+
/** Stale approval timeout in minutes (default: 10) */
|
|
64
|
+
staleMins?: number;
|
|
65
|
+
/** Enable verbose diagnostic logging (default: false) */
|
|
66
|
+
verbose?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolved (validated) configuration with all defaults applied.
|
|
71
|
+
*/
|
|
72
|
+
export interface ResolvedConfig {
|
|
73
|
+
chatId: string;
|
|
74
|
+
botToken: string;
|
|
75
|
+
staleMins: number;
|
|
76
|
+
verbose: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Diagnostic health check result.
|
|
81
|
+
*/
|
|
82
|
+
export interface HealthCheck {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
config: { chatId: boolean; botToken: boolean };
|
|
85
|
+
telegram: { reachable: boolean; botUsername?: string; error?: string };
|
|
86
|
+
store: { pending: number; totalProcessed: number };
|
|
87
|
+
uptime: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Minimal logger interface matching OpenClaw's plugin logger.
|
|
92
|
+
*/
|
|
93
|
+
export interface Logger {
|
|
94
|
+
debug?: (message: string) => void;
|
|
95
|
+
info: (message: string) => void;
|
|
96
|
+
warn: (message: string) => void;
|
|
97
|
+
error: (message: string) => void;
|
|
98
|
+
}
|