Telegram Mini App v1: initData auth bridge into the existing site
- POST /api/public/tg-webapp-auth: HMAC-verifies WebApp initData against the companion bot token (12h freshness, timing-safe), maps chat -> member via tg-links.json, mints a message session -> linked members land on /my/<id> with zero login - /app entry page (vendored telegram-web-app.js keeps CSP script-src 'self'); unlinked users get the one-time wallet-link instructions - tg-app.js on all pages: no-op in browsers; inside the webview lazy-loads the SDK, expands, themes header/background #071421, wires native BackButton - Bot menu button set programmatically to open /app; /start + help mention it - Synced chat.js canned answer + AI system prompt (Mini App facts) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,28 @@ async function dm(chatId, text, extra) {
|
||||
return api('sendMessage', Object.assign({ chat_id: chatId, text, disable_web_page_preview: true }, extra || {}));
|
||||
}
|
||||
|
||||
// --- Mini App: verify Telegram WebApp initData (HMAC per Bot API spec) ------
|
||||
// secret_key = HMAC_SHA256(key="WebAppData", bot_token); hash covers the
|
||||
// sorted key=value lines of every field except hash itself.
|
||||
const INITDATA_MAX_AGE_S = 12 * 3600;
|
||||
function verifyInitData(initData) {
|
||||
const t = token(); if (!t) return { error: 'bot-offline' };
|
||||
if (typeof initData !== 'string' || !initData || initData.length > 4096) return { error: 'bad-initdata' };
|
||||
let params; try { params = new URLSearchParams(initData); } catch (e) { return { error: 'bad-initdata' }; }
|
||||
const hash = params.get('hash');
|
||||
if (!hash || !/^[0-9a-f]{64}$/.test(hash)) return { error: 'bad-initdata' };
|
||||
params.delete('hash');
|
||||
const dcs = [...params.entries()].map(([k, v]) => `${k}=${v}`).sort().join('\n');
|
||||
const secret = crypto.createHmac('sha256', 'WebAppData').update(t).digest();
|
||||
const check = crypto.createHmac('sha256', secret).update(dcs).digest('hex');
|
||||
if (!crypto.timingSafeEqual(Buffer.from(check), Buffer.from(hash))) return { error: 'bad-hash' };
|
||||
const authDate = Number(params.get('auth_date')) || 0;
|
||||
if (Math.abs(Date.now() / 1000 - authDate) > INITDATA_MAX_AGE_S) return { error: 'stale' };
|
||||
let user = null; try { user = JSON.parse(params.get('user') || 'null'); } catch (e) {}
|
||||
if (!user || !user.id) return { error: 'no-user' };
|
||||
return { userId: user.id, user };
|
||||
}
|
||||
|
||||
// --- setup: learn our username + point the webhook at ourselves -------------
|
||||
async function ensureWebhook(baseUrl) {
|
||||
const d = load();
|
||||
@@ -42,6 +64,9 @@ async function ensureWebhook(baseUrl) {
|
||||
if (me && me.ok) { d.u = me.result.username; save(d); }
|
||||
const secret = webhookSecret();
|
||||
if (!secret) return;
|
||||
// Menu button (bottom-left ☰ in the private chat) opens the Mini App.
|
||||
// Idempotent; BotFather /newapp is only needed for t.me/<bot>/<app> links.
|
||||
await api('setChatMenuButton', { menu_button: { type: 'web_app', text: 'Open App', web_app: { url: `${baseUrl}/app` } } });
|
||||
const url = `${baseUrl}/api/tg-hook/${secret}`;
|
||||
const info = await api('getWebhookInfo', {});
|
||||
if (info && info.ok && info.result.url === url) return;
|
||||
@@ -228,7 +253,7 @@ async function handleUpdate(update) {
|
||||
d.members[String(rec.id)] = chatId;
|
||||
d.chats[String(chatId)] = rec.id;
|
||||
save(d);
|
||||
await dm(chatId, `✅ Linked to position #${rec.id}!\n\nFrom now on:\n💰 You get a DM the moment your position catches a payment\n📨 Team messages reach you here — reply to answer\n🎉 You're pinged when someone joins on your link\n\nTry: coach · team · links — or: msg <id> <your message>\nYour weekly 20-minute digest arrives Saturdays 10am Central (rhythm to adjust).`);
|
||||
await dm(chatId, `✅ Linked to position #${rec.id}!\n\nFrom now on:\n💰 You get a DM the moment your position catches a payment\n📨 Team messages reach you here — reply to answer\n🎉 You're pinged when someone joins on your link\n\nTry: coach · team · links — or: msg <id> <your message>\n📱 Tap the ☰ menu button (next to the message box) to open your full dashboard right inside Telegram — no login needed.\nYour weekly 20-minute digest arrives Saturdays 10am Central (rhythm to adjust).`);
|
||||
return;
|
||||
}
|
||||
await dm(chatId, `That link code is expired or already used. Get a fresh one from the Messages panel on your dashboard: https://rmcircle.team/my`);
|
||||
@@ -241,7 +266,7 @@ async function handleUpdate(update) {
|
||||
if (!linked) { await dm(chatId, `You're not linked yet. Open https://rmcircle.team/my → Messages → "Connect Telegram".`); return; }
|
||||
|
||||
if (/^\/?links$/i.test(text)) { await dm(chatId, linksText(linked)); return; }
|
||||
if (/^\/?help$/i.test(text)) { await dm(chatId, `Commands:\nlinks — your invite + angle links\ncoach — who to help + which lesson to send\nteam — your org numbers\nmsg <id> <text> — message a teammate\nlesson <1-10> — grab any Circle Method lesson link\nrhythm — your weekly 20-minute digest (rhythm now / rhythm sat 9 / rhythm off)\nReply to any 📨 message to answer it.\nDashboard: https://rmcircle.team/my/${linked}`); return; }
|
||||
if (/^\/?help$/i.test(text)) { await dm(chatId, `Commands:\nlinks — your invite + angle links\ncoach — who to help + which lesson to send\nteam — your org numbers\nmsg <id> <text> — message a teammate\nlesson <1-10> — grab any Circle Method lesson link\nrhythm — your weekly 20-minute digest (rhythm now / rhythm sat 9 / rhythm off)\nReply to any 📨 message to answer it.\n📱 The ☰ menu button opens your full dashboard inside Telegram — no login.\nDashboard: https://rmcircle.team/my/${linked}`); return; }
|
||||
|
||||
if (/^\/?coach$/i.test(text)) { const d = await fetchMember(linked); await dm(chatId, coachText(d)); return; }
|
||||
if (/^\/?team$/i.test(text)) { const d = await fetchMember(linked); await dm(chatId, teamText(d)); return; }
|
||||
@@ -275,4 +300,4 @@ async function handleUpdate(update) {
|
||||
} catch (e) { console.error('tgbot handleUpdate', e.message); }
|
||||
}
|
||||
|
||||
module.exports = { init, handleUpdate, notifyEvent, notifyMessage, makeLinkCode, webhookSecret, memberChat };
|
||||
module.exports = { init, handleUpdate, notifyEvent, notifyMessage, makeLinkCode, webhookSecret, memberChat, chatMember, verifyInitData };
|
||||
|
||||
Reference in New Issue
Block a user