bloby-bot 0.60.1 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -551,6 +551,14 @@ 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
+ // 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
+ 'GET /api/channels/telegram/pair-page',
557
+ 'GET /api/channels/telegram/status',
558
+ 'POST /api/channels/telegram/configure',
559
+ 'POST /api/channels/telegram/disconnect',
560
+ 'POST /api/channels/telegram/reconnect',
561
+ 'POST /api/channels/telegram/logout',
554
562
  ];
555
563
  // Method-specific public PREFIXES — onboarding namespaces with sub-paths / params that carry no
556
564
  // private chat data: provider OAuth setup/status (all of /api/auth/*), and handle availability
@@ -677,6 +685,11 @@ export async function startSupervisor() {
677
685
  'POST /api/channels/whatsapp/logout',
678
686
  'POST /api/channels/whatsapp/pairing-code',
679
687
  'POST /api/channels/send',
688
+ // Telegram mutation endpoints that seize/alter the agent — agent-driven over loopback only.
689
+ 'POST /api/channels/telegram/configure',
690
+ 'POST /api/channels/telegram/disconnect',
691
+ 'POST /api/channels/telegram/reconnect',
692
+ 'POST /api/channels/telegram/logout',
680
693
  ]);
681
694
  if (WA_MUTATION_ROUTES.has(`${req.method} ${channelPath}`)) {
682
695
  const remoteIp = req.socket.remoteAddress || '';
@@ -1348,6 +1361,388 @@ mint();
1348
1361
  return;
1349
1362
  }
1350
1363
 
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
+
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).
1372
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/connect') {
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
+ }
1403
+
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(() => {});
1448
+ res.writeHead(200);
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
+ }
1456
+ }
1457
+ })();
1458
+ });
1459
+ return;
1460
+ }
1461
+
1462
+ // POST /api/channels/telegram/configure — set mode + admins + skill (loopback-only).
1463
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/configure') {
1464
+ let body = '';
1465
+ req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
1466
+ req.on('end', () => {
1467
+ try {
1468
+ const data = JSON.parse(body || '{}');
1469
+ const cfg = loadConfig();
1470
+ if (!cfg.channels) cfg.channels = {};
1471
+ if (!cfg.channels.telegram) cfg.channels.telegram = { enabled: false, mode: 'channel' };
1472
+ if (data.mode) cfg.channels.telegram.mode = data.mode;
1473
+ if (data.admins !== undefined) cfg.channels.telegram.admins = data.admins;
1474
+ if (data.skill !== undefined) cfg.channels.telegram.skill = data.skill;
1475
+ if (data.allowGroups !== undefined) cfg.channels.telegram.allowGroups = !!data.allowGroups;
1476
+ if (data.allowOthersToTrigger !== undefined) cfg.channels.telegram.allowOthersToTrigger = !!data.allowOthersToTrigger;
1477
+ saveConfig(cfg);
1478
+ res.writeHead(200);
1479
+ res.end(JSON.stringify({ ok: true, config: { ...cfg.channels.telegram, botToken: cfg.channels.telegram.botToken ? '***' : undefined } }));
1480
+ } catch (err: any) {
1481
+ res.writeHead(400);
1482
+ res.end(JSON.stringify({ error: err.message }));
1483
+ }
1484
+ });
1485
+ return;
1486
+ }
1487
+
1488
+ // POST /api/channels/telegram/disconnect — stop polling, KEEP the token (loopback-only).
1489
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/disconnect') {
1490
+ (async () => {
1491
+ try {
1492
+ await channelManager.disconnectChannel('telegram');
1493
+ res.writeHead(200);
1494
+ res.end(JSON.stringify({ ok: true }));
1495
+ } catch (err: any) {
1496
+ res.writeHead(500);
1497
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1498
+ }
1499
+ })();
1500
+ return;
1501
+ }
1502
+
1503
+ // POST /api/channels/telegram/logout — stop polling + forget the token (loopback-only).
1504
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/logout') {
1505
+ (async () => {
1506
+ try {
1507
+ await channelManager.disconnectChannel('telegram');
1508
+ const cfg = loadConfig();
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.
1512
+ delete cfg.channels.telegram.botToken;
1513
+ delete cfg.channels.telegram.ownerUserId;
1514
+ delete cfg.channels.telegram.admins;
1515
+ cfg.channels.telegram.enabled = false;
1516
+ saveConfig(cfg);
1517
+ }
1518
+ res.writeHead(200);
1519
+ res.end(JSON.stringify({ ok: true }));
1520
+ } catch (err: any) {
1521
+ res.writeHead(500);
1522
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1523
+ }
1524
+ })();
1525
+ return;
1526
+ }
1527
+
1528
+ // POST /api/channels/telegram/reconnect — resume polling an already-connected bot
1529
+ // after a disconnect, without re-pairing (loopback-only).
1530
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/reconnect') {
1531
+ (async () => {
1532
+ try {
1533
+ const cfg = loadConfig();
1534
+ if (!cfg.channels?.telegram?.botToken) {
1535
+ res.writeHead(400);
1536
+ res.end(JSON.stringify({ ok: false, error: 'no-bot-token' }));
1537
+ return;
1538
+ }
1539
+ if (cfg.channels.telegram.enabled !== true) {
1540
+ cfg.channels.telegram.enabled = true;
1541
+ saveConfig(cfg);
1542
+ }
1543
+ await channelManager.init().catch(() => {});
1544
+ res.writeHead(200);
1545
+ res.end(JSON.stringify({ ok: true }));
1546
+ } catch (err: any) {
1547
+ res.writeHead(500);
1548
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1549
+ }
1550
+ })();
1551
+ return;
1552
+ }
1553
+
1554
+ // GET /api/channels/telegram/status — current telegram channel status
1555
+ if (req.method === 'GET' && channelPath === '/api/channels/telegram/status') {
1556
+ const status = channelManager.getStatus('telegram');
1557
+ res.writeHead(200);
1558
+ res.end(JSON.stringify(status || { channel: 'telegram', connected: false }));
1559
+ return;
1560
+ }
1561
+
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.
1564
+ if (req.method === 'GET' && channelPath === '/api/channels/telegram/pair-page') {
1565
+ res.setHeader('Content-Type', 'text/html');
1566
+ const tgStatus = channelManager.getStatus('telegram');
1567
+ const alreadyLinked = !!(tgStatus?.info as any)?.linked;
1568
+ const linkedUsername = (tgStatus?.info as any)?.botUsername || '';
1569
+ const confettiHTML = Array.from({ length: 30 }, (_, i) => {
1570
+ const colors = ['#0166FF', '#009AFE', '#4AEEFF', '#4ade80', '#facc15', '#818cf8'];
1571
+ const color = colors[Math.floor(Math.random() * colors.length)];
1572
+ const left = Math.random() * 100;
1573
+ const delay = i * 0.04;
1574
+ const drift = (Math.random() - 0.5) * 120;
1575
+ const duration = 1.8 + Math.random() * 0.8;
1576
+ return `<div class="confetti-dot" style="left:${left}%;background:${color};animation-delay:${delay}s;animation-duration:${duration}s;--drift:${drift}px"></div>`;
1577
+ }).join('');
1578
+ res.writeHead(200);
1579
+ res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connect Telegram</title>
1580
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1581
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1582
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
1583
+ <style>
1584
+ *{margin:0;padding:0;box-sizing:border-box}
1585
+ body{background:#212121;color:#f5f5f5;font-family:'Inter',system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;margin:0;overflow-x:hidden}
1586
+ .container{display:flex;flex-direction:column;align-items:center;max-width:380px;width:100%;padding:20px}
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}
1588
+ .header{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:18px}
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}
1590
+ .badge::before{content:'';width:8px;height:8px;border-radius:50%;background:#229ED9;box-shadow:0 0 8px rgba(34,158,217,0.6)}
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}
1592
+ .sub{font-size:13px;color:#999;line-height:1.6;margin-top:6px}
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}
1612
+ .step{display:flex;gap:12px;font-size:13px;color:#bbb;line-height:1.5;padding:6px 0}
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}
1614
+ .step b{color:#f5f5f5;font-weight:600}
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
+
1625
+ .confetti-wrap{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0}
1626
+ .confetti-dot{position:absolute;width:8px;height:8px;border-radius:50%;top:-10px;animation:confetti-fall 2s ease-out forwards}
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)}}
1628
+ .video-wrap{margin-bottom:8px;animation:pop-in .5s cubic-bezier(.34,1.56,.64,1) forwards}
1629
+ .video-wrap video{width:200px;object-fit:contain;pointer-events:none}
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}}
1633
+ .text-wrap{text-align:center;animation:fade-up .5s ease-out .3s both}
1634
+ .success-sub{font-size:14px;color:#999;line-height:1.5;margin-top:6px}
1635
+ @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
1636
+ @keyframes fade-up{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
1637
+ </style></head><body>
1638
+ <div class="container" id="root">
1639
+ ${alreadyLinked
1640
+ ? `<div class="confetti-wrap">${confettiHTML}</div>
1641
+ <div class="video-wrap"><video autoplay muted playsinline><source src="/bloby_happy_reappearing.mov" type='video/mp4; codecs="hvc1"'><source src="/bloby_happy_reappearing.webm" type="video/webm"></video></div>
1642
+ <div class="text-wrap">
1643
+ <div class="title">Connected!</div>
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>
1645
+ <p class="success-sub" style="margin-top:14px;font-size:12px;color:#666">You can close this page.</p>
1646
+ </div>`
1647
+ : `<div class="card">
1648
+ <div class="header">
1649
+ <span class="badge">Telegram</span>
1650
+ <div class="title">Connect Telegram</div>
1651
+ <p class="sub">Bring your own bot. Create one for free in @BotFather, paste its token, and you're live.</p>
1652
+ </div>
1653
+ <div class="cta-row">
1654
+ <button class="btn btn-tg" id="openModal">Connect Telegram</button>
1655
+ </div>
1656
+ <p class="hint">It takes about a minute and keeps your bot fully private to you.</p>
1657
+ </div>`}
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">&times;</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 &rarr;</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
+
1683
+ <script>
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
+ })();
1740
+ `}
1741
+ </script>
1742
+ </body></html>`);
1743
+ return;
1744
+ }
1745
+
1351
1746
  // Fallback for unknown channel routes
1352
1747
  res.writeHead(404);
1353
1748
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -60,9 +60,9 @@
60
60
  '<source src="/what-happened.webm" type="video/webm"><source src="/what-happened.mp4" type="video/mp4">' +
61
61
  '</video>' +
62
62
  '</div>' +
63
- '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">A screen in your app has an error</h1>' +
64
- '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest change didn\'t compile.</p>' +
65
- '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">Ask your agent to fix it the chat is in the bottom corner. Copy the error so it can debug faster.</p>' +
63
+ '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">Workspace error</h1>' +
64
+ '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest frontend change didnt compile. If your Bloby is currently building or editing something, this is usually normal while the work is still in progress.</p>' +
65
+ '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">When your agent finishes, this may fix itself. If it doesn’t, ask your Bloby to fix it and copy the error so it can debug faster.</p>' +
66
66
  '<div><button id="__bloby_fe_copy" style="font:inherit;cursor:pointer;border:none;border-radius:10px;padding:.65rem 1.2rem;font-size:.9rem;font-weight:600;background:linear-gradient(135deg,#0166FF,#0069FE);color:#fff">Copy error for your agent</button> ' +
67
67
  '<button id="__bloby_fe_dismiss" style="font:inherit;cursor:pointer;border-radius:10px;padding:.65rem 1.1rem;font-size:.9rem;font-weight:600;border:1px solid #27272a;background:#18181b;color:#e4e4e7">Dismiss</button></div>' +
68
68
  '</div>';
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "telegram",
3
+ "version": "1.0.0",
4
+ "description": "Telegram channel via your own @BotFather bot token. Paste-token connect, direct long-poll, voice/photo, channel/business/assistant modes.",
5
+ "skills": "./"
6
+ }