| 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'; |
|
|
| |
| 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' }, |
| ]; |
|
|
| |
| 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); |
|
|
| |
| 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**`; |
| } |
|
|
| |
| 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 } |
| ); |
| }); |
|
|
| |
| 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' } |
| ); |
| }); |
|
|
| |
| 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) { |
| |
| 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' } |
| ); |
| } |
| }); |
|
|
| |
| 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' } |
| ); |
| }); |
|
|
| |
| 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 <amount> <number 1-6>\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' } |
| ); |
| }); |
|
|
| |
|
|
| |
| 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 |
| }); |
| }); |
|
|
| |
| 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' }); |
| }); |
|
|
| |
| 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' } |
| ); |
| }); |
|
|
| |
| 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) { |
| |
| 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 { |
| |
| 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); |
| |
| 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' } |
| ); |
| }); |
|
|
| |
| 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}`, |
| 'XTR', |
| [{ label: `${pkg.credits} Credits`, amount: pkg.stars }] |
| ); |
| }); |
| } |
|
|
| |
| bot.on('pre_checkout_query', async (ctx) => { |
| await ctx.answerPreCheckoutQuery(true); |
| }); |
|
|
| |
| 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; |
|
|