bloby-bot 0.61.0 → 0.62.2
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/dist-bloby/assets/{bloby-DO7g-v11.js → bloby-Dmxp6AFI.js} +6 -6
- package/dist-bloby/assets/globals-CvipyZv-.css +2 -0
- package/dist-bloby/assets/globals-W8wOsf_q.js +26 -0
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-C2Wmb17B.js → highlighted-body-OFNGDK62-mdecRvJB.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-DLezgHEY.js +1 -0
- package/dist-bloby/assets/{onboard-DcGLkITd.js → onboard-D1woNbAz.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +1 -1
- package/shared/config.ts +2 -3
- package/supervisor/channels/manager.ts +5 -6
- package/supervisor/channels/telegram.ts +4 -5
- package/supervisor/chat/OnboardWizard.tsx +18 -11
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +29 -0
- package/supervisor/index.ts +216 -134
- package/workspace/skills/telegram/.claude-plugin/plugin.json +1 -1
- package/workspace/skills/telegram/SKILL.md +12 -13
- package/workspace/skills/telegram/skill.json +1 -1
- package/dist-bloby/assets/globals-CF0bs396.css +0 -2
- package/dist-bloby/assets/globals-CwR3dDCz.js +0 -26
- package/dist-bloby/assets/mermaid-GHXKKRXX-CILe07ZG.js +0 -1
package/supervisor/index.ts
CHANGED
|
@@ -551,8 +551,8 @@ export async function startSupervisor() {
|
|
|
551
551
|
'POST /api/channels/whatsapp/react',
|
|
552
552
|
'POST /api/channels/send',
|
|
553
553
|
'POST /api/channels/alexa/handle',
|
|
554
|
-
'POST /api/channels/telegram/connect',
|
|
555
|
-
|
|
554
|
+
// NOTE: 'POST /api/channels/telegram/connect' is NOT public — it sets the bot token, so it is
|
|
555
|
+
// gated in-handler (first run open, portal token required once a password is set), like /api/onboard.
|
|
556
556
|
'GET /api/channels/telegram/pair-page',
|
|
557
557
|
'GET /api/channels/telegram/status',
|
|
558
558
|
'POST /api/channels/telegram/configure',
|
|
@@ -1361,99 +1361,101 @@ mint();
|
|
|
1361
1361
|
return;
|
|
1362
1362
|
}
|
|
1363
1363
|
|
|
1364
|
-
// ── Telegram (
|
|
1365
|
-
// long-
|
|
1364
|
+
// ── Telegram (BYO bot — the user pastes a @BotFather token; we validate it and
|
|
1365
|
+
// long-poll Telegram DIRECTLY. NO relay anywhere in the pairing or message path) ──
|
|
1366
1366
|
|
|
1367
|
-
// POST /api/channels/telegram/connect —
|
|
1368
|
-
//
|
|
1367
|
+
// POST /api/channels/telegram/connect — { botToken }. Sets the agent's bot token, so it is
|
|
1368
|
+
// GATED like /api/onboard: open only on genuine first run (no portal_pass yet); once a portal
|
|
1369
|
+
// password is set it requires a valid Bearer token (the pair-page sends the dashboard's
|
|
1370
|
+
// bloby_token). Without this gate, any unauthenticated caller over the tunnel could inject
|
|
1371
|
+
// their own bot token and hijack the channel (and, via trust-on-first-use, the agent).
|
|
1369
1372
|
if (req.method === 'POST' && channelPath === '/api/channels/telegram/connect') {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1373
|
+
let body = '';
|
|
1374
|
+
let tooLarge = false;
|
|
1375
|
+
req.on('data', (chunk: Buffer) => {
|
|
1376
|
+
body += chunk.toString();
|
|
1377
|
+
if (body.length > 8_000) { tooLarge = true; req.destroy(); }
|
|
1378
|
+
});
|
|
1379
|
+
req.on('error', () => {
|
|
1380
|
+
if (res.headersSent) return;
|
|
1381
|
+
res.writeHead(400);
|
|
1382
|
+
res.end(JSON.stringify({ ok: false, error: 'read-error' }));
|
|
1383
|
+
});
|
|
1384
|
+
req.on('end', () => {
|
|
1385
|
+
(async () => {
|
|
1386
|
+
try {
|
|
1387
|
+
if (tooLarge) {
|
|
1388
|
+
res.writeHead(413);
|
|
1389
|
+
res.end(JSON.stringify({ ok: false, error: 'body-too-large' }));
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// Auth gate (matches /api/onboard): internal calls bypass; otherwise require a valid
|
|
1393
|
+
// portal token whenever a password is set. First run (no password) stays open.
|
|
1394
|
+
if (req.headers['x-internal'] !== internalSecret && await isAuthRequired()) {
|
|
1395
|
+
const auth = req.headers.authorization;
|
|
1396
|
+
const tok = auth?.startsWith('Bearer ') ? auth.slice(7) : null;
|
|
1397
|
+
if (!tok || !(await validateToken(tok))) {
|
|
1398
|
+
res.writeHead(401);
|
|
1399
|
+
res.end(JSON.stringify({ ok: false, error: 'auth-required' }));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1399
1403
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1404
|
+
const data = JSON.parse(body || '{}');
|
|
1405
|
+
const botToken = typeof data.botToken === 'string' ? data.botToken.trim() : '';
|
|
1406
|
+
if (!botToken || !/^\d{6,}:[A-Za-z0-9_-]{30,}$/.test(botToken)) {
|
|
1407
|
+
res.writeHead(400);
|
|
1408
|
+
res.end(JSON.stringify({ ok: false, error: 'invalid-token-format' }));
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
// Authoritative validation: ask Telegram who this token belongs to (with a timeout
|
|
1412
|
+
// so a hung TLS connection can't wedge the request / the pair modal).
|
|
1413
|
+
let me: any;
|
|
1414
|
+
try {
|
|
1415
|
+
const r = await fetch(`https://api.telegram.org/bot${encodeURIComponent(botToken)}/getMe`, { signal: AbortSignal.timeout(8000) });
|
|
1416
|
+
me = await r.json().catch(() => ({}));
|
|
1417
|
+
} catch {
|
|
1418
|
+
res.writeHead(502);
|
|
1419
|
+
res.end(JSON.stringify({ ok: false, error: 'telegram-unreachable' }));
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (!me || me.ok !== true || !me.result || me.result.is_bot !== true) {
|
|
1423
|
+
res.writeHead(400);
|
|
1424
|
+
res.end(JSON.stringify({ ok: false, error: 'invalid-bot-token' }));
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const botUsername = me.result.username || undefined;
|
|
1428
|
+
const cfg = loadConfig();
|
|
1429
|
+
if (!cfg.channels) cfg.channels = {};
|
|
1430
|
+
// On a token CHANGE (not a same-token reconnect), drop identity-bound fields so
|
|
1431
|
+
// trust-on-first-use re-adopts the owner against the NEW bot — otherwise a stale
|
|
1432
|
+
// ownerUserId/admins would silently lock out the new operator.
|
|
1433
|
+
const prev = cfg.channels.telegram;
|
|
1434
|
+
const tokenChanged = !!prev?.botToken && prev.botToken !== botToken;
|
|
1435
|
+
cfg.channels.telegram = {
|
|
1436
|
+
...(prev || {}),
|
|
1437
|
+
enabled: true,
|
|
1438
|
+
mode: prev?.mode || 'channel',
|
|
1439
|
+
botToken,
|
|
1440
|
+
botUsername,
|
|
1441
|
+
...(tokenChanged ? { ownerUserId: undefined, admins: undefined } : {}),
|
|
1442
|
+
};
|
|
1443
|
+
saveConfig(cfg);
|
|
1444
|
+
// Re-connecting with a new token: tear down any existing provider so init()
|
|
1445
|
+
// reconnects with the new token (init() is a no-op when a provider already exists).
|
|
1446
|
+
await channelManager.disconnectChannel('telegram').catch(() => {});
|
|
1447
|
+
await channelManager.init().catch(() => {});
|
|
1427
1448
|
res.writeHead(200);
|
|
1428
|
-
res.end(JSON.stringify({ ok: true,
|
|
1429
|
-
|
|
1449
|
+
res.end(JSON.stringify({ ok: true, botUsername }));
|
|
1450
|
+
} catch (err: any) {
|
|
1451
|
+
log.warn(`[telegram/connect] ${err.message}`);
|
|
1452
|
+
if (!res.headersSent) {
|
|
1453
|
+
res.writeHead(500);
|
|
1454
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1455
|
+
}
|
|
1430
1456
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
const fresh = loadConfig();
|
|
1434
|
-
if (!fresh.channels) fresh.channels = {};
|
|
1435
|
-
fresh.channels.telegram = {
|
|
1436
|
-
...(fresh.channels.telegram || {}),
|
|
1437
|
-
enabled: true,
|
|
1438
|
-
mode: fresh.channels.telegram?.mode || 'channel',
|
|
1439
|
-
botToken: data.token,
|
|
1440
|
-
botUsername: data.botUsername || fresh.channels.telegram?.botUsername,
|
|
1441
|
-
ownerUserId: data.ownerUserId != null ? String(data.ownerUserId) : fresh.channels.telegram?.ownerUserId,
|
|
1442
|
-
};
|
|
1443
|
-
saveConfig(fresh);
|
|
1444
|
-
// Re-pairing replaces the token — tear down any existing provider so init() reconnects
|
|
1445
|
-
// with the NEW token (init() is a no-op when a provider is already in the map).
|
|
1446
|
-
await channelManager.disconnectChannel('telegram').catch(() => {});
|
|
1447
|
-
await channelManager.init().catch(() => {});
|
|
1448
|
-
|
|
1449
|
-
res.writeHead(200);
|
|
1450
|
-
res.end(JSON.stringify({ ok: true, ready: true, botUsername: data.botUsername }));
|
|
1451
|
-
} catch (err: any) {
|
|
1452
|
-
log.warn(`[telegram/poll] ${err.message}`);
|
|
1453
|
-
res.writeHead(500);
|
|
1454
|
-
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1455
|
-
}
|
|
1456
|
-
})();
|
|
1457
|
+
})();
|
|
1458
|
+
});
|
|
1457
1459
|
return;
|
|
1458
1460
|
}
|
|
1459
1461
|
|
|
@@ -1505,7 +1507,11 @@ mint();
|
|
|
1505
1507
|
await channelManager.disconnectChannel('telegram');
|
|
1506
1508
|
const cfg = loadConfig();
|
|
1507
1509
|
if (cfg.channels?.telegram) {
|
|
1510
|
+
// "Forget the token" also forgets the identity bound to it, so a future bot re-adopts
|
|
1511
|
+
// its owner cleanly via trust-on-first-use.
|
|
1508
1512
|
delete cfg.channels.telegram.botToken;
|
|
1513
|
+
delete cfg.channels.telegram.ownerUserId;
|
|
1514
|
+
delete cfg.channels.telegram.admins;
|
|
1509
1515
|
cfg.channels.telegram.enabled = false;
|
|
1510
1516
|
saveConfig(cfg);
|
|
1511
1517
|
}
|
|
@@ -1519,7 +1525,7 @@ mint();
|
|
|
1519
1525
|
return;
|
|
1520
1526
|
}
|
|
1521
1527
|
|
|
1522
|
-
// POST /api/channels/telegram/reconnect — resume polling an already-
|
|
1528
|
+
// POST /api/channels/telegram/reconnect — resume polling an already-connected bot
|
|
1523
1529
|
// after a disconnect, without re-pairing (loopback-only).
|
|
1524
1530
|
if (req.method === 'POST' && channelPath === '/api/channels/telegram/reconnect') {
|
|
1525
1531
|
(async () => {
|
|
@@ -1553,7 +1559,8 @@ mint();
|
|
|
1553
1559
|
return;
|
|
1554
1560
|
}
|
|
1555
1561
|
|
|
1556
|
-
// GET /api/channels/telegram/pair-page —
|
|
1562
|
+
// GET /api/channels/telegram/pair-page — BYO BotFather token flow: button opens a modal explaining
|
|
1563
|
+
// how to mint a token in @BotFather, takes the pasted token, POSTs {botToken} to /connect.
|
|
1557
1564
|
if (req.method === 'GET' && channelPath === '/api/channels/telegram/pair-page') {
|
|
1558
1565
|
res.setHeader('Content-Type', 'text/html');
|
|
1559
1566
|
const tgStatus = channelManager.getStatus('telegram');
|
|
@@ -1580,26 +1587,49 @@ mint();
|
|
|
1580
1587
|
.card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px 24px;width:100%;box-shadow:0 0 0 1px rgba(0,105,254,0.1),0 0 20px -5px rgba(0,105,254,0.15);animation:fade-up .5s ease-out both;text-align:center}
|
|
1581
1588
|
.header{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:18px}
|
|
1582
1589
|
.badge{display:inline-flex;align-items:center;gap:6px;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);border-radius:9999px;padding:4px 10px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:0.6px}
|
|
1583
|
-
.badge::before{content:'';width:8px;height:8px;border-radius:50%;background
|
|
1590
|
+
.badge::before{content:'';width:8px;height:8px;border-radius:50%;background:#229ED9;box-shadow:0 0 8px rgba(34,158,217,0.6)}
|
|
1584
1591
|
.title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-top:6px}
|
|
1585
1592
|
.sub{font-size:13px;color:#999;line-height:1.6;margin-top:6px}
|
|
1586
|
-
.
|
|
1593
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;width:100%;border:none;border-radius:10px;padding:13px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;text-decoration:none}
|
|
1594
|
+
.btn-tg{background:linear-gradient(135deg,#229ED9,#2AABEE);color:#fff;box-shadow:0 4px 14px -4px rgba(34,158,217,0.5)}
|
|
1595
|
+
.btn-tg:hover{opacity:.92}
|
|
1596
|
+
.btn-tg:disabled{opacity:.5;cursor:not-allowed}
|
|
1597
|
+
.btn-ghost{background:#1a1a1a;border:1px solid rgba(255,255,255,0.1);color:#999}
|
|
1598
|
+
.btn-ghost:hover{border-color:rgba(34,158,217,0.4);color:#f5f5f5}
|
|
1599
|
+
.cta-row{margin-top:8px;animation:fade-up .5s ease-out .15s both}
|
|
1600
|
+
.hint{font-size:12px;color:#666;margin-top:14px;line-height:1.6}
|
|
1601
|
+
|
|
1602
|
+
/* modal */
|
|
1603
|
+
.overlay{position:fixed;inset:0;background:rgba(10,10,10,0.72);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;padding:20px;z-index:50}
|
|
1604
|
+
.overlay.open{display:flex;animation:fade-in .2s ease-out both}
|
|
1605
|
+
.modal{background:#2a2a2a;border:1px solid rgba(255,255,255,0.1);border-radius:20px;padding:24px 22px;width:100%;max-width:380px;max-height:90dvh;overflow-y:auto;box-shadow:0 24px 60px -12px rgba(0,0,0,0.6);animation:pop-up .28s cubic-bezier(.34,1.56,.64,1) both}
|
|
1606
|
+
.modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}
|
|
1607
|
+
.modal-title{font-family:'Space Grotesk',sans-serif;font-size:18px;font-weight:700;color:#f5f5f5}
|
|
1608
|
+
.modal-x{flex-shrink:0;width:28px;height:28px;border-radius:8px;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);color:#999;font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
|
1609
|
+
.modal-x:hover{color:#f5f5f5;border-color:rgba(34,158,217,0.4)}
|
|
1610
|
+
.modal-sub{font-size:12px;color:#999;line-height:1.6;margin-bottom:16px}
|
|
1611
|
+
.steps{text-align:left;margin:0 0 16px}
|
|
1587
1612
|
.step{display:flex;gap:12px;font-size:13px;color:#bbb;line-height:1.5;padding:6px 0}
|
|
1588
1613
|
.step-num{flex-shrink:0;width:22px;height:22px;border-radius:50%;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#888}
|
|
1589
1614
|
.step b{color:#f5f5f5;font-weight:600}
|
|
1590
|
-
.
|
|
1591
|
-
.
|
|
1592
|
-
.
|
|
1593
|
-
.
|
|
1594
|
-
.
|
|
1595
|
-
.
|
|
1596
|
-
.
|
|
1615
|
+
.step code{font-family:'Space Grotesk',monospace;color:#2AABEE;background:#1a1a1a;border:1px solid rgba(255,255,255,0.06);border-radius:5px;padding:1px 6px;font-size:12px}
|
|
1616
|
+
.bf-link{display:inline-flex;align-items:center;justify-content:center;gap:7px;width:100%;border:1px solid rgba(34,158,217,0.35);background:#1a1a1a;color:#2AABEE;border-radius:10px;padding:10px 14px;font-size:13px;font-weight:600;text-decoration:none;transition:all .2s;margin-bottom:16px}
|
|
1617
|
+
.bf-link:hover{background:rgba(34,158,217,0.08)}
|
|
1618
|
+
.field-label{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:0.6px;margin-bottom:8px;display:block}
|
|
1619
|
+
.token-input{width:100%;background:#1a1a1a;border:1px solid rgba(255,255,255,0.1);border-radius:10px;padding:12px 14px;color:#f5f5f5;font-family:'Space Grotesk',monospace;font-size:14px;outline:none;transition:border-color .2s}
|
|
1620
|
+
.token-input:focus{border-color:#229ED9}
|
|
1621
|
+
.token-input::placeholder{color:#555;font-family:'Inter',sans-serif}
|
|
1622
|
+
.modal-actions{margin-top:18px}
|
|
1623
|
+
.err{color:#FB4072;font-size:13px;margin-top:12px;min-height:1.2em;text-align:left}
|
|
1624
|
+
|
|
1597
1625
|
.confetti-wrap{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0}
|
|
1598
1626
|
.confetti-dot{position:absolute;width:8px;height:8px;border-radius:50%;top:-10px;animation:confetti-fall 2s ease-out forwards}
|
|
1599
1627
|
@keyframes confetti-fall{0%{opacity:1;transform:translateY(0) translateX(0) rotate(0) scale(1)}100%{opacity:0;transform:translateY(100vh) translateX(var(--drift)) rotate(360deg) scale(.5)}}
|
|
1600
1628
|
.video-wrap{margin-bottom:8px;animation:pop-in .5s cubic-bezier(.34,1.56,.64,1) forwards}
|
|
1601
1629
|
.video-wrap video{width:200px;object-fit:contain;pointer-events:none}
|
|
1602
1630
|
@keyframes pop-in{0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
|
|
1631
|
+
@keyframes pop-up{0%{transform:translateY(12px) scale(.96);opacity:0}100%{transform:translateY(0) scale(1);opacity:1}}
|
|
1632
|
+
@keyframes fade-in{0%{opacity:0}100%{opacity:1}}
|
|
1603
1633
|
.text-wrap{text-align:center;animation:fade-up .5s ease-out .3s both}
|
|
1604
1634
|
.success-sub{font-size:14px;color:#999;line-height:1.5;margin-top:6px}
|
|
1605
1635
|
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
|
|
@@ -1614,47 +1644,99 @@ ${alreadyLinked
|
|
|
1614
1644
|
<p class="success-sub">Telegram is linked${linkedUsername ? ` to <b style="color:#f5f5f5">@${linkedUsername}</b>` : ''}. Open Telegram and message your bot to start chatting.</p>
|
|
1615
1645
|
<p class="success-sub" style="margin-top:14px;font-size:12px;color:#666">You can close this page.</p>
|
|
1616
1646
|
</div>`
|
|
1617
|
-
: `<div class="card"
|
|
1647
|
+
: `<div class="card">
|
|
1618
1648
|
<div class="header">
|
|
1619
1649
|
<span class="badge">Telegram</span>
|
|
1620
1650
|
<div class="title">Connect Telegram</div>
|
|
1621
|
-
<p class="sub">
|
|
1651
|
+
<p class="sub">Bring your own bot. Create one for free in @BotFather, paste its token, and you're live.</p>
|
|
1622
1652
|
</div>
|
|
1623
|
-
<div class="
|
|
1624
|
-
<
|
|
1625
|
-
<div class="step"><div class="step-num">2</div><div>Tap <b>Create Bot</b> inside Telegram to confirm</div></div>
|
|
1626
|
-
<div class="step"><div class="step-num">3</div><div>Come back here — it links automatically</div></div>
|
|
1653
|
+
<div class="cta-row">
|
|
1654
|
+
<button class="btn btn-tg" id="openModal">Connect Telegram</button>
|
|
1627
1655
|
</div>
|
|
1628
|
-
<
|
|
1629
|
-
<div class="status" id="st"><span class="dot"></span><span id="stText">Getting your link ready…</span></div>
|
|
1630
|
-
<p class="err" id="err"></p>
|
|
1656
|
+
<p class="hint">It takes about a minute and keeps your bot fully private to you.</p>
|
|
1631
1657
|
</div>`}
|
|
1632
1658
|
</div>
|
|
1659
|
+
|
|
1660
|
+
${alreadyLinked ? '' : `<div class="overlay" id="overlay">
|
|
1661
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
|
|
1662
|
+
<div class="modal-head">
|
|
1663
|
+
<div class="modal-title" id="mTitle">Get your bot token</div>
|
|
1664
|
+
<button class="modal-x" id="closeModal" aria-label="Close">×</button>
|
|
1665
|
+
</div>
|
|
1666
|
+
<p class="modal-sub">Telegram bots are created by a bot called <b style="color:#f5f5f5">@BotFather</b>. Follow these steps, then paste the token below.</p>
|
|
1667
|
+
<div class="steps">
|
|
1668
|
+
<div class="step"><div class="step-num">1</div><div>Open <b>@BotFather</b> in Telegram (button below)</div></div>
|
|
1669
|
+
<div class="step"><div class="step-num">2</div><div>Send <code>/newbot</code></div></div>
|
|
1670
|
+
<div class="step"><div class="step-num">3</div><div>Choose a <b>name</b> and a <b>username</b> ending in <code>bot</code></div></div>
|
|
1671
|
+
<div class="step"><div class="step-num">4</div><div>BotFather replies with a <b>token</b> like <code>1234:AbCdEf…</code> — copy it</div></div>
|
|
1672
|
+
</div>
|
|
1673
|
+
<a class="bf-link" href="https://t.me/BotFather" target="_blank" rel="noopener">Open @BotFather in Telegram →</a>
|
|
1674
|
+
<label class="field-label" for="token">Paste your bot token</label>
|
|
1675
|
+
<input class="token-input" id="token" type="text" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="1234567890:ABCdefGhIJKlmNoPQRstuVwxyz">
|
|
1676
|
+
<div class="modal-actions">
|
|
1677
|
+
<button class="btn btn-tg" id="connectBtn">Connect</button>
|
|
1678
|
+
</div>
|
|
1679
|
+
<p class="err" id="err"></p>
|
|
1680
|
+
</div>
|
|
1681
|
+
</div>`}
|
|
1682
|
+
|
|
1633
1683
|
<script>
|
|
1634
|
-
${alreadyLinked ?
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1684
|
+
${alreadyLinked ? '' : `
|
|
1685
|
+
(function(){
|
|
1686
|
+
var overlay=document.getElementById('overlay');
|
|
1687
|
+
var input=document.getElementById('token');
|
|
1688
|
+
var btn=document.getElementById('connectBtn');
|
|
1689
|
+
var err=document.getElementById('err');
|
|
1690
|
+
function open(){overlay.classList.add('open');setTimeout(function(){input.focus()},150)}
|
|
1691
|
+
function close(){overlay.classList.remove('open')}
|
|
1692
|
+
document.getElementById('openModal').addEventListener('click',open);
|
|
1693
|
+
document.getElementById('closeModal').addEventListener('click',close);
|
|
1694
|
+
overlay.addEventListener('click',function(e){if(e.target===overlay)close()});
|
|
1695
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape')close()});
|
|
1696
|
+
input.addEventListener('input',function(){err.textContent='';input.style.borderColor=''});
|
|
1697
|
+
input.addEventListener('keydown',function(e){if(e.key==='Enter')connect()});
|
|
1698
|
+
btn.addEventListener('click',connect);
|
|
1699
|
+
|
|
1700
|
+
function setLoading(on){
|
|
1701
|
+
btn.disabled=on;
|
|
1702
|
+
btn.textContent=on?'Connecting…':'Connect';
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async function connect(){
|
|
1706
|
+
var token=(input.value||'').trim();
|
|
1707
|
+
err.textContent='';
|
|
1708
|
+
if(!/^\\d{6,}:[A-Za-z0-9_-]{30,}$/.test(token)){
|
|
1709
|
+
err.textContent="That doesn't look like a bot token. Copy the full line BotFather sent you.";
|
|
1710
|
+
input.style.borderColor='#FB4072';
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
setLoading(true);
|
|
1714
|
+
try{
|
|
1715
|
+
var headers={'Content-Type':'application/json'};
|
|
1716
|
+
// Send the dashboard's portal token so the (password-gated) connect endpoint authorizes us.
|
|
1717
|
+
try{var t=localStorage.getItem('bloby_token');if(t)headers['Authorization']='Bearer '+t;}catch(e){}
|
|
1718
|
+
var r=await fetch('/api/channels/telegram/connect',{method:'POST',headers:headers,body:JSON.stringify({botToken:token})});
|
|
1719
|
+
var d=await r.json().catch(function(){return {}});
|
|
1720
|
+
if(!r.ok||!d.ok){
|
|
1721
|
+
err.textContent=friendly(d.error,r.status)||'Could not connect. Check the token and try again.';
|
|
1722
|
+
setLoading(false);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
btn.textContent='Connected ✓';
|
|
1726
|
+
location.reload();
|
|
1727
|
+
}catch(e){
|
|
1728
|
+
err.textContent='Network error. Try again.';
|
|
1729
|
+
setLoading(false);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function friendly(code,status){
|
|
1734
|
+
if(status===401||/auth-required/i.test(code||''))return 'Please sign in to your Bloby dashboard first, then try again.';
|
|
1735
|
+
if(/invalid.*token|getMe|invalid-bot-token/i.test(code||''))return 'Telegram rejected that token. Make sure you copied the whole line from BotFather.';
|
|
1736
|
+
if(/unreachable|network|502|timeout/i.test(code||''))return 'Could not reach Telegram. Check your connection and try again.';
|
|
1737
|
+
return code||'';
|
|
1738
|
+
}
|
|
1739
|
+
})();
|
|
1658
1740
|
`}
|
|
1659
1741
|
</script>
|
|
1660
1742
|
</body></html>`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telegram",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Telegram channel via
|
|
4
|
+
"description": "Telegram channel via your own @BotFather bot token. Paste-token connect, direct long-poll, voice/photo, channel/business/assistant modes.",
|
|
5
5
|
"skills": "./"
|
|
6
6
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telegram
|
|
3
|
-
description: "Telegram channel for your agent.
|
|
3
|
+
description: "Telegram channel for your agent. Create a free bot in @BotFather, paste its token, and your Bloby talks to Telegram directly. Messaging, voice notes, photos, channel/business/assistant modes."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Telegram
|
|
7
7
|
|
|
8
8
|
## What This Is
|
|
9
9
|
|
|
10
|
-
Gives your agent its own Telegram bot.
|
|
10
|
+
Gives your agent its own Telegram bot. Your human creates a free bot via Telegram's **@BotFather** and pastes the token into a connect page — then your Bloby sends and receives messages, handles voice notes and photos, and switches between personal (channel), business, and assistant modes.
|
|
11
11
|
|
|
12
|
-
Under the hood: Bloby
|
|
12
|
+
Under the hood: your Bloby holds the bot token locally and long-polls Telegram **directly** — there's no relay or middle server in the message path. It keeps working behind any network with no public URL, and the bot stays fully private to your human.
|
|
13
13
|
|
|
14
14
|
## Dependencies
|
|
15
15
|
|
|
@@ -51,7 +51,7 @@ The format is: `[Telegram | chatId | role | name (optional)]`
|
|
|
51
51
|
|
|
52
52
|
## Channel Config
|
|
53
53
|
|
|
54
|
-
Your channel configuration lives in `~/.bloby/config.json` under `channels.telegram` — a file the supervisor manages, outside your workspace. It holds the bot token (
|
|
54
|
+
Your channel configuration lives in `~/.bloby/config.json` under `channels.telegram` — a file the supervisor manages, outside your workspace. It holds the bot token (from @BotFather, saved when your human connects), the bot @username, the owner's Telegram user id, and the mode.
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -82,17 +82,17 @@ curl -s -X POST http://localhost:7400/api/channels/telegram/configure \
|
|
|
82
82
|
|
|
83
83
|
### 1. Connect Telegram
|
|
84
84
|
|
|
85
|
-
When your human asks to connect Telegram, give them the pairing page (send it as a relative URL — their browser is already on the right domain). The dashboard renders
|
|
85
|
+
When your human asks to connect Telegram, give them the pairing page (send it as a relative URL — their browser is already on the right domain). The dashboard renders this URL as a pretty Telegram button:
|
|
86
86
|
|
|
87
87
|
```
|
|
88
88
|
/api/channels/telegram/pair-page
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
That page
|
|
91
|
+
That page walks them through everything: they click **Connect Telegram**, a modal explains how to create a free bot in **@BotFather** (`/newbot` → pick a name + username → copy the token), they paste the token, and click **Connect**. The page posts the token to the supervisor, which validates it and brings the channel up automatically.
|
|
92
92
|
|
|
93
|
-
The default mode after
|
|
93
|
+
The default mode after connecting is **channel** (owner-only). The owner is set automatically the first time your human messages the bot.
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
Don't mention the URL until your human actually asks to connect — then hand them `/api/channels/telegram/pair-page` and let the page do the rest.
|
|
96
96
|
|
|
97
97
|
### 2. Choose a Mode
|
|
98
98
|
|
|
@@ -197,8 +197,8 @@ curl -s -X POST http://localhost:7400/api/channels/telegram/logout
|
|
|
197
197
|
|
|
198
198
|
## Human Interaction
|
|
199
199
|
|
|
200
|
-
-
|
|
201
|
-
- Your human's bot is a normal Telegram contact — they (and anyone they share it with) message it directly.
|
|
200
|
+
- Connecting is one page: **Connect Telegram** → paste the **@BotFather** token → **Connect**. The page handles it.
|
|
201
|
+
- Your human's bot is a normal Telegram contact — they (and anyone they share it with) message it directly. The first person to message it becomes the owner.
|
|
202
202
|
- In business mode, explain that admin user ids get full agent access while everyone else gets the customer-facing skill.
|
|
203
203
|
- Privacy: the bot token is stored locally at `~/.bloby/config.json`. Messages flow directly between this machine and Telegram — they do not pass through Bloby's servers.
|
|
204
204
|
|
|
@@ -210,9 +210,8 @@ curl -s -X POST http://localhost:7400/api/channels/telegram/logout
|
|
|
210
210
|
|----------|--------|---------|
|
|
211
211
|
| `/api/channels/status` | GET | List all channel statuses |
|
|
212
212
|
| `/api/channels/telegram/status` | GET | Telegram channel status |
|
|
213
|
-
| `/api/channels/telegram/pair-page` | GET |
|
|
214
|
-
| `/api/channels/telegram/connect` | POST |
|
|
215
|
-
| `/api/channels/telegram/poll?sessionId=…` | GET | Poll provisioning; links when the bot is created |
|
|
213
|
+
| `/api/channels/telegram/pair-page` | GET | Connect page — hand this to the human (renders as a button) |
|
|
214
|
+
| `/api/channels/telegram/connect` | POST | `{"botToken":"123456789:AAH..."}` — validate token + bring the channel up (the page calls this) |
|
|
216
215
|
| `/api/channels/telegram/configure` | POST | Set mode + admins + skill |
|
|
217
216
|
| `/api/channels/telegram/disconnect` | POST | Stop polling (keep token) |
|
|
218
217
|
| `/api/channels/telegram/reconnect` | POST | Resume the same bot after a disconnect |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"bloby_human": "Bruno Bertapeli",
|
|
6
6
|
"bloby": "bloby-bruno",
|
|
7
7
|
"author": "newbot-official",
|
|
8
|
-
"description": "Telegram channel for your agent.
|
|
8
|
+
"description": "Telegram channel for your agent. Create a free bot in @BotFather, paste its token, and your Bloby talks to Telegram directly. Messaging, voice notes, photos, channel/business/assistant modes.",
|
|
9
9
|
"depends": [],
|
|
10
10
|
"env_keys": [],
|
|
11
11
|
"has_telemetry": false,
|