require('dotenv').config(); const { Bot, webhookCallback, InlineKeyboard } = require('grammy'); const db = require('./db'); const { spin } = require('./games/slots'); const { rollDice } = require('./games/dice'); const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN); const WEBAPP_URL = process.env.WEBAPP_URL || 'https://your-space.hf.space'; // ── Prices in Telegram Stars (XTR) ────────────────────────────────────────── const CREDIT_PACKAGES = [ { stars: 10, credits: 100, label: '💫 100 Credits — ⭐ 10 Stars' }, { stars: 25, credits: 300, label: '💎 300 Credits — ⭐ 25 Stars' }, { stars: 50, credits: 700, label: '🏆 700 Credits — ⭐ 50 Stars' }, ]; // ── Native Telegram Gifts Cache ────────────────────────────────────────── let cachedGifts = []; async function refreshGifts() { try { cachedGifts = await bot.api.getAvailableGifts(); console.log(`🎁 Loaded ${cachedGifts.length} native Telegram gifts`); } catch (e) { console.error('❌ Failed to fetch available gifts:', e.message); } } refreshGifts(); setInterval(refreshGifts, 1000 * 60 * 60); // Refresh hourly // ── Helpers ────────────────────────────────────────────────────────────────── function ensureUser(ctx) { const u = ctx.from; return db.upsertUser(u.id, u.username, u.first_name); } function balanceBar(balance) { return `💰 Your balance: **${balance} credits**`; } // ── /start ─────────────────────────────────────────────────────────────────── bot.command('start', async (ctx) => { const user = ensureUser(ctx); const keyboard = new InlineKeyboard() .webApp('🎮 Open Gift Bot', WEBAPP_URL).row() .text('💰 Buy Credits', 'buy_credits').text('🏆 Leaderboard', 'leaderboard').row() .text('📊 My Stats', 'my_stats').text('❓ Help', 'help'); await ctx.reply( `🎁 *Welcome to Gift Bot, ${ctx.from.first_name}!*\n\n` + `🎰 Spin the slots, 🎲 bet on dice, 🎁 send gifts!\n` + `💡 Buy credits with Telegram Stars and play to win!\n\n` + `${balanceBar(user.balance)}\n\n` + `Use the button below to open the full Mini App 👇`, { parse_mode: 'Markdown', reply_markup: keyboard } ); }); // ── /balance ──────────────────────────────────────────────────────────────── bot.command('balance', async (ctx) => { const user = ensureUser(ctx); await ctx.reply( `💰 *Your Balance*\n\n` + `Credits: **${user.balance}**\n` + `Total Won: ${user.total_won} | Lost: ${user.total_lost} | Bets: ${user.total_bets}`, { parse_mode: 'Markdown' } ); }); // ── /spin [amount] ─────────────────────────────────────────────────────────── bot.command('spin', async (ctx) => { const user = ensureUser(ctx); if (user.pending_prize_value) { return ctx.reply(`🎁 You have a pending gift: **${user.pending_prize_name}**!\n\nUse the buttons below to Claim or Retry.`, { parse_mode: 'Markdown', reply_markup: new InlineKeyboard().text('✅ Claim', 'claim_prize').text('🔄 Retry', 'retry_prize') }); } const parts = ctx.message.text.trim().split(/\s+/); const bet = parseInt(parts[1]) || 10; if (bet < 5) return ctx.reply('❌ Minimum bet is 5 credits.'); if (user.balance < bet) return ctx.reply(`❌ Not enough credits! You have ${user.balance} credits.\nUse /buy to top up.`); db.deductBalance(user.tg_id, bet, `Slot spin bet`); const result = spin(bet, false); db.recordBetWin(user.tg_id, result.isWin ? result.payout : 0, bet); if (result.isWin) { // Find a real gift that matches or pick a random one from cache const targetGift = cachedGifts.find(g => g.name === result.prizeName) || cachedGifts[0]; db.setPendingPrize(user.tg_id, result.prizeName, result.payout, targetGift?.id); const keyboard = new InlineKeyboard() .text('🎁 Claim Native Gift', 'claim_prize') .text('🔄 Retry (Risk it!)', 'retry_prize'); await ctx.reply( `🎡 *SPINNING THE WHEEL...*\n\n` + `${result.display}\n\n` + `🎉 *WON A NATIVE GIFT: ${result.prizeName}!*\n` + `This will be sent to your *Telegram Profile*!\n\n` + `What would you like to do?`, { parse_mode: 'Markdown', reply_markup: keyboard } ); } else { const updated = db.getUser(user.tg_id); await ctx.reply( `🎰 *SPINNING...*\n\n` + `${result.display}\n\n` + `${result.label}\n` + `Bet: ${bet} credits\n\n` + `${balanceBar(updated.balance)}`, { parse_mode: 'Markdown' } ); } }); // ── /demo ─────────────────────────────────────────────────────────────────── bot.command('demo', async (ctx) => { const result = spin(10, true); await ctx.reply( `🧪 *DEMO SPIN*\n\n` + `${result.display}\n\n` + `${result.isWin ? `🎉 (Demo) Won ${result.prizeName}!` : '😢 (Demo) No luck.'}\n\n` + `💡 This was a free spin. Try /spin for real!`, { parse_mode: 'Markdown' } ); }); // ── /bet [amount] [guess 1-6] ──────────────────────────────────────────────── bot.command('bet', async (ctx) => { const user = ensureUser(ctx); const parts = ctx.message.text.trim().split(/\s+/); const bet = parseInt(parts[1]) || 10; const guess = parseInt(parts[2]); if (!guess || guess < 1 || guess > 6) return ctx.reply('❌ Usage: /bet \nExample: /bet 20 3'); if (bet < 5) return ctx.reply('❌ Minimum bet is 5 credits.'); if (user.balance < bet) return ctx.reply(`❌ Not enough credits! You have ${user.balance}.\nUse /buy to top up.`); db.deductBalance(user.tg_id, bet, `Dice bet`); const result = rollDice(bet, guess); db.recordBetWin(user.tg_id, result.payout, bet); if (result.isWin) { db.addBalance(user.tg_id, result.payout, `Dice win x5`); } const updated = db.getUser(user.tg_id); await ctx.reply( `🎲 *ROLLING...*\n\n` + `You guessed: ${result.guessEmoji}\n` + `Result: ${result.resultEmoji}\n\n` + `${result.label}\n` + `Bet: ${bet} credits${result.isWin ? ` → Won: **${result.payout} credits**` : ''}\n\n` + `${balanceBar(updated.balance)}`, { parse_mode: 'Markdown' } ); }); // /gift command removed as per user request (no credit gifting) // ── /buy ───────────────────────────────────────────────────────────────────── bot.command('buy', async (ctx) => { ensureUser(ctx); const keyboard = new InlineKeyboard(); CREDIT_PACKAGES.forEach((pkg, i) => { keyboard.text(pkg.label, `buy_pkg_${i}`).row(); }); await ctx.reply('⭐ *Buy Credits with Telegram Stars*\n\nChoose a package:', { parse_mode: 'Markdown', reply_markup: keyboard }); }); // ── /leaderboard ───────────────────────────────────────────────────────────── bot.command('leaderboard', async (ctx) => { ensureUser(ctx); const top = db.getLeaderboard(10); const medals = ['🥇', '🥈', '🥉', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; const lines = top.map((u, i) => `${medals[i]} ${u.first_name || u.username || 'User'} — **${u.balance} credits**` ).join('\n'); await ctx.reply(`🏆 *Leaderboard*\n\n${lines || 'No users yet!'}`, { parse_mode: 'Markdown' }); }); // ── /help ───────────────────────────────────────────────────────────────────── bot.command('help', async (ctx) => { ensureUser(ctx); await ctx.reply( `*🎁 Gift Bot Commands*\n\n` + `/start — Welcome screen\n` + `/balance — Check your credits\n` + `/spin [amount] — Spin for real Telegram Gifts\n` + `/bet [amount] [1-6] — Dice betting\n` + `/buy — Top up credits with Stars\n` + `/leaderboard — Top winners\n\n` + `💡 *Prizes:* Real native Telegram profile gifts!`, { parse_mode: 'Markdown' } ); }); // ── Inline Button Callbacks ────────────────────────────────────────────────── bot.callbackQuery('claim_prize', async (ctx) => { const user = ensureUser(ctx); const prize = db.claimPrize(user.tg_id); if (!prize) return ctx.answerCallbackQuery('❌ No prize to claim'); await ctx.answerCallbackQuery(`🎁 Sending native gift...`); try { if (prize.gift_id) { // NATIVE TELEGRAM GIFT SENDING await ctx.api.sendGift(user.tg_id, prize.gift_id, "Congratulations from GiftBot! 🎆"); db.logSentGift(user.tg_id, prize.gift_id, prize.name); await ctx.editMessageText( `🎆 *SUCCESS! GIFT SENT!*\n\n` + `A real **${prize.name}** has been sent to your Telegram Profile!\n` + `Check your *Gifts* section in your profile.\n\n` + balanceBar(db.getUser(user.tg_id).balance), { parse_mode: 'Markdown' } ); } else { // Fallback to credits if no gift_id (old record) await ctx.editMessageText( `✅ *Credits Claimed!*\n\n` + `You received **${prize.val} credits**.\n\n` + balanceBar(db.getUser(user.tg_id).balance), { parse_mode: 'Markdown' } ); } } catch (e) { console.error('sendGift Error:', e); // If error (e.g. PAYMENT_REQUIRED), return credits as fallback so user isn't stuck db.addBalance(user.tg_id, prize.val || 100, "Refund: Gift sending failed"); await ctx.reply(`❌ *Gift Delivery Failed*\n\nError: ${e.message}\n\n> 💡 Note: Bot needs a Star balance and Business subscription.\nWe've credited **100 credits** to your balance as compensation.`, { parse_mode: 'Markdown' }); } }); bot.callbackQuery('retry_prize', async (ctx) => { const user = ensureUser(ctx); if (!user.pending_prize_value) return ctx.answerCallbackQuery('❌ No prize to retry'); const oldPrizeName = user.pending_prize_name; const originalBet = user.pending_prize_value / 10; db.clearPendingPrize(user.tg_id); await ctx.answerCallbackQuery('🔄 Retrying spin...'); await ctx.editMessageText(`🎰 *RE-SPINNING...*`); const result = spin(originalBet, false); if (result.isWin) { db.setPendingPrize(user.tg_id, result.prizeName, result.payout); const keyboard = new InlineKeyboard() .text('✅ Claim Credits', 'claim_prize') .text('🔄 Retry (Risk it!)', 'retry_prize'); await ctx.editMessageText( `🎰 *RE-SPIN RESULT*\n\n` + `${result.display}\n\n` + `🎉 *WON ${result.prizeName}!*\n` + `Value: **${result.payout} credits**\n\n` + `Claim or Retry?`, { parse_mode: 'Markdown', reply_markup: keyboard } ); } else { const updated = db.getUser(user.tg_id); await ctx.editMessageText( `🎰 *RE-SPIN RESULT*\n\n` + `${result.display}\n\n` + `❌ Lost the retry!\n` + `The ${oldPrizeName} is gone.\n\n` + balanceBar(updated.balance), { parse_mode: 'Markdown' } ); } }); bot.callbackQuery('buy_credits', async (ctx) => { await ctx.answerCallbackQuery(); const keyboard = new InlineKeyboard(); CREDIT_PACKAGES.forEach((pkg, i) => keyboard.text(pkg.label, `buy_pkg_${i}`).row()); await ctx.reply('⭐ *Choose a Credit Package:*', { parse_mode: 'Markdown', reply_markup: keyboard }); }); bot.callbackQuery('leaderboard', async (ctx) => { await ctx.answerCallbackQuery(); const top = db.getLeaderboard(10); const medals = ['🥇', '🥈', '🥉', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; const lines = top.map((u, i) => `${medals[i]} ${u.first_name || u.username || 'User'} — **${u.balance}**` ).join('\n'); await ctx.reply(`🏆 *Leaderboard*\n\n${lines || 'No players yet!'}`, { parse_mode: 'Markdown' }); }); bot.callbackQuery('my_stats', async (ctx) => { await ctx.answerCallbackQuery(); const user = ensureUser(ctx); await ctx.reply( `📊 *Your Stats*\n\n` + `💰 Balance: ${user.balance} credits\n` + `🏆 Total Won: ${user.total_won}\n` + `💸 Total Lost: ${user.total_lost}\n` + `🎲 Total Bets: ${user.total_bets}`, { parse_mode: 'Markdown' } ); }); bot.callbackQuery('help', async (ctx) => { await ctx.answerCallbackQuery(); await ctx.reply( `*🎁 Gift Bot Commands*\n\n/start /balance /spin /bet /gift /buy /leaderboard /help`, { parse_mode: 'Markdown' } ); }); // ── Buy Package — send Stars invoice ───────────────────────────────────────── for (let i = 0; i < CREDIT_PACKAGES.length; i++) { bot.callbackQuery(`buy_pkg_${i}`, async (ctx) => { await ctx.answerCallbackQuery(); const pkg = CREDIT_PACKAGES[i]; await ctx.replyWithInvoice( `Buy ${pkg.credits} Credits`, `Top up your Gift Bot wallet with ${pkg.credits} credits and start playing!`, `credits_${i}`, // payload 'XTR', // Telegram Stars currency [{ label: `${pkg.credits} Credits`, amount: pkg.stars }] ); }); } // ── Pre-checkout: always approve ───────────────────────────────────────────── bot.on('pre_checkout_query', async (ctx) => { await ctx.answerPreCheckoutQuery(true); }); // ── Successful payment: credit user ────────────────────────────────────────── bot.on('message:successful_payment', async (ctx) => { const payload = ctx.message.successful_payment.invoice_payload; const stars = ctx.message.successful_payment.total_amount; const user = ensureUser(ctx); const pkgIndex = parseInt(payload.replace('credits_', '')); const pkg = CREDIT_PACKAGES[pkgIndex]; if (pkg) { db.addBalance(user.tg_id, pkg.credits, `Bought ${pkg.credits} credits for ${stars} Stars`); const updated = db.getUser(user.tg_id); await ctx.reply( `✅ *Payment Successful!*\n\n` + `You received **${pkg.credits} credits**!\n\n` + `${balanceBar(updated.balance)}`, { parse_mode: 'Markdown' } ); } }); module.exports = bot;