arena-bridge 0.7.4 → 0.7.6
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 +25 -0
- package/README.md +53 -9
- package/package.json +1 -1
- package/src/approvals.mjs +22 -2
- package/src/cli.mjs +13 -0
- package/src/direct.mjs +25 -4
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +12 -0
- package/src/mcp.mjs +1 -1
- package/src/telegram.mjs +65 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.7.6
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Release artifacts: every tag publishes the npm archive, the python sdist and wheel, and the
|
|
11
|
+
macOS app for arm64 and x86_64 as a GitHub Release.
|
|
12
|
+
- `install.sh`: one command installs the command line tool and the macOS app.
|
|
13
|
+
- `feature-tree.md`: the complete feature tree of the product.
|
|
14
|
+
- The ax-tree artifact opens in the app as a readable list of elements, with a switch to the raw
|
|
15
|
+
answer of the tool.
|
|
16
|
+
- `mac_see` asks for the accessibility tree on its own when the first answer has no elements.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- The README says in one line how to get past Gatekeeper.
|
|
20
|
+
|
|
21
|
+
## 0.7.5
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Approvals in Telegram carry native buttons: Approve once, Approve for 3h, Deny. "Approve for 3h"
|
|
25
|
+
also opens a trust window for the same tool.
|
|
26
|
+
- `arena-bridge trust --status --json` prints the trust window as JSON.
|
|
27
|
+
- The native macOS app: a trust window status with the time left, an "Approve for 3 h" button,
|
|
28
|
+
local notifications when a request needs a decision, the command arguments in the work log,
|
|
29
|
+
a click-through to screenshots and accessibility trees, and a button that copies the agent prompt.
|
|
30
|
+
- The app icon: `macos/ArenaBridge/Resources/AppIcon.png`, turned into an `.icns` by `build.sh`.
|
|
31
|
+
|
|
7
32
|
## 0.7.4
|
|
8
33
|
|
|
9
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
14
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square" alt="dependencies">
|
|
15
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-154%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
## Что это
|
|
25
25
|
|
|
26
|
+
Полное дерево функциональности — в [feature-tree.md](feature-tree.md).
|
|
27
|
+
|
|
26
28
|
Облачный агент (Arena.ai Agent Mode, Claude, Cursor, любой MCP-клиент) работает в песочнице
|
|
27
29
|
с выходом в интернет, но не имеет доступа к вашему компьютеру. `arena-bridge` — это маленький
|
|
28
30
|
HTTP-сервис на вашей машине: он слушает только localhost, выставляется наружу туннелем
|
|
@@ -41,16 +43,31 @@ HTTP-сервис на вашей машине: он слушает только
|
|
|
41
43
|
|
|
42
44
|
## Быстрый старт
|
|
43
45
|
|
|
46
|
+
Одна команда ставит и командную строку, и нативное приложение для macOS:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -fsSL https://raw.githubusercontent.com/vernikr/arena-bridge/main/install.sh | sh
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Или по отдельности:
|
|
53
|
+
|
|
44
54
|
```bash
|
|
45
|
-
#
|
|
46
|
-
|
|
55
|
+
npm i -g arena-bridge # только командная строка
|
|
56
|
+
pip install arena-bridge # то же самое на Python
|
|
57
|
+
```
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
```bash
|
|
60
|
+
# 1. один раз: создать конфиг, проверить машину, поднять API + туннель
|
|
49
61
|
arena-bridge setup --root ~/projects --root ~/Documents
|
|
50
62
|
|
|
51
|
-
#
|
|
63
|
+
# 2. скопировать блок «Paste this into the agent chat» и вставить его в чат с агентом
|
|
52
64
|
```
|
|
53
65
|
|
|
66
|
+
> **macOS и Gatekeeper.** Приложение подписано локально, поэтому при первом запуске система может
|
|
67
|
+
> сказать «нельзя открыть, так как разработчик не проверен». Обойти это можно без терминала:
|
|
68
|
+
> правый клик по **Arena Bridge** в «Программах» → **Открыть** → **Открыть**. Через терминал:
|
|
69
|
+
> `xattr -dr com.apple.quarantine "/Applications/Arena Bridge.app"`.
|
|
70
|
+
|
|
54
71
|
`setup` печатает готовый промпт и публичный URL. Дальше — просто говорите агенту задачу.
|
|
55
72
|
|
|
56
73
|
Туннель под присмотром: если `cloudflared`/`ngrok` упадёт, мост поднимет его заново и сам
|
|
@@ -275,14 +292,40 @@ Apple Silicon.
|
|
|
275
292
|
|
|
276
293
|
| Блок в окне | Что делает |
|
|
277
294
|
| --- | --- |
|
|
278
|
-
| Статус | запущен/остановлен, версия, режим, число инструментов, публичный адрес, Telegram |
|
|
279
|
-
| Управление | «Запустить» / «Остановить» (служба `ai.arena.bridge`)
|
|
280
|
-
|
|
|
281
|
-
|
|
|
295
|
+
| Статус | запущен/остановлен, версия, режим, число инструментов, публичный адрес, уведомления, Telegram |
|
|
296
|
+
| Управление | «Запустить» / «Остановить» (служба `ai.arena.bridge`), «Настроить…», «Скопировать промпт агенту» |
|
|
297
|
+
| Доверенное окно | «Открыть на 3 ч» / «Закрыть» и статус «действует ещё 2:59» |
|
|
298
|
+
| Подтверждения | ожидающие запросы агента с кнопками «Разрешить» / «Разрешить на 3 ч» / «Отклонить» |
|
|
299
|
+
| Журнал | команды с аргументами, клик по скриншоту или ax-tree открывает его целиком, «Сохранить…» в файл (с ax-tree) |
|
|
300
|
+
|
|
301
|
+
Когда агенту нужно решение, приложение показывает системное уведомление macOS
|
|
302
|
+
«Approval needed» — в строке меню и в центре уведомлений. Внутри окна запрос тоже виден
|
|
303
|
+
сразу, в карточке «Подтверждения».
|
|
282
304
|
|
|
283
305
|
«Настроить…» собирает конфиг, службу и запуск одной кнопкой: туннель (ngrok/cloudflared),
|
|
284
306
|
зарезервированный домен, authtoken, папки для агента, Telegram (токен бота и chat id).
|
|
285
307
|
|
|
308
|
+
Иконка приложения лежит в `macos/ArenaBridge/Resources/AppIcon.png`; `build.sh` сам собирает
|
|
309
|
+
из неё `.icns` и кладёт в бандл.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Подтверждения в Telegram: кнопки
|
|
314
|
+
|
|
315
|
+
Под сообщением «Approval needed» появляются нативные кнопки:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
Approve once Approve for 3h Deny
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
* **Approve once** — разрешить этот запрос и всё;
|
|
322
|
+
* **Approve for 3h** — разрешить и открыть доверенное окно на 3 часа для этого же инструмента
|
|
323
|
+
(команды больше не спрашивают);
|
|
324
|
+
* **Deny** — отклонить.
|
|
325
|
+
|
|
326
|
+
Кнопки работают и в приложении на Mac, и в Telegram; сообщение в Telegram после нажатия
|
|
327
|
+
переписывается на итог. Прежние команды `/approve <id>` и `/deny <id>` тоже работают.
|
|
328
|
+
|
|
286
329
|
---
|
|
287
330
|
|
|
288
331
|
## Доверенное окно: работать без подтверждений
|
|
@@ -292,6 +335,7 @@ arena-bridge trust --for 2h # всё, на 2
|
|
|
292
335
|
arena-bridge trust --for 30m --tools bash # только команды
|
|
293
336
|
arena-bridge trust --for 1h --paths ~/projects/my-app # только эта папка
|
|
294
337
|
arena-bridge trust --status
|
|
338
|
+
arena-bridge trust --status --json # для приложений и скриптов
|
|
295
339
|
arena-bridge trust --off
|
|
296
340
|
```
|
|
297
341
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Give a cloud AI agent (Arena.ai Agent Mode, Claude, Cursor, ...) controlled access to your own machine over HTTP/MCP: files, shell, Chrome and the macOS desktop, with an approval gate, trust windows and an audit log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/approvals.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { APPROVALS_DIR, ensureDirs } from './config.mjs';
|
|
5
|
-
import { sendTelegram } from './telegram.mjs';
|
|
5
|
+
import { sendTelegram, approvalKeyboard } from './telegram.mjs';
|
|
6
|
+
import { saveTrust } from './trust.mjs';
|
|
6
7
|
|
|
7
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8
9
|
|
|
@@ -43,6 +44,24 @@ export function decide(id, decision, by = 'cli') {
|
|
|
43
44
|
return { ok: true, approval: rec };
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Settle a request and, when asked, open a trust window for the same tool.
|
|
49
|
+
*/
|
|
50
|
+
export function settle(id, decision, by = 'cli', { trustSec = 0 } = {}) {
|
|
51
|
+
const res = decide(id, decision, by);
|
|
52
|
+
if (res.ok && trustSec > 0) {
|
|
53
|
+
const tool = res.approval?.tool;
|
|
54
|
+
saveTrust({
|
|
55
|
+
enabled: true,
|
|
56
|
+
until: Date.now() + trustSec * 1000,
|
|
57
|
+
tools: tool ? [tool] : [],
|
|
58
|
+
paths: [],
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return res;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
function redact(args) {
|
|
47
66
|
const clone = { ...args };
|
|
48
67
|
for (const k of Object.keys(clone)) {
|
|
@@ -82,7 +101,8 @@ export async function requestApproval(tool, args, cfg) {
|
|
|
82
101
|
text:
|
|
83
102
|
`Tool: ${tool}\n` +
|
|
84
103
|
`id: ${id}\n` +
|
|
85
|
-
`
|
|
104
|
+
`Use the buttons below, reply /approve ${id} or /deny ${id}, or run: arena-bridge approve ${id}`,
|
|
105
|
+
buttons: approvalKeyboard(id),
|
|
86
106
|
}).catch(() => {});
|
|
87
107
|
}
|
|
88
108
|
|
package/src/cli.mjs
CHANGED
|
@@ -359,6 +359,19 @@ function cmdTrust(args) {
|
|
|
359
359
|
return;
|
|
360
360
|
}
|
|
361
361
|
if (args.flags.status || (!args.flags.for && !args.flags.tools && !args.flags.paths)) {
|
|
362
|
+
if (args.flags.json) {
|
|
363
|
+
const remaining = state && state.active ? Math.max(0, Math.round((state.until - Date.now()) / 1000)) : 0;
|
|
364
|
+
console.log(
|
|
365
|
+
JSON.stringify({
|
|
366
|
+
enabled: !!(state && state.active),
|
|
367
|
+
until: state && state.active ? state.until : null,
|
|
368
|
+
remaining_sec: remaining,
|
|
369
|
+
tools: state ? state.tools : [],
|
|
370
|
+
paths: state ? state.paths : [],
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
362
375
|
if (!state || !state.active) {
|
|
363
376
|
console.log('no active trust window (every side-effecting action needs approval)');
|
|
364
377
|
} else {
|
package/src/direct.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import fsp from 'node:fs/promises';
|
|
|
3
3
|
import { sendJson, sendText, sendRaw, readJson, readBody, isAuthorized, bearer, safeEqual } from './http.mjs';
|
|
4
4
|
import { handleMcp } from './mcp.mjs';
|
|
5
5
|
import { buildPrompt } from './prompt.mjs';
|
|
6
|
-
import { listApprovals, decide } from './approvals.mjs';
|
|
6
|
+
import { listApprovals, decide, settle } from './approvals.mjs';
|
|
7
7
|
import { checkPath } from './policy.mjs';
|
|
8
8
|
import { loadConfig } from './config.mjs';
|
|
9
9
|
import { tailAudit, readArtifact, auditDir } from './audit.mjs';
|
|
10
|
-
import { startPoller, parseCommand, sendTelegram } from './telegram.mjs';
|
|
10
|
+
import { startPoller, parseCommand, sendTelegram, answerCallback, editMessageText } from './telegram.mjs';
|
|
11
11
|
|
|
12
12
|
export function createDirectServer(cfg, engine) {
|
|
13
13
|
const mcpCtx = {
|
|
@@ -37,6 +37,27 @@ export function createDirectServer(cfg, engine) {
|
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
const handleTelegramCallback = async (cb) => {
|
|
41
|
+
const current = loadConfig();
|
|
42
|
+
const [action, id] = String(cb.data || '').split(':');
|
|
43
|
+
if (!id || !['approve', 'approve3h', 'deny'].includes(action)) {
|
|
44
|
+
await answerCallback(current, cb.callback_id, 'Unknown action');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const trustSec = action === 'approve3h' ? 3 * 3600 : 0;
|
|
48
|
+
const res = settle(id, action === 'deny' ? 'deny' : 'approve', 'telegram', { trustSec });
|
|
49
|
+
const tool = res.approval?.tool || 'the tool';
|
|
50
|
+
const text = !res.ok
|
|
51
|
+
? `Cannot ${action}: ${res.error || 'unknown error'}`
|
|
52
|
+
: action === 'deny'
|
|
53
|
+
? `Denied ${id}`
|
|
54
|
+
: trustSec
|
|
55
|
+
? `Approved ${id} — ${tool} is trusted for the next 3 hours`
|
|
56
|
+
: `Approved ${id}`;
|
|
57
|
+
await answerCallback(current, cb.callback_id, res.ok ? 'Done' : 'Error');
|
|
58
|
+
await editMessageText(current, { chatId: cb.chat_id, messageId: cb.message_id, text });
|
|
59
|
+
};
|
|
60
|
+
|
|
40
61
|
const server = http.createServer(async (req, res) => {
|
|
41
62
|
let url;
|
|
42
63
|
try {
|
|
@@ -52,7 +73,7 @@ export function createDirectServer(cfg, engine) {
|
|
|
52
73
|
ok: true,
|
|
53
74
|
mode: 'direct',
|
|
54
75
|
device: cfg.device,
|
|
55
|
-
version: '0.7.
|
|
76
|
+
version: '0.7.6',
|
|
56
77
|
uptime_sec: Math.round(process.uptime()),
|
|
57
78
|
});
|
|
58
79
|
}
|
|
@@ -188,7 +209,7 @@ export function createDirectServer(cfg, engine) {
|
|
|
188
209
|
}
|
|
189
210
|
});
|
|
190
211
|
|
|
191
|
-
const poller = startPoller(cfg, { onMessage: handleTelegramMessage });
|
|
212
|
+
const poller = startPoller(cfg, { onMessage: handleTelegramMessage, onCallback: handleTelegramCallback });
|
|
192
213
|
server.on('close', () => poller?.stop());
|
|
193
214
|
|
|
194
215
|
return server;
|
package/src/hub.mjs
CHANGED
package/src/mac.mjs
CHANGED
|
@@ -218,6 +218,18 @@ export function macTools(cfg = {}) {
|
|
|
218
218
|
const denied = isPermissionError(json, res);
|
|
219
219
|
if (res.exit_code !== 0 && !denied && !UNKNOWN.test(res.stderr || '')) break;
|
|
220
220
|
}
|
|
221
|
+
// no elements yet: ask for the accessibility tree on its own
|
|
222
|
+
if (!best || !best.elements.length) {
|
|
223
|
+
for (const variant of [
|
|
224
|
+
['see', '--tree', '--no-screenshot', '--json', ...targetFlags(args)],
|
|
225
|
+
['see', '--tree', '--no-screenshot', '--no-remote', '--json', ...targetFlags(args)],
|
|
226
|
+
]) {
|
|
227
|
+
const res = await run(peek(), variant, { timeout: 60 });
|
|
228
|
+
const json = parseJson(res.stdout);
|
|
229
|
+
const elements = collectElements(json);
|
|
230
|
+
if (elements.length) return { res, json, elements, variant };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
221
233
|
return best;
|
|
222
234
|
};
|
|
223
235
|
|
package/src/mcp.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function handleMcp(req, res, ctx) {
|
|
|
48
48
|
mcpResponse(id, {
|
|
49
49
|
protocolVersion: PROTOCOL_VERSION,
|
|
50
50
|
capabilities: { tools: { listChanged: false } },
|
|
51
|
-
serverInfo: { name: 'arena-bridge', version: '0.7.
|
|
51
|
+
serverInfo: { name: 'arena-bridge', version: '0.7.6' },
|
|
52
52
|
instructions:
|
|
53
53
|
'Tools operate the user\'s own machine. Dangerous operations may return ' +
|
|
54
54
|
'PROTECTED and require a local human approval.',
|
package/src/telegram.mjs
CHANGED
|
@@ -110,10 +110,23 @@ export function rateOk(limit = RATE_LIMIT, windowMs = RATE_WINDOW_MS) {
|
|
|
110
110
|
return true;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The inline keyboard under an approval message: approve once, approve for 3 hours, deny.
|
|
115
|
+
*/
|
|
116
|
+
export function approvalKeyboard(id) {
|
|
117
|
+
return [
|
|
118
|
+
[
|
|
119
|
+
{ text: 'Approve once', callback_data: `approve:${id}` },
|
|
120
|
+
{ text: 'Approve for 3h', callback_data: `approve3h:${id}` },
|
|
121
|
+
],
|
|
122
|
+
[{ text: 'Deny', callback_data: `deny:${id}` }],
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Send a text message to one of the allowed chats.
|
|
115
128
|
*/
|
|
116
|
-
export async function sendTelegram(cfg, { text, chatId, title, silent } = {}) {
|
|
129
|
+
export async function sendTelegram(cfg, { text, chatId, title, silent, buttons } = {}) {
|
|
117
130
|
const s = telegramSettings(cfg);
|
|
118
131
|
if (!s.enabled) {
|
|
119
132
|
return {
|
|
@@ -151,17 +164,16 @@ export async function sendTelegram(cfg, { text, chatId, title, silent } = {}) {
|
|
|
151
164
|
};
|
|
152
165
|
}
|
|
153
166
|
const text_ = title ? `${title}\n${body}` : body;
|
|
154
|
-
|
|
167
|
+
const markup = buttons && buttons.length ? { inline_keyboard: buttons } : undefined;
|
|
168
|
+
const payload = {
|
|
155
169
|
chat_id: target,
|
|
156
170
|
text: text_,
|
|
157
171
|
disable_notification: !!silent,
|
|
158
|
-
|
|
172
|
+
...(markup ? { reply_markup: markup } : {}),
|
|
173
|
+
};
|
|
174
|
+
let res = await apiCall(cfg, 'sendMessage', payload);
|
|
159
175
|
if (!res.ok && /parse entities|Markdown/i.test(res.reason || '')) {
|
|
160
|
-
res = await apiCall(cfg, 'sendMessage',
|
|
161
|
-
chat_id: target,
|
|
162
|
-
text: text_,
|
|
163
|
-
disable_notification: !!silent,
|
|
164
|
-
});
|
|
176
|
+
res = await apiCall(cfg, 'sendMessage', payload);
|
|
165
177
|
}
|
|
166
178
|
if (!res.ok) return res;
|
|
167
179
|
return {
|
|
@@ -172,6 +184,29 @@ export async function sendTelegram(cfg, { text, chatId, title, silent } = {}) {
|
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Acknowledge a button press so Telegram stops showing the spinner.
|
|
189
|
+
*/
|
|
190
|
+
export async function answerCallback(cfg, callbackId, text = '') {
|
|
191
|
+
if (!callbackId) return { ok: false, error: 'telegram_no_callback' };
|
|
192
|
+
const payload = { callback_query_id: callbackId };
|
|
193
|
+
if (text) payload.text = String(text).slice(0, 200);
|
|
194
|
+
return apiCall(cfg, 'answerCallbackQuery', payload);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Replace the text of a message and drop its keyboard.
|
|
199
|
+
*/
|
|
200
|
+
export async function editMessageText(cfg, { chatId, messageId, text } = {}) {
|
|
201
|
+
if (!chatId || !messageId) return { ok: false, error: 'telegram_no_message' };
|
|
202
|
+
return apiCall(cfg, 'editMessageText', {
|
|
203
|
+
chat_id: chatId,
|
|
204
|
+
message_id: messageId,
|
|
205
|
+
text: String(text ?? ''),
|
|
206
|
+
reply_markup: { inline_keyboard: [] },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
175
210
|
/* ------------------------------------------------------------------ *
|
|
176
211
|
* inbox
|
|
177
212
|
* ------------------------------------------------------------------ */
|
|
@@ -258,7 +293,7 @@ export function parseCommand(text) {
|
|
|
258
293
|
* Poll the bot for incoming messages and hand them to the callback.
|
|
259
294
|
* Returns a handle with stop().
|
|
260
295
|
*/
|
|
261
|
-
export function startPoller(cfg, { onMessage } = {}) {
|
|
296
|
+
export function startPoller(cfg, { onMessage, onCallback } = {}) {
|
|
262
297
|
const s = telegramSettings(cfg);
|
|
263
298
|
if (!s.enabled || !s.poll) return null;
|
|
264
299
|
let stopped = false;
|
|
@@ -282,7 +317,7 @@ export function startPoller(cfg, { onMessage } = {}) {
|
|
|
282
317
|
const res = await apiCall(fresh(), 'getUpdates', {
|
|
283
318
|
offset,
|
|
284
319
|
timeout: 20,
|
|
285
|
-
allowed_updates: ['message'],
|
|
320
|
+
allowed_updates: ['message', 'callback_query'],
|
|
286
321
|
});
|
|
287
322
|
if (!res.ok) {
|
|
288
323
|
const state = readState();
|
|
@@ -301,6 +336,26 @@ export function startPoller(cfg, { onMessage } = {}) {
|
|
|
301
336
|
const state = readState();
|
|
302
337
|
state.offset = offset;
|
|
303
338
|
writeState(state);
|
|
339
|
+
const cb = upd.callback_query;
|
|
340
|
+
if (cb) {
|
|
341
|
+
const item = {
|
|
342
|
+
id: upd.update_id,
|
|
343
|
+
callback_id: cb.id,
|
|
344
|
+
data: cb.data || '',
|
|
345
|
+
chat_id: cb.message?.chat?.id ?? null,
|
|
346
|
+
message_id: cb.message?.message_id ?? null,
|
|
347
|
+
from: cb.from?.username || cb.from?.first_name || 'unknown',
|
|
348
|
+
date: cb.message?.date || null,
|
|
349
|
+
};
|
|
350
|
+
if (onCallback) {
|
|
351
|
+
try {
|
|
352
|
+
await onCallback(item);
|
|
353
|
+
} catch {
|
|
354
|
+
/* the callback must not kill the loop */
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
304
359
|
const msg = upd.message;
|
|
305
360
|
if (!msg) continue;
|
|
306
361
|
const item = {
|