giftgo / server.js
Alexainc
Native Telegram Gifts Integration: sendGift + Real Profile Gifts + Removal of Legacy Gifting
eac398e
Raw
History Blame Contribute Delete
10.3 kB
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`)
});