require('dotenv').config(); const express = require('express'); const cors = require('cors'); const crypto = require('crypto'); const path = require('path'); const bot = require('./bot'); const db = require('./db'); const { spin } = require('./games/slots'); const { rollDice } = require('./games/dice'); const app = express(); const PORT = process.env.PORT || 7860; const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; const ADMIN_IDS = (process.env.ADMIN_IDS || '').split(',').map(s => s.trim()); app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // ── Validate Telegram initData ─────────────────────────────────────────────── function validateInitData(initData) { try { const params = new URLSearchParams(initData); const hash = params.get('hash'); params.delete('hash'); const dataCheckString = [...params.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k}=${v}`) .join('\n'); const secret = crypto.createHmac('sha256', 'WebAppData').update(BOT_TOKEN).digest(); const expectedHash = crypto.createHmac('sha256', secret).update(dataCheckString).digest('hex'); return hash === expectedHash; } catch { return false; } } function parseInitData(initData) { const params = new URLSearchParams(initData); const userStr = params.get('user'); if (!userStr) return null; return JSON.parse(userStr); } // ── Auth middleware ────────────────────────────────────────────────────────── function authMiddleware(req, res, next) { const initData = req.headers['x-init-data']; if (!initData) return res.status(401).json({ error: 'Missing initData' }); // Dev mode: skip validation if token not set if (BOT_TOKEN && !validateInitData(initData)) { return res.status(403).json({ error: 'Invalid initData' }); } const tgUser = parseInitData(initData); if (!tgUser) return res.status(400).json({ error: 'No user in initData' }); req.tgUser = tgUser; db.upsertUser(tgUser.id, tgUser.username, tgUser.first_name); next(); } // ── Routes ─────────────────────────────────────────────────────────────────── // Auth check app.post('/api/auth', (req, res) => { const { initData } = req.body; if (!initData) return res.status(400).json({ error: 'Missing initData' }); const valid = !BOT_TOKEN || validateInitData(initData); if (!valid) return res.status(403).json({ error: 'Invalid' }); const tgUser = parseInitData(initData); db.upsertUser(tgUser.id, tgUser.username, tgUser.first_name); res.json({ ok: true, user: tgUser }); }); // Get current user app.get('/api/me', authMiddleware, (req, res) => { const user = db.getUser(req.tgUser.id); res.json(user); }); // Spin slots app.post('/api/spin', authMiddleware, (req, res) => { const { bet, isDemo } = req.body; const betAmount = parseInt(bet) || 10; if (!isDemo && betAmount < 5) return res.status(400).json({ error: 'Minimum bet is 5 credits' }); const user = db.getUser(req.tgUser.id); // If not demo, check balance and if they have a pending prize if (!isDemo) { if (user.pending_prize_value) return res.status(400).json({ error: 'You have a pending gift! Claim or Retry first.' }); if (!user || user.balance < betAmount) return res.status(400).json({ error: 'Insufficient balance' }); db.deductBalance(req.tgUser.id, betAmount, 'Slot spin bet'); } const result = spin(betAmount, !!isDemo); if (!isDemo) { db.recordBetWin(req.tgUser.id, result.isWin ? result.payout : 0, betAmount); if (result.isWin) { // Don't add balance yet, set pending prize db.setPendingPrize(req.tgUser.id, result.prizeName, result.payout); } } const updated = db.getUser(req.tgUser.id); res.json({ ...result, newBalance: updated.balance, hasPending: !!updated.pending_prize_value, pendingPrize: updated.pending_prize_name }); }); // Claim pending prize app.post('/api/claim', async (req, res) => { try { const user = db.getUser(req.tgUser.id); if (!user || (!user.pending_prize_value && !user.pending_gift_id)) { return res.status(400).json({ error: 'No prize to claim' }); } const prize = db.claimPrize(user.tg_id); // Handle native gift fulfillment if (prize.gift_id) { try { // Call bot api directly const bot = require('./bot'); await bot.api.sendGift(user.tg_id, prize.gift_id, "Congratulations from GiftBot! 🎆"); db.logSentGift(user.tg_id, prize.gift_id, prize.name); } catch (e) { console.error('API claim sendGift error:', e.message); // Credit fallback on failure db.addBalance(user.tg_id, 100, "Refund: Gift delivery failed"); } } const updated = db.getUser(user.tg_id); res.json({ success: true, prize, newBalance: updated.balance }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Retry (Gamble) pending prize app.post('/api/retry', async (req, res) => { try { const user = db.getUser(req.tgUser.id); if (!user || (!user.pending_prize_value && !user.pending_gift_id)) { return res.status(400).json({ error: 'No prize to retry' }); } const originalBet = user.pending_prize_value ? user.pending_prize_value / 10 : 10; db.clearPendingPrize(user.tg_id); const result = spin(originalBet, false); if (result.isWin) { // Find a real gift for the re-spin win const bot = require('./bot'); const cachedGifts = await bot.api.getAvailableGifts().catch(() => []); const targetGift = cachedGifts.find(g => g.name === result.prizeName) || (cachedGifts.length ? cachedGifts[0] : null); db.setPendingPrize(user.tg_id, result.prizeName, result.payout, targetGift?.id); } const updated = db.getUser(user.tg_id); res.json({ ...result, newBalance: updated.balance }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Dice bet app.post('/api/bet', authMiddleware, (req, res) => { const { bet, guess } = req.body; const betAmount = parseInt(bet) || 10; const guessNum = parseInt(guess); if (betAmount < 5) return res.status(400).json({ error: 'Minimum bet is 5 credits' }); if (!guessNum || guessNum < 1 || guessNum > 6) return res.status(400).json({ error: 'Guess must be 1-6' }); const user = db.getUser(req.tgUser.id); if (!user || user.balance < betAmount) return res.status(400).json({ error: 'Insufficient balance' }); db.deductBalance(req.tgUser.id, betAmount, 'Dice bet'); const result = rollDice(betAmount, guessNum); db.recordBetWin(req.tgUser.id, result.payout, betAmount); if (result.isWin) { db.addBalance(req.tgUser.id, result.payout, 'Dice win x5'); } const updated = db.getUser(req.tgUser.id); res.json({ ...result, newBalance: updated.balance }); }); // Send gift app.post('/api/gift', authMiddleware, (req, res) => { const { toUsername, amount } = req.body; const giftAmount = parseInt(amount); if (!toUsername || !giftAmount || giftAmount < 1) { return res.status(400).json({ error: 'Invalid gift parameters' }); } const sender = db.getUser(req.tgUser.id); if (!sender || sender.balance < giftAmount) return res.status(400).json({ error: 'Insufficient balance' }); const cleaned = toUsername.replace('@', ''); const allUsers = db.getAllUsers(); const target = allUsers.find(u => u.username === cleaned); if (!target) return res.status(404).json({ error: 'User not found. They must start the bot first.' }); if (target.tg_id === req.tgUser.id) return res.status(400).json({ error: 'Cannot gift yourself' }); db.deductBalance(req.tgUser.id, giftAmount, `Gift to @${cleaned}`); db.addBalance(target.tg_id, giftAmount, `Gift from @${req.tgUser.username || 'someone'}`); db.logGift(req.tgUser.id, target.tg_id, req.tgUser.username, cleaned, giftAmount); // Notify recipient via bot bot.api.sendMessage(target.tg_id, `🎁 *You received a gift!*\n\n@${req.tgUser.username || req.tgUser.first_name} sent you *${giftAmount} credits*!`, { parse_mode: 'Markdown' } ).catch(() => { }); const updated = db.getUser(req.tgUser.id); res.json({ ok: true, newBalance: updated.balance }); }); // Transaction history app.get('/api/history', authMiddleware, (req, res) => { const history = db.getHistory(req.tgUser.id, 30); res.json(history); }); // Leaderboard app.get('/api/leaderboard', (req, res) => { const board = db.getLeaderboard(10); res.json(board); }); // Admin stats (protected by tg_id) app.get('/api/admin/stats', authMiddleware, (req, res) => { if (!ADMIN_IDS.includes(String(req.tgUser.id))) { return res.status(403).json({ error: 'Forbidden' }); } res.json(db.getStats()); }); // Admin: all users app.get('/api/admin/users', authMiddleware, (req, res) => { if (!ADMIN_IDS.includes(String(req.tgUser.id))) { return res.status(403).json({ error: 'Forbidden' }); } res.json(db.getAllUsers()); }); // ── Start bot polling + HTTP server ───────────────────────────────────────── app.listen(PORT, () => { console.log(`🌐 Server running on port ${PORT}`); console.log(`🎮 Mini App: http://localhost:${PORT}`); }); bot.start({ onStart: (info) => console.log(`🤖 Bot @${info.username} started`) });