Alexainc
Native Telegram Gifts Integration: sendGift + Real Profile Gifts + Removal of Legacy Gifting
eac398e | 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`) | |
| }); | |