bloby-bot 0.61.0 → 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,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
- 'GET /api/channels/telegram/poll',
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 (relay provisions a per-user bot via "Managed Bots"; the Bloby then
1365
- // long-polls Telegram DIRECTLY the relay is only in the pairing path) ──
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 — ask the relay to start provisioning a child bot.
1368
- // Returns a t.me/newbot deep link the user taps + a sessionId to poll. No token yet.
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
- (async () => {
1371
- try {
1372
- const cfg = loadConfig();
1373
- if (!cfg.relay?.token) {
1374
- res.writeHead(400);
1375
- res.end(JSON.stringify({ ok: false, error: 'no-relay-token' }));
1376
- return;
1377
- }
1378
- const resp = await fetch('https://api.bloby.bot/api/telegram/provision', {
1379
- method: 'POST',
1380
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.relay.token}` },
1381
- body: JSON.stringify({ name: cfg.username || 'Bloby' }),
1382
- });
1383
- const data = await resp.json() as { sessionId?: string; deepLink?: string; botUsername?: string; expiresAt?: string; error?: string };
1384
- if (!resp.ok || !data.sessionId || !data.deepLink) {
1385
- res.writeHead(resp.status || 500);
1386
- res.end(JSON.stringify({ ok: false, error: data.error || 'provision-failed' }));
1387
- return;
1388
- }
1389
- res.writeHead(200);
1390
- res.end(JSON.stringify({ ok: true, sessionId: data.sessionId, deepLink: data.deepLink, botUsername: data.botUsername, expiresAt: data.expiresAt }));
1391
- } catch (err: any) {
1392
- log.warn(`[telegram/connect] ${err.message}`);
1393
- res.writeHead(500);
1394
- res.end(JSON.stringify({ ok: false, error: err.message }));
1395
- }
1396
- })();
1397
- return;
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
- // GET /api/channels/telegram/poll?sessionId=… poll the relay for the provisioned token.
1401
- // When ready, persist the child token locally and start the long-poll provider.
1402
- if (req.method === 'GET' && channelPath === '/api/channels/telegram/poll') {
1403
- (async () => {
1404
- try {
1405
- const sessionId = new URL(req.url || '', 'http://localhost').searchParams.get('sessionId') || '';
1406
- if (!sessionId) {
1407
- res.writeHead(400);
1408
- res.end(JSON.stringify({ ok: false, error: 'missing-sessionId' }));
1409
- return;
1410
- }
1411
- const cfg = loadConfig();
1412
- if (!cfg.relay?.token) {
1413
- res.writeHead(400);
1414
- res.end(JSON.stringify({ ok: false, error: 'no-relay-token' }));
1415
- return;
1416
- }
1417
- const resp = await fetch(`https://api.bloby.bot/api/telegram/provision/${encodeURIComponent(sessionId)}`, {
1418
- headers: { Authorization: `Bearer ${cfg.relay.token}` },
1419
- });
1420
- const data = await resp.json() as { ready?: boolean; token?: string; botUsername?: string; ownerUserId?: string | number; error?: string };
1421
- if (!resp.ok) {
1422
- res.writeHead(resp.status);
1423
- res.end(JSON.stringify({ ok: false, error: data.error || 'poll-failed' }));
1424
- return;
1425
- }
1426
- if (!data.ready || !data.token) {
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, ready: false }));
1429
- return;
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
- // Provisioned — persist the child bot token and bring the channel up.
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-provisioned bot
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 — inline page: tap to create the bot, then auto-detects link.
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:linear-gradient(135deg,#0166FF,#009AFE);box-shadow:0 0 8px rgba(74,238,255,0.5)}
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
- .steps{text-align:left;margin:18px 0 6px;animation:fade-up .5s ease-out .2s both}
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
- .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;margin-top:8px}
1591
- .btn-primary{background:linear-gradient(135deg,#0166FF,#009AFE);color:#fff}
1592
- .btn-primary:hover{opacity:.92}
1593
- .btn-disabled{opacity:.5;pointer-events:none}
1594
- .status{font-size:12px;color:#666;margin-top:14px;display:inline-flex;align-items:center;gap:6px;min-height:1.2em}
1595
- .status .dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pulse 1.6s ease-in-out infinite;opacity:.6}
1596
- .err{color:#FB4072;font-size:13px;margin-top:12px;min-height:1.2em}
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" id="pendingCard">
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">Create your own private Telegram bot in two taps no token, no copy-paste.</p>
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="steps">
1624
- <div class="step"><div class="step-num">1</div><div>Tap <b>Create my bot</b> below — Telegram opens with a confirm screen</div></div>
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
- <a class="btn btn-primary btn-disabled" id="cta" href="#" target="_blank" rel="noopener">Preparing…</a>
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">&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
+
1633
1683
  <script>
1634
- ${alreadyLinked ? `` : `
1635
- let sessionId=null,poller=null;
1636
- async function start(){
1637
- try{
1638
- const r=await fetch('/api/channels/telegram/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
1639
- const d=await r.json();
1640
- if(!d.ok){document.getElementById('err').textContent=d.error||'Could not start. Try again.';return}
1641
- sessionId=d.sessionId;
1642
- const cta=document.getElementById('cta');
1643
- cta.href=d.deepLink;cta.classList.remove('btn-disabled');cta.textContent='Create my bot';
1644
- document.getElementById('stText').textContent='Waiting for you to confirm in Telegram…';
1645
- if(poller)clearInterval(poller);
1646
- poller=setInterval(poll,2500);
1647
- }catch(e){document.getElementById('err').textContent=e.message}
1648
- }
1649
- async function poll(){
1650
- if(!sessionId)return;
1651
- try{
1652
- const r=await fetch('/api/channels/telegram/poll?sessionId='+encodeURIComponent(sessionId));
1653
- const d=await r.json();
1654
- if(d.ok&&d.ready){clearInterval(poller);location.reload()}
1655
- }catch(e){/* keep polling */}
1656
- }
1657
- start();
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 a relay-provisioned private bot. Two-tap pairing, direct long-poll, voice/photo, channel/business/assistant modes.",
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. Two-tap pairing creates a private bot, then your Bloby talks to Telegram directly. Messaging, voice notes, photos, channel/business/assistant modes."
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. Connect in two taps (no token, no copy-paste), then send and receive messages, handle voice notes and photos, and switch between personal (channel), business, and assistant modes.
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's relay provisions a private bot for you via Telegram's "Managed Bots" feature, hands the token to your Bloby, and steps out. From then on **your Bloby talks to Telegram directly** — the relay is never in the message path. It keeps working behind any network with no public URL.
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 (provisioned at pairing), the bot @username, the owner's Telegram user id, and the mode.
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 any `/pair-page` URL as a button:
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 does everything: it asks the relay to provision a private bot, shows a **"Create my bot"** button that opens Telegram, and links automatically once the human taps **Create Bot** inside Telegram. No token, no copy-paste.
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 pairing is **channel** (owner-only).
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
- You do not need to call `connect` yourself the pair-page handles provisioning. (`POST /api/channels/telegram/connect` exists if you want to fetch the deep link directly.)
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
- - Pairing is two taps: **Create my bot** → **Create Bot** in Telegram. No token or copy-paste.
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 | Pairing page (hand this to the human) |
214
- | `/api/channels/telegram/connect` | POST | Start provisioning, returns the t.me deep link |
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. Two-tap pairing provisions a private bot (no token copy-paste), then your Bloby talks to Telegram directly. Messaging, voice notes, photos, channel/business/assistant modes.",
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,