File size: 10,250 Bytes
7b55b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eac398e
 
 
 
 
 
7b55b01
eac398e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b55b01
 
 
eac398e
 
 
 
 
 
7b55b01
eac398e
 
7b55b01
eac398e
 
 
 
 
 
 
 
 
7b55b01
eac398e
 
 
 
7b55b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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`)
});