From 3199347ca7d68f89d90798ed22e80ef61315f593 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 19 Sep 2026 16:37:34 -0500 Subject: [PATCH] Missions may place the token with {token} and name the embed host explicitly (Telegram Mini App launch links) Co-Authored-By: Claude Fable 5.1 --- server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index befce94..afe8a4b 100644 --- a/server.js +++ b/server.js @@ -124,7 +124,9 @@ const server = http.createServer(async (req, res) => { if (rewards.completed(me.memberId, m.id)) return json(res, 400, { error: 'You already completed this one.' }); if (limited('start:' + me.memberId, 20, 3600000)) return json(res, 429, { error: 'Easy. Twenty starts an hour is plenty.' }); const t = missions.issue(me.memberId, m.id); - const url = m.url + (m.url.includes('?') ? '&' : '?') + 'ph=' + t.t; + // a mission URL may place the token itself with {token} (a Telegram Mini App takes it in + // ?startapp=, not as our own query string); otherwise it is appended as ?ph= + const url = m.url.includes('{token}') ? m.url.replace('{token}', t.t) : m.url + (m.url.includes('?') ? '&' : '?') + 'ph=' + t.t; return json(res, 200, { ok: true, token: t.t, url, dwell: m.dwell || 30, expires: t.exp }); } if (p === '/api/my/submit' && req.method === 'POST') { @@ -147,6 +149,8 @@ const server = http.createServer(async (req, res) => { const b = await readBody(req); const id = String(b.id || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 40); if (!id) return json(res, 400, { error: 'id required' }); let host = ''; try { host = new URL(String(b.url)).hostname.replace(/^www\./, ''); } catch (e) { return json(res, 400, { error: 'url must be a full https URL' }); } + // the origin the embed calls from can differ from the link (a t.me launch link opens a Mini App on its own host) + if (b.host) host = String(b.host).trim().toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, ''); missions.save({ id, site: String(b.site || host).slice(0, 60), host, name: String(b.name || '').slice(0, 80), brief: String(b.brief || '').slice(0, 400), url: String(b.url), dwell: Math.max(5, Number(b.dwell) || 45), slots: Math.max(1, Number(b.slots) || 1), budget: Math.max(0, Number(b.budget) || 0), active: b.active !== false }); return json(res, 200, { ok: true, missions: missions.list() }); } @@ -155,7 +159,7 @@ const server = http.createServer(async (req, res) => { if (p === '/api/admin/faucet/tick' && req.method === 'POST') { const r = await faucet.tick(notify); return json(res, 200, Object.assign(r, { balance: await faucet.balance() })); } if (p === '/api/admin/embed-test') { // mint a token for any mission so the embed can be tried without a hunter const m = missions.get(String(u.searchParams.get('id') || '')); if (!m) return json(res, 404, { error: 'no such mission' }); - const t = missions.issue(0, m.id); return json(res, 200, { url: m.url + (m.url.includes('?') ? '&' : '?') + 'ph=' + t.t, token: t.t, dwell: m.dwell }); + const t = missions.issue(0, m.id); return json(res, 200, { url: m.url.includes('{token}') ? m.url.replace('{token}', t.t) : m.url + (m.url.includes('?') ? '&' : '?') + 'ph=' + t.t, token: t.t, dwell: m.dwell }); } return json(res, 404, { error: 'No such admin call.' }); }