Spaces:
Sleeping
Sleeping
| require("./logger"); | |
| require("dotenv").config(); | |
| const { TelegramBot } = require("node-telegram-bot-api"); | |
| const { DEFAULT_CHARACTERS } = require("./characters"); | |
| const { initDB, loadDB, saveDB, getUser } = require("./db"); | |
| const TOKEN = process.env.BOT_TOKEN; | |
| const ADMIN_IDS = (process.env.ADMIN_IDS || "").split(",").map((id) => id.trim()); | |
| if (!TOKEN) { | |
| console.error("❌ BOT_TOKEN not set in .env file!"); | |
| process.exit(1); | |
| } | |
| const bot = new TelegramBot(TOKEN, { | |
| polling: true, | |
| request: { | |
| agentOptions: { | |
| family: 4 // Force IPv4 to avoid network resolution errors in cloud environments | |
| } | |
| }, | |
| baseApiUrl: process.env.TELEGRAM_API_PROXY || undefined | |
| }); | |
| let botUsername = ""; | |
| bot.getMe().then((me) => { | |
| botUsername = me.username; | |
| console.log(`🤖 Bot @${botUsername} is ready.`); | |
| }).catch((err) => { | |
| console.error("❌ Failed to get bot details:", err.message || err); | |
| }); | |
| bot.on("polling_error", (error) => { | |
| console.error("⚠️ Polling error:", error.message || error); | |
| }); | |
| process.on("unhandledRejection", (reason, promise) => { | |
| console.error("⚠️ Unhandled Rejection at:", promise, "reason:", reason); | |
| }); | |
| process.on("uncaughtException", (error) => { | |
| console.error("⚠️ Uncaught Exception:", error); | |
| }); | |
| // ─── Constants & Dynamic Configs ────────────────────────────────────────────── | |
| let REQUIRED_PLAYERS = 1; | |
| // Initialize MongoDB and health-check HTTP server | |
| const http = require("http"); | |
| initDB().then(() => { | |
| const dbBoot = loadDB(); | |
| if (typeof dbBoot.requiredPlayers === "number") { | |
| REQUIRED_PLAYERS = dbBoot.requiredPlayers; | |
| } | |
| console.log(`🤖 Loaded REQUIRED_PLAYERS configuration: ${REQUIRED_PLAYERS}`); | |
| }); | |
| const server = http.createServer((req, res) => { | |
| if (req.url === "/health" || req.url === "/") { | |
| res.writeHead(200, { "Content-Type": "text/plain" }); | |
| res.end("OK"); | |
| } else { | |
| res.writeHead(404); | |
| res.end(); | |
| } | |
| }); | |
| const PORT = process.env.PORT || 10000; | |
| server.listen(PORT, () => { | |
| console.log(`📡 Health-check web server listening on port ${PORT}`); | |
| }); | |
| const STARTING_COINS = 500; | |
| const BID_TIMER_SECONDS = 15; | |
| const CHARACTERS_PER_SESSION = 5; | |
| const NEXT_CHARACTER_DELAY_MS = 20000; // 20-second delay between characters | |
| // ─── In-memory session state ────────────────────────────────────────────────── | |
| // sessions[chatId] = { | |
| // phase: "waiting" | "bidding" | "inactive", | |
| // readyUsers: Set of userIds, | |
| // participants: { userId: { username, coins } }, | |
| // characters: [...], | |
| // currentCharacterIndex: 0, | |
| // currentHighestBid: null, // { userId, username, amount } | |
| // timer: null (setTimeout handle), | |
| // timerMessageId: null, | |
| // timerInterval: null, | |
| // secondsLeft: 0, | |
| // } | |
| const sessions = {}; | |
| const mediaGroupAccumulators = {}; | |
| const mediaGroupBuffer = {}; | |
| // ─── Helpers ────────────────────────────────────────────────────────────────── | |
| function isAdmin(userId) { | |
| return ADMIN_IDS.includes(String(userId)); | |
| } | |
| function getAllCharacters(db) { | |
| const deletedIds = db.deletedDefaultIds || []; | |
| const activeDefaults = DEFAULT_CHARACTERS.filter((c) => !deletedIds.includes(c.id)); | |
| return [...activeDefaults, ...(db.customCharacters || [])]; | |
| } | |
| function shuffleArray(arr) { | |
| const a = [...arr]; | |
| for (let i = a.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [a[i], a[j]] = [a[j], a[i]]; | |
| } | |
| return a; | |
| } | |
| function getSession(chatId) { | |
| if (!sessions[chatId]) { | |
| sessions[chatId] = { phase: "inactive" }; | |
| } | |
| return sessions[chatId]; | |
| } | |
| function formatCoins(n) { | |
| return `💰 ${n} coins`; | |
| } | |
| function trackMessage(chatId, message) { | |
| const session = getSession(chatId); | |
| if (session && session.phase !== "inactive") { | |
| session.messagesToDelete = session.messagesToDelete || []; | |
| if (typeof message === "number" || typeof message === "string") { | |
| session.messagesToDelete.push(Number(message)); | |
| } else if (message && message.message_id) { | |
| session.messagesToDelete.push(message.message_id); | |
| } | |
| } | |
| } | |
| function deleteAfterDelay(chatId, messageId, delayMs = 3000) { | |
| setTimeout(async () => { | |
| try { | |
| await bot.deleteMessage(chatId, messageId); | |
| } catch (_) { } | |
| }, delayMs); | |
| } | |
| function getPrestigeTitle(db, userId) { | |
| const users = Object.entries(db.users); | |
| if (users.length === 0) return ""; | |
| // Sort by wins to find rankings | |
| const sorted = [...users].sort((a, b) => b[1].totalWins - a[1].totalWins); | |
| const rankIndex = sorted.findIndex(([uid]) => uid === userId); | |
| // Find all character ownerships | |
| const allChars = getAllCharacters(db); | |
| const winsMap = {}; | |
| for (const [uid, user] of users) { | |
| const uName = user.username || `User${uid}`; | |
| const charsWon = user.characters || []; | |
| for (const won of charsWon) { | |
| if (!winsMap[won.characterId]) winsMap[won.characterId] = []; | |
| winsMap[won.characterId].push({ userId: uid, username: uName, paidAmount: won.paidAmount || 0 }); | |
| } | |
| } | |
| const ownedCharacterNames = []; | |
| for (const char of allChars) { | |
| const wins = winsMap[char.id]; | |
| if (wins && wins.length > 0) { | |
| const counts = {}; | |
| for (const win of wins) { | |
| if (!counts[win.userId]) counts[win.userId] = { count: 0, maxBid: 0 }; | |
| counts[win.userId].count += 1; | |
| if (win.paidAmount > counts[win.userId].maxBid) { | |
| counts[win.userId].maxBid = win.paidAmount; | |
| } | |
| } | |
| const sortedOwners = Object.entries(counts).sort((a, b) => { | |
| if (b[1].count !== a[1].count) return b[1].count - a[1].count; | |
| return b[1].maxBid - a[1].maxBid; | |
| }); | |
| if (sortedOwners[0] && sortedOwners[0][0] === userId) { | |
| ownedCharacterNames.push(char.name); | |
| } | |
| } | |
| } | |
| let title = ""; | |
| if (rankIndex === 0) { | |
| title += "👑 Server Champ #1 | "; | |
| } else if (rankIndex === 1) { | |
| title += "🥇 #2 | "; | |
| } else if (rankIndex === 2) { | |
| title += "🥈 #3 | "; | |
| } else if (rankIndex > 2 && rankIndex < 10) { | |
| title += `🏅 #${rankIndex + 1} | `; | |
| } | |
| if (ownedCharacterNames.length > 0) { | |
| if (ownedCharacterNames.length === 1) { | |
| title += `[Owner of ${ownedCharacterNames[0]}] `; | |
| } else { | |
| title += `[Owner of ${ownedCharacterNames.length} actresses] `; | |
| } | |
| } | |
| return title.trim(); | |
| } | |
| function generateTextTable(headers, widths, alignments, rows) { | |
| let result = ""; | |
| function formatCell(str, width, align) { | |
| let cleanStr = String(str); | |
| if (cleanStr.length > width) { | |
| cleanStr = cleanStr.substring(0, width - 1) + "…"; | |
| } | |
| if (align === "right") { | |
| return cleanStr.padStart(width, " "); | |
| } else { | |
| return cleanStr.padEnd(width, " "); | |
| } | |
| } | |
| // Draw Header | |
| const headerCells = headers.map((h, i) => formatCell(h, widths[i], alignments[i])); | |
| result += headerCells.join(" | ") + "\n"; | |
| // Draw Separator line | |
| const separators = widths.map((w) => "-".repeat(w)); | |
| result += separators.join("-+-") + "\n"; | |
| // Draw Rows | |
| for (const row of rows) { | |
| const rowCells = row.map((cell, i) => formatCell(cell, widths[i], alignments[i])); | |
| result += rowCells.join(" | ") + "\n"; | |
| } | |
| return `\`\`\`\n${result}\`\`\``; | |
| } | |
| function findUserByIdOrUsername(db, input) { | |
| const cleanInput = input.trim().replace(/^@/, ""); | |
| if (db.users[cleanInput]) { | |
| return { userId: cleanInput, user: db.users[cleanInput] }; | |
| } | |
| const match = Object.entries(db.users).find(([uid, u]) => { | |
| const dbUser = u.username ? u.username.replace(/^@/, "") : ""; | |
| return dbUser.toLowerCase() === cleanInput.toLowerCase(); | |
| }); | |
| if (match) { | |
| return { userId: match[0], user: match[1] }; | |
| } | |
| return null; | |
| } | |
| function buildSelectionKeyboard(groupId, ownerId, allChars, userSet) { | |
| const keyboard = []; | |
| // 2 characters per row | |
| for (let i = 0; i < allChars.length; i += 2) { | |
| const row = []; | |
| const char1 = allChars[i]; | |
| const isSelected1 = userSet.has(char1.id); | |
| row.push({ | |
| text: `${isSelected1 ? "✅ " : ""}${char1.name}`, | |
| callback_data: `sel:${groupId}:${ownerId}:${char1.id}` | |
| }); | |
| if (i + 1 < allChars.length) { | |
| const char2 = allChars[i + 1]; | |
| const isSelected2 = userSet.has(char2.id); | |
| row.push({ | |
| text: `${isSelected2 ? "✅ " : ""}${char2.name}`, | |
| callback_data: `sel:${groupId}:${ownerId}:${char2.id}` | |
| }); | |
| } | |
| keyboard.push(row); | |
| } | |
| // Done button at the bottom | |
| keyboard.push([ | |
| { | |
| text: "🏁 Done Selecting", | |
| callback_data: `done:${groupId}:${ownerId}` | |
| } | |
| ]); | |
| return keyboard; | |
| } | |
| async function checkLobbyReadyAndStart(chatId) { | |
| console.log(`[LOBBY] checkLobbyReadyAndStart: Checking lobby state in chat: ${chatId}`); | |
| const session = getSession(chatId); | |
| if (!session || session.phase !== "waiting") return; | |
| const count = session.readyUsers.size; | |
| if (count < REQUIRED_PLAYERS) { | |
| console.log(`[LOBBY] checkLobbyReadyAndStart: Only ${count}/${REQUIRED_PLAYERS} players ready in chat: ${chatId}`); | |
| return; | |
| } | |
| // Check if everyone who is in readyUsers is done selecting | |
| let allDone = true; | |
| for (const uid of session.readyUsers.keys()) { | |
| if (!session.userSelectionsDone || !session.userSelectionsDone[uid]) { | |
| allDone = false; | |
| break; | |
| } | |
| } | |
| if (allDone) { | |
| if (session.lobbyTimer) { | |
| clearTimeout(session.lobbyTimer); | |
| session.lobbyTimer = null; | |
| } | |
| console.log(`[LOBBY] checkLobbyReadyAndStart: All selections done in chat ${chatId}. Initializing bidding session...`); | |
| const db = loadDB(); | |
| // Start the bidding session! | |
| session.phase = "bidding"; | |
| session.participants = {}; | |
| session.readyUsers.forEach((uname, uid) => { | |
| const user = getUser(db, uid, uname); | |
| const globalCoins = user.coins || 0; | |
| session.participants[uid] = { | |
| username: uname, | |
| coins: STARTING_COINS + globalCoins, | |
| actressesWonCount: 0, | |
| amountSpent: 0 | |
| }; | |
| }); | |
| session.readyUsers = new Map(); | |
| const allChars = getAllCharacters(db); | |
| // Gather all unique character selections from lobby participants | |
| const selectedIds = new Set(); | |
| if (session.userSelections) { | |
| Object.entries(session.userSelections).forEach(([uid, charSet]) => { | |
| if (session.participants[uid] && charSet instanceof Set) { | |
| charSet.forEach((cid) => selectedIds.add(cid)); | |
| } | |
| }); | |
| } | |
| const selectedChars = allChars.filter((c) => selectedIds.has(c.id)); | |
| const remainingChars = allChars.filter((c) => !selectedIds.has(c.id)); | |
| let pool = [...selectedChars]; | |
| // Pad with random characters if there are less than 5 | |
| if (pool.length < 5) { | |
| const needed = Math.min(5 - pool.length, remainingChars.length); | |
| const randomSelection = shuffleArray(remainingChars).slice(0, needed); | |
| pool.push(...randomSelection); | |
| console.log(`[LOBBY] Padding session with ${needed} random actresses to reach minimum of 5.`); | |
| } | |
| pool = shuffleArray(pool); | |
| session.characters = pool; | |
| session.currentCharacterIndex = 0; | |
| session.currentHighestBid = null; | |
| session.userSelections = {}; // Clear selections for next session | |
| session.userSelectionsDone = {}; // Clear done flags | |
| const playerList = Object.entries(session.participants) | |
| .map(([uid, p]) => { | |
| const title = getPrestigeTitle(db, uid); | |
| const titleStr = title ? `[${title}] ` : ""; | |
| return `• ${titleStr}${p.username}`; | |
| }) | |
| .join("\n"); | |
| console.log(`[SESSION] Starting auction session in chat ${chatId} with participants:`, Object.values(session.participants).map(p => p.username)); | |
| const startMsg = await bot.sendMessage( | |
| chatId, | |
| `🎉 *AUCTION STARTS NOW!*\n\n` + | |
| `👥 *Players:*\n${playerList}\n\n` + | |
| `💰 Each player starts with *${STARTING_COINS}$*\n` + | |
| `🎴 *${pool.length} actresses* up for auction!\n\n` + | |
| `_Get ready..._ 🥁`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| trackMessage(chatId, startMsg); | |
| setTimeout(() => startNextCharacter(chatId), 3000); | |
| } else { | |
| console.log(`[LOBBY] checkLobbyReadyAndStart: Waiting for some players to complete their actress selections in chat: ${chatId}`); | |
| } | |
| } | |
| // ─── Timer logic ────────────────────────────────────────────────────────────── | |
| async function startBidTimer(chatId) { | |
| try { | |
| const session = getSession(chatId); | |
| // Clear any existing timer | |
| clearBidTimer(session); | |
| // Delete the previous countdown message to keep the active screen clutter-free | |
| if (session.timerMessageId) { | |
| try { | |
| await bot.deleteMessage(chatId, session.timerMessageId); | |
| } catch (_) { } | |
| session.timerMessageId = null; | |
| } | |
| session.secondsLeft = BID_TIMER_SECONDS; | |
| // Post a countdown message | |
| const char = session.characters[session.currentCharacterIndex]; | |
| const highBid = session.currentHighestBid; | |
| console.log(`[TIMER] Starting bid timer for ${BID_TIMER_SECONDS}s in chat: ${chatId} for character: ${char.name}. Highest bid:`, highBid); | |
| let timerMsg; | |
| try { | |
| timerMsg = await bot.sendMessage( | |
| chatId, | |
| buildTimerText(session.secondsLeft, highBid, char) | |
| ); | |
| session.timerMessageId = timerMsg.message_id; | |
| trackMessage(chatId, timerMsg); | |
| } catch (err) { | |
| console.error("❌ Failed to start bid timer (sendMessage failed):", err.message || err); | |
| return; | |
| } | |
| // Update at specific intervals to avoid Telegram rate limits (429) on editing same message | |
| session.timerInterval = setInterval(async () => { | |
| session.secondsLeft -= 1; | |
| const shouldUpdateText = | |
| session.secondsLeft % 5 === 0 || | |
| session.secondsLeft <= 3; | |
| if (shouldUpdateText || session.secondsLeft <= 0) { | |
| try { | |
| await bot.editMessageText( | |
| buildTimerText(session.secondsLeft, session.currentHighestBid, char), | |
| { chat_id: chatId, message_id: session.timerMessageId } | |
| ); | |
| } catch (_) { } | |
| } | |
| if (session.secondsLeft <= 0) { | |
| console.log(`[TIMER] Timer expired for character: ${char.name} in chat: ${chatId}`); | |
| clearInterval(session.timerInterval); | |
| session.timerInterval = null; | |
| try { | |
| await resolveBid(chatId); | |
| } catch (err) { | |
| console.error("❌ Error resolving bid:", err); | |
| } | |
| } | |
| }, 1000); | |
| } catch (err) { | |
| console.error("❌ Error starting bid timer:", err); | |
| } | |
| } | |
| function clearBidTimer(session) { | |
| if (session.timerInterval) { | |
| console.log(`[TIMER] Clearing active timer interval`); | |
| clearInterval(session.timerInterval); | |
| session.timerInterval = null; | |
| } | |
| } | |
| function buildTimerText(seconds, highBid, char) { | |
| const bar = buildProgressBar(seconds, BID_TIMER_SECONDS); | |
| if (!highBid) { | |
| return `⏳ *No bids yet!* Place a bid before time runs out!\n${bar} ${seconds}s`; | |
| } | |
| const titlePrefix = highBid.title ? `[${highBid.title}] ` : ""; | |
| return ( | |
| `Highest Bid: ${highBid.amount}$ by ${highBid.username}\n` + | |
| `${bar} ${seconds}s remaining` | |
| ); | |
| } | |
| function buildProgressBar(current, total) { | |
| const filled = Math.round((current / total) * 10); | |
| return "🟩".repeat(filled) + "⬛".repeat(10 - filled); | |
| } | |
| // ─── Bid resolution ─────────────────────────────────────────────────────────── | |
| async function resolveBid(chatId) { | |
| try { | |
| const session = getSession(chatId); | |
| if (session.phase !== "bidding" || session.resolving) return; | |
| session.resolving = true; // Lock session during resolution transition | |
| const char = session.characters[session.currentCharacterIndex]; | |
| const highBid = session.currentHighestBid; | |
| console.log(`[BID] Resolving bid in chat ${chatId} for character: ${char.name}. Highest bid:`, highBid); | |
| const db = loadDB(); | |
| const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []); | |
| let selectedImage = session.currentCharacterImage || (imageUrls.length > 0 ? char.image_url : null); | |
| if (imageUrls.length > 1 && selectedImage) { | |
| const currentIndex = imageUrls.findIndex((img) => { | |
| const curFileId = typeof selectedImage === "object" && selectedImage !== null ? selectedImage.file_id : selectedImage; | |
| const imgFileId = typeof img === "object" && img !== null ? img.file_id : img; | |
| return curFileId === imgFileId; | |
| }); | |
| const otherImages = imageUrls.filter((_, idx) => idx !== currentIndex); | |
| if (otherImages.length > 0) { | |
| selectedImage = otherImages[Math.floor(Math.random() * otherImages.length)]; | |
| } | |
| } | |
| let fileId = selectedImage; | |
| if (typeof selectedImage === "object" && selectedImage !== null) { | |
| fileId = selectedImage.file_id; | |
| } | |
| const hasImage = fileId && imageUrls.length > 0; | |
| if (!highBid) { | |
| console.log(`[BID] UNSOLD: Character "${char.name}" in chat ${chatId}`); | |
| const unsoldText = `😔 *${char.name}* goes *UNSOLD!* No one placed a bid in time.`; | |
| if (hasImage) { | |
| try { | |
| const unsoldMsg = await bot.sendPhoto(chatId, fileId, { | |
| caption: unsoldText, | |
| parse_mode: "Markdown" | |
| }); | |
| trackMessage(chatId, unsoldMsg); | |
| } catch (err) { | |
| console.warn(`[BID] Failed to send photo for unsold character "${char.name}", sending text backup:`, err.message); | |
| const unsoldMsg = await bot.sendMessage(chatId, unsoldText, { parse_mode: "Markdown" }); | |
| trackMessage(chatId, unsoldMsg); | |
| } | |
| } else { | |
| const unsoldMsg = await bot.sendMessage(chatId, unsoldText, { parse_mode: "Markdown" }); | |
| trackMessage(chatId, unsoldMsg); | |
| } | |
| } else { | |
| console.log(`[BID] SOLD: Character "${char.name}" sold to ${highBid.username} (ID: ${highBid.userId}) for ${highBid.amount} coins in chat ${chatId}`); | |
| // Deduct coins from winner's session balance | |
| session.participants[highBid.userId].coins -= highBid.amount; | |
| session.participants[highBid.userId].actressesWonCount += 1; | |
| session.participants[highBid.userId].amountSpent += highBid.amount; | |
| // Save to persistent DB | |
| const winner = getUser(db, highBid.userId, highBid.username); | |
| winner.totalWins += 1; | |
| winner.characters.push({ | |
| characterId: char.id, | |
| characterName: char.name, | |
| paidAmount: highBid.amount, | |
| wonAt: new Date().toISOString(), | |
| }); | |
| // Deduct bid from global persistent wallet balance | |
| const oldGlobalCoins = winner.coins || 0; | |
| winner.coins = Math.max(0, oldGlobalCoins - highBid.amount); | |
| saveDB(db); | |
| console.log(`[BID] Global wallet deduction for ${highBid.username}: ${oldGlobalCoins} -> ${winner.coins}`); | |
| const title = getPrestigeTitle(db, highBid.userId); | |
| const titlePrefix = title ? `[${title}] ` : ""; | |
| const soldText = | |
| `🎉 *${char.name}* SOLD to *${titlePrefix}${highBid.username}* for ${formatCoins(highBid.amount)}!\n\n` + | |
| `💼 Remaining session coins: ${formatCoins(session.participants[highBid.userId].coins)}\n` + | |
| `💰 Global wallet balance: ${formatCoins(winner.coins)}`; | |
| if (hasImage) { | |
| try { | |
| const soldMsg = await bot.sendPhoto(chatId, fileId, { | |
| caption: soldText, | |
| parse_mode: "Markdown" | |
| }); | |
| trackMessage(chatId, soldMsg); | |
| } catch (err) { | |
| console.warn(`[BID] Failed to send photo for sold character "${char.name}", sending text backup:`, err.message); | |
| const soldMsg = await bot.sendMessage(chatId, soldText, { parse_mode: "Markdown" }); | |
| trackMessage(chatId, soldMsg); | |
| } | |
| } else { | |
| const soldMsg = await bot.sendMessage(chatId, soldText, { parse_mode: "Markdown" }); | |
| trackMessage(chatId, soldMsg); | |
| } | |
| } | |
| // Move to next character | |
| session.currentCharacterIndex += 1; | |
| session.currentHighestBid = null; | |
| if (session.currentCharacterIndex >= session.characters.length) { | |
| console.log(`[SESSION] All characters processed in chat ${chatId}. Ending session...`); | |
| await endSession(chatId); | |
| } else { | |
| const nextChar = session.characters[session.currentCharacterIndex]; | |
| const delayMsg = await bot.sendMessage( | |
| chatId, | |
| `⏳ *Next Bid in 20s* — Get ready for *${nextChar.name}*!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| trackMessage(chatId, delayMsg); | |
| console.log(`[SESSION] Queueing next character "${nextChar.name}" in 20 seconds for chat ${chatId}`); | |
| // 20 seconds delay before next character | |
| setTimeout(async () => { | |
| try { | |
| await startNextCharacter(chatId); | |
| } catch (err) { | |
| console.error("❌ Error starting next character:", err); | |
| } | |
| }, NEXT_CHARACTER_DELAY_MS); | |
| } | |
| } catch (err) { | |
| console.error("❌ Error resolving bid:", err); | |
| } | |
| } | |
| // ─── Session flow ───────────────────────────────────────────────────────────── | |
| async function startNextCharacter(chatId) { | |
| try { | |
| const session = getSession(chatId); | |
| if (session.phase !== "bidding") return; | |
| session.resolving = false; // Unlock session for new bids | |
| const char = session.characters[session.currentCharacterIndex]; | |
| if (!char) { | |
| console.warn("⚠️ No character found at index:", session.currentCharacterIndex); | |
| await endSession(chatId); | |
| return; | |
| } | |
| const total = session.characters.length; | |
| const idx = session.currentCharacterIndex + 1; | |
| console.log(`[SESSION] startNextCharacter: Character ${idx}/${total} ("${char.name}") in chat ${chatId}`); | |
| const textCaption = | |
| `*${char.name}*\n\n` + | |
| (char.features || []).join("\n") + | |
| `\n\n💵 *Base Price:* ${formatCoins(char.base_price)}\n` + | |
| `📢 Minimum bid: ${formatCoins(char.base_price + 1)}\n\n` + | |
| `_Send your bid amount (e.g. \`120\`) to place a bid!_`; | |
| const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []); | |
| if (imageUrls.length === 0) { | |
| session.photoIndex = null; | |
| session.currentCharacterImage = null; | |
| try { | |
| const textMsg = await bot.sendMessage(chatId, textCaption, { parse_mode: "Markdown" }); | |
| trackMessage(chatId, textMsg); | |
| } catch (err) { | |
| console.error("❌ Failed to send character text:", err); | |
| } | |
| } else { | |
| const initialIndex = Math.floor(Math.random() * imageUrls.length); | |
| session.photoIndex = initialIndex; | |
| const selectedImage = imageUrls[initialIndex]; | |
| session.currentCharacterImage = selectedImage; | |
| let fileId = selectedImage; | |
| let extraCaption = ""; | |
| if (typeof selectedImage === "object" && selectedImage !== null) { | |
| fileId = selectedImage.file_id; | |
| if (selectedImage.caption) { | |
| extraCaption = ` _${selectedImage.caption}_\n\n`; | |
| } | |
| } | |
| const updatedCaption = | |
| `*${char.name}*\n\n` + | |
| extraCaption + | |
| (char.features || []).join("\n") + | |
| `\n\n💵 *Base Price:* ${formatCoins(char.base_price)}\n` + | |
| `📢 Minimum bid: ${formatCoins(char.base_price + 1)}\n\n` + | |
| `_Send your bid amount (e.g. \`120\`) to place a bid!_`; | |
| try { | |
| const photoMsg = await bot.sendPhoto(chatId, fileId, { | |
| caption: updatedCaption, | |
| parse_mode: "Markdown", | |
| }); | |
| trackMessage(chatId, photoMsg); | |
| } catch (err) { | |
| console.warn(`[SESSION] Failed to send character photo for "${char.name}", sending text backup:`, err.message); | |
| try { | |
| const textMsg = await bot.sendMessage(chatId, `🖼️ [Image unavailable]\n\n${textCaption}`, { | |
| parse_mode: "Markdown", | |
| }); | |
| trackMessage(chatId, textMsg); | |
| } catch (err) { | |
| console.error("❌ Failed to send backup text message:", err); | |
| } | |
| } | |
| } | |
| session.currentHighestBid = null; | |
| await startBidTimer(chatId); | |
| } catch (err) { | |
| console.error("❌ Error in startNextCharacter:", err); | |
| } | |
| } | |
| async function endSession(chatId) { | |
| try { | |
| const session = getSession(chatId); | |
| console.log(`[SESSION] endSession: Ending bidding session in chat: ${chatId}`); | |
| clearBidTimer(session); | |
| const db = loadDB(); | |
| // Build final scoreboard sorted by wins (descending) and tie-broken by amount spent (ascending) | |
| const sortedParticipants = Object.entries(session.participants) | |
| .sort((a, b) => { | |
| const winsA = a[1].actressesWonCount || 0; | |
| const winsB = b[1].actressesWonCount || 0; | |
| if (winsB !== winsA) { | |
| return winsB - winsA; | |
| } | |
| const spentA = a[1].amountSpent || 0; | |
| const spentB = b[1].amountSpent || 0; | |
| return spentA - spentB; // Option B: less spent wins the tie-breaker | |
| }); | |
| const lines = sortedParticipants.map(([uid, p], i) => { | |
| const medal = ["🥇", "🥈", "🥉"][i] || `${i + 1}.`; | |
| const title = getPrestigeTitle(db, uid); | |
| const titlePrefix = title ? `[${title}] ` : ""; | |
| return `${medal} *${titlePrefix}${p.username}* — ${p.actressesWonCount} wins (${formatCoins(p.amountSpent)} spent) | ${formatCoins(p.coins)} left`; | |
| }); | |
| // Display the result first | |
| await bot.sendMessage( | |
| chatId, | |
| `🏁 *Auction Session Complete!*\n\n` + | |
| `📊 *Final Standings:*\n${lines.join("\n")}\n\n` + | |
| `Type \`bid start\` to begin a new session!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| // Delete all tracked messages to clear chat clutter after showing results | |
| if (session.messagesToDelete && session.messagesToDelete.length > 0) { | |
| console.log(`[SESSION] Cleaning up ${session.messagesToDelete.length} temporary messages from chat ${chatId}...`); | |
| for (const msgId of session.messagesToDelete) { | |
| try { | |
| await bot.deleteMessage(chatId, msgId); | |
| } catch (_) { } | |
| } | |
| session.messagesToDelete = []; | |
| } | |
| // Update member tags for top 5 users based on global wins | |
| await updateGroupTagsForTopUsers(chatId); | |
| } catch (err) { | |
| console.error("❌ Error in endSession:", err); | |
| } finally { | |
| sessions[chatId] = { phase: "inactive" }; | |
| console.log(`[SESSION] Session state reset to inactive for chat: ${chatId}`); | |
| } | |
| } | |
| // ─── Command Handlers ───────────────────────────────────────────────────────── | |
| // ─── Command Handlers ───────────────────────────────────────────────────────── | |
| // BID START | |
| bot.onText(/^bid start$/i, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const username = msg.from.username | |
| ? `@${msg.from.username}` | |
| : msg.from.first_name || `User${userId}`; | |
| console.log(`[CMD] "bid start" invoked by ${username} (ID: ${userId}) in chat: ${chatId}`); | |
| const session = getSession(chatId); | |
| // Track user's bid start message | |
| trackMessage(chatId, msg); | |
| if (session.phase === "bidding") { | |
| console.log(`[CMD] "bid start" ignored: bidding session already active in chat: ${chatId}`); | |
| const activeMsg = await bot.sendMessage(chatId, "⚠️ A bidding session is already in progress!"); | |
| trackMessage(chatId, activeMsg); | |
| return; | |
| } | |
| if (session.phase !== "waiting") { | |
| session.phase = "waiting"; | |
| session.readyUsers = new Map(); // userId -> username | |
| session.userSelections = {}; // userId -> Set of charIds | |
| session.userSelectionsDone = {}; // userId -> boolean | |
| // Start 5-minute lobby timer | |
| if (session.lobbyTimer) clearTimeout(session.lobbyTimer); | |
| session.lobbyTimer = setTimeout(async () => { | |
| console.log(`[LOBBY] Lobby timeout expired for chat: ${chatId}`); | |
| const currentSession = getSession(chatId); | |
| if (currentSession && currentSession.phase === "waiting") { | |
| if (currentSession.messagesToDelete && currentSession.messagesToDelete.length > 0) { | |
| for (const msgId of currentSession.messagesToDelete) { | |
| try { | |
| await bot.deleteMessage(chatId, msgId); | |
| } catch (_) { } | |
| } | |
| currentSession.messagesToDelete = []; | |
| } | |
| sessions[chatId] = { phase: "inactive" }; | |
| try { | |
| await bot.sendMessage( | |
| chatId, | |
| `⏱️ *Lobby Timed Out!*\n\nThe required number of players (*${REQUIRED_PLAYERS}*) did not join and finalize selections within 5 minutes. The session has been cancelled.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } catch (_) {} | |
| } | |
| }, 5 * 60 * 1000); | |
| } | |
| if (session.readyUsers.has(userId)) { | |
| console.log(`[CMD] "bid start" duplicate sign-up by ${username} in chat: ${chatId}`); | |
| const dupMsg = await bot.sendMessage(chatId, `✅ ${username}, you already signed up for this session!`); | |
| trackMessage(chatId, dupMsg); | |
| return; | |
| } | |
| session.readyUsers.set(userId, username); | |
| session.userSelections[userId] = new Set(); | |
| session.userSelectionsDone[userId] = false; | |
| // Send the character selection in the group chat, but customized for this user | |
| const db = loadDB(); | |
| const allChars = getAllCharacters(db); | |
| const selectionKeyboard = buildSelectionKeyboard(chatId, userId, allChars, new Set()); | |
| console.log(`[CMD] Sending selection menu to player ${username} (ID: ${userId}) in chat: ${chatId}`); | |
| const selMsg = await bot.sendMessage( | |
| chatId, | |
| `🎮 *Character Selection for* ${username}\n` + | |
| `Please select the characters you want to include in this session, then click *Done Selecting*!`, | |
| { | |
| parse_mode: "Markdown", | |
| reply_markup: { inline_keyboard: selectionKeyboard } | |
| } | |
| ); | |
| trackMessage(chatId, selMsg); | |
| const count = session.readyUsers.size; | |
| const remaining = REQUIRED_PLAYERS - count; | |
| if (remaining > 0) { | |
| const countMsg = await bot.sendMessage( | |
| chatId, | |
| `✋ ${username} wants to bid!\n📋 *${count}/${REQUIRED_PLAYERS}* players ready.\n` + | |
| `Need *${remaining}* more — type \`bid start\` to join!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| trackMessage(chatId, countMsg); | |
| } else { | |
| const countMsg = await bot.sendMessage( | |
| chatId, | |
| `📋 *${count}/${REQUIRED_PLAYERS}* players ready.\n` + | |
| `⏱️ Waiting for all players to complete their actress selections!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| trackMessage(chatId, countMsg); | |
| } | |
| // Defer start until selections are finalized | |
| await checkLobbyReadyAndStart(chatId); | |
| }); | |
| // BID AMOUNT (plain number) | |
| bot.on("message", async (msg) => { | |
| if (!msg.text) return; | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const username = msg.from.username | |
| ? `@${msg.from.username}` | |
| : msg.from.first_name || `User${userId}`; | |
| const session = getSession(chatId); | |
| if (session.phase !== "bidding" || session.resolving) return; | |
| // Skip commands | |
| if (msg.text.startsWith("/") || /^bid /i.test(msg.text)) return; | |
| const amount = parseInt(msg.text.trim(), 10); | |
| if (isNaN(amount) || amount <= 0) return; | |
| console.log(`[BID] Received bid request: user=${username} (ID: ${userId}), amount=${amount}, chat=${chatId}`); | |
| // Only participants can bid | |
| if (!session.participants[userId]) { | |
| console.log(`[BID] Bid rejected: user ${username} is not a participant in this session.`); | |
| const err = await bot.sendMessage( | |
| chatId, | |
| `⛔ ${username}, you didn't join this session. Wait for the next one and type \`bid start\`!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| deleteAfterDelay(chatId, msg.message_id); | |
| deleteAfterDelay(chatId, err.message_id); | |
| return; | |
| } | |
| const participant = session.participants[userId]; | |
| const char = session.characters[session.currentCharacterIndex]; | |
| const highBid = session.currentHighestBid; | |
| // Must exceed base price | |
| if (amount <= char.base_price) { | |
| console.log(`[BID] Bid rejected: amount ${amount} <= base price ${char.base_price} for character: ${char.name}`); | |
| const err = await bot.sendMessage( | |
| chatId, | |
| `❌ Minimum bid is *${char.base_price + 1} coins* (base price: ${char.base_price})`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| deleteAfterDelay(chatId, msg.message_id); | |
| deleteAfterDelay(chatId, err.message_id); | |
| return; | |
| } | |
| // Must be higher than current highest bid | |
| if (highBid && amount <= highBid.amount) { | |
| console.log(`[BID] Bid rejected: amount ${amount} <= current highest bid ${highBid.amount}`); | |
| const err = await bot.sendMessage( | |
| chatId, | |
| `❌ You must bid higher than the current bid of *${highBid.amount} coins*!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| deleteAfterDelay(chatId, msg.message_id); | |
| deleteAfterDelay(chatId, err.message_id); | |
| return; | |
| } | |
| // Same person can't outbid themselves | |
| if (highBid && highBid.userId === userId) { | |
| console.log(`[BID] Bid rejected: user ${username} is already the highest bidder.`); | |
| const err = await bot.sendMessage(chatId, `⚠️ You are already the highest bidder!`); | |
| deleteAfterDelay(chatId, msg.message_id); | |
| deleteAfterDelay(chatId, err.message_id); | |
| return; | |
| } | |
| // Must have enough coins | |
| if (amount > participant.coins) { | |
| console.log(`[BID] Bid rejected: amount ${amount} > user coins balance ${participant.coins}`); | |
| const err = await bot.sendMessage( | |
| chatId, | |
| `❌ You only have *${participant.coins} coins*! You can't bid *${amount}*.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| deleteAfterDelay(chatId, msg.message_id); | |
| deleteAfterDelay(chatId, err.message_id); | |
| return; | |
| } | |
| // Valid bid! | |
| console.log(`[BID] Bid accepted: user ${username} is now highest bidder with ${amount} coins.`); | |
| const db = loadDB(); | |
| const title = getPrestigeTitle(db, userId); | |
| session.currentHighestBid = { userId, username, amount, title }; | |
| // Track the user's incoming valid bid message for final session cleanup | |
| trackMessage(chatId, msg); | |
| // Reset timer (sends the combined countdown/highest bid message) | |
| await startBidTimer(chatId); | |
| }); | |
| // /leaderboard | |
| bot.onText(/\/leaderboard/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const username = msg.from.username || msg.from.first_name; | |
| console.log(`[CMD] "/leaderboard" requested by ${username} in chat: ${chatId}`); | |
| const db = loadDB(); | |
| const users = Object.entries(db.users); | |
| if (users.length === 0) { | |
| return bot.sendMessage(msg.chat.id, "📊 No data yet! Start a session with `bid start`", { | |
| parse_mode: "Markdown", | |
| }); | |
| } | |
| const sorted = users.sort((a, b) => b[1].totalWins - a[1].totalWins).slice(0, 10); | |
| const rows = sorted.map(([uid, u], i) => { | |
| const rankStr = ["1st", "2nd", "3rd"][i] || `${i + 1}th`; | |
| const title = getPrestigeTitle(db, uid); | |
| const playerStr = title ? `${u.username} (${title})` : u.username; | |
| const winsStr = String(u.totalWins); | |
| const coinsStr = String(u.coins || 0); | |
| return [rankStr, playerStr, winsStr, coinsStr]; | |
| }); | |
| const tableText = generateTextTable( | |
| ['R', 'Player', 'W', 'Coins'], | |
| [3, 17, 2, 5], | |
| ['left', 'left', 'right', 'right'], | |
| rows | |
| ); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `🏆 *All-Time Leaderboard*\n\n${tableText}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /mystats | |
| bot.onText(/\/mystats/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const username = msg.from.username || msg.from.first_name; | |
| console.log(`[CMD] "/mystats" requested by ${username} (ID: ${userId}) in chat: ${chatId}`); | |
| const db = loadDB(); | |
| const user = db.users[userId]; | |
| if (!user) { | |
| return bot.sendMessage(msg.chat.id, "You haven't participated in any sessions yet!"); | |
| } | |
| let charList = "_None yet_"; | |
| if (user.characters && user.characters.length > 0) { | |
| const grouped = {}; | |
| for (const c of user.characters) { | |
| const key = c.characterId || c.characterName; | |
| if (!grouped[key]) { | |
| grouped[key] = { name: c.characterName, count: 0, totalSpent: 0 }; | |
| } | |
| grouped[key].count += 1; | |
| grouped[key].totalSpent += c.paidAmount; | |
| } | |
| charList = Object.values(grouped) | |
| .map((gc) => `• ${gc.name} (x${gc.count}) (Total Spent: ${gc.totalSpent}$)`) | |
| .join("\n"); | |
| } | |
| const title = getPrestigeTitle(db, userId); | |
| const titleStr = title ? title : "None"; | |
| const statsMessage = | |
| `👤 *YOUR PROFILE STATS* 👤\n` + | |
| `━━━━━━━━━━━━━━━━━━━━━━━━\n` + | |
| `🎖️ *Prestige Title:* _${titleStr}_\n` + | |
| `🏆 *Total Wins:* \`${user.totalWins}\`\n` + | |
| `💰 *Wallet Balance:* \`${user.coins || 0} coins\`\n\n` + | |
| `🎴 *Actresses Owned:*\n${charList}\n` + | |
| `━━━━━━━━━━━━━━━━━━━━━━━━`; | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| statsMessage, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /daily | |
| bot.onText(/\/daily/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const username = msg.from.username | |
| ? `@${msg.from.username}` | |
| : msg.from.first_name || `User${userId}`; | |
| console.log(`[CMD] "/daily" requested by ${username} (ID: ${userId}) in chat: ${chatId}`); | |
| const db = loadDB(); | |
| const user = getUser(db, userId, username); | |
| const now = Date.now(); | |
| const lastDaily = user.lastDaily ? new Date(user.lastDaily).getTime() : 0; | |
| const cooldown = 24 * 60 * 60 * 1000; // 24 hours | |
| if (now - lastDaily < cooldown) { | |
| const timeLeft = cooldown - (now - lastDaily); | |
| const hours = Math.floor(timeLeft / (3600 * 1000)); | |
| const minutes = Math.floor((timeLeft % (3600 * 1000)) / (60 * 1000)); | |
| const seconds = Math.floor((timeLeft % (60 * 1000)) / 1000); | |
| console.log(`[CMD] "/daily" claim rejected: cooldown active for user ${username}. Time left: ${hours}h ${minutes}m ${seconds}s`); | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| `⏳ You have already claimed your daily allowance!\n` + | |
| `Please wait *${hours}h ${minutes}m ${seconds}s* before claiming again.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| user.coins = (user.coins || 0) + 10; | |
| user.lastDaily = new Date().toISOString(); | |
| saveDB(db); | |
| console.log(`[CMD] "/daily" claim successful for user ${username}. Added 10 coins. New balance: ${user.coins}`); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `💰 *Daily Allowance Claimed!*\n\n` + | |
| `Added: *10 coins*\n` + | |
| `Total Wallet Balance: *${user.coins} coins*`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /owners | |
| bot.onText(/\/owners/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const username = msg.from.username || msg.from.first_name; | |
| console.log(`[CMD] "/owners" requested by ${username} in chat: ${chatId}`); | |
| const db = loadDB(); | |
| const allChars = getAllCharacters(db); | |
| // Map of charId -> list of wins | |
| const winsMap = {}; | |
| for (const [userId, user] of Object.entries(db.users)) { | |
| const uName = user.username || `User${userId}`; | |
| const charsWon = user.characters || []; | |
| for (const won of charsWon) { | |
| if (!winsMap[won.characterId]) { | |
| winsMap[won.characterId] = []; | |
| } | |
| winsMap[won.characterId].push({ | |
| userId, | |
| username: uName, | |
| paidAmount: won.paidAmount || 0, | |
| wonAt: won.wonAt || "" | |
| }); | |
| } | |
| } | |
| const rows = []; | |
| for (const char of allChars) { | |
| const wins = winsMap[char.id]; | |
| if (!wins || wins.length === 0) { | |
| rows.push([char.name, 'No owner', '0']); | |
| continue; | |
| } | |
| // Aggregate wins by user | |
| const counts = {}; | |
| for (const win of wins) { | |
| if (!counts[win.userId]) { | |
| counts[win.userId] = { count: 0, maxBid: 0, username: win.username }; | |
| } | |
| counts[win.userId].count += 1; | |
| if (win.paidAmount > counts[win.userId].maxBid) { | |
| counts[win.userId].maxBid = win.paidAmount; | |
| } | |
| } | |
| // Sort by win count, then highest paid amount | |
| const sorted = Object.entries(counts).sort((a, b) => { | |
| if (b[1].count !== a[1].count) { | |
| return b[1].count - a[1].count; | |
| } | |
| return b[1].maxBid - a[1].maxBid; | |
| }); | |
| const [, ownerData] = sorted[0]; | |
| rows.push([char.name, ownerData.username, String(ownerData.count)]); | |
| } | |
| const tableText = generateTextTable( | |
| ['Character', 'Owner', 'Wins'], | |
| [13, 14, 4], | |
| ['left', 'left', 'right'], | |
| rows | |
| ); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `🎴 *Character Ownership* 🏆\n\n${tableText}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /addactress (admin only) | |
| bot.onText(/\/addactress|\/addcharacter/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/addactress" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/addactress" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can add actresses."); | |
| } | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `📝 *Add an Actress* (Admin)\n\n` + | |
| `Choose one of the two methods:\n\n` + | |
| `*Method 1: Send with a Photo (Simpler & Recommended)*\n` + | |
| `Send a photo to the bot with a caption in this simple format:\n` + | |
| `\`\`\`\n` + | |
| `/newactress Actress Name\n` + | |
| `Base Price\n` + | |
| `Feature 1\n` + | |
| `Feature 2\n` + | |
| `\`\`\`\n\n` + | |
| `*Method 2: Add by Text (No Image Required)*\n` + | |
| `Send this text command:\n` + | |
| `\`/submitactress NAME , BASE_PRICE , Feature 1 , Feature 2 , Feature 3\``, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /submitactress (admin only) | |
| bot.onText(/(?:\/submitactress|\/submitchar)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/submitactress" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/submitactress" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can add actresses."); | |
| } | |
| const parts = match[1].split(",").map((s) => s.trim()); | |
| if (parts.length < 2) { | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| "❌ Format: `/submitactress NAME , BASE_PRICE , Feature1 , Feature2 ...`", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| const [name, basePriceStr, ...features] = parts; | |
| const basePrice = parseInt(basePriceStr, 10); | |
| if (!name || isNaN(basePrice)) { | |
| return bot.sendMessage(msg.chat.id, "❌ Invalid format. Check NAME and BASE_PRICE."); | |
| } | |
| console.log(`[CMD] Admin ${userId} adding actress via text: name="${name}", price=${basePrice}`); | |
| const db = loadDB(); | |
| const newChar = { | |
| id: `custom_${Date.now()}`, | |
| name, | |
| image_url: [], // Text additions start with no images/skins | |
| base_price: basePrice, | |
| features, | |
| addedBy: userId, | |
| }; | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(newChar); | |
| saveDB(db); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `✅ *${name}* added to the actress pool!\n💵 Base price: ${basePrice} coins`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /newactress (admin only, sent as a photo caption) | |
| bot.on("photo", async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const caption = msg.caption; | |
| const isNewActress = caption && caption.toLowerCase().startsWith("/newactress"); | |
| const isNewChar = caption && caption.toLowerCase().startsWith("/newchar"); | |
| if (!isNewActress && !isNewChar) return; | |
| console.log(`[CMD] "/newactress" photo flow invoked by user ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/newactress" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can add actresses."); | |
| } | |
| // Split by newline and clean empty lines | |
| const lines = caption | |
| .split("\n") | |
| .map((s) => s.trim()) | |
| .filter(Boolean); | |
| // Expected: | |
| // Line 1: /newactress Actress Name | |
| // Line 2: Base Price | |
| // Line 3+: Features | |
| if (lines.length < 2) { | |
| return bot.sendMessage( | |
| chatId, | |
| "❌ *Invalid format!*\n\n" + | |
| "Please send a photo with a caption using this simple format:\n\n" + | |
| "`/newactress Actress Name`\n" + | |
| "`Base Price`\n" + | |
| "`Feature 1`\n" + | |
| "`Feature 2`\n" + | |
| "`...`", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| const commandPrefix = isNewActress ? /^\/newactress\s*/i : /^\/newchar\s*/i; | |
| const name = lines[0].replace(commandPrefix, "").trim(); | |
| const basePrice = parseInt(lines[1], 10); | |
| const features = lines.slice(2); | |
| if (!name || isNaN(basePrice) || basePrice <= 0) { | |
| return bot.sendMessage( | |
| chatId, | |
| "❌ *Parsing Error!*\n\n" + | |
| "Make sure the first line has the actress name and the second line is a valid number for the base price.", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| console.log(`[CMD] Admin ${userId} parsed new actress from photo caption: name="${name}", price=${basePrice}`); | |
| const photo = msg.photo; | |
| const largestPhoto = photo[photo.length - 1]; | |
| const fileId = largestPhoto.file_id; | |
| try { | |
| const db = loadDB(); | |
| const newChar = { | |
| id: `custom_${Date.now()}`, | |
| name, | |
| image_url: fileId, // Store Telegram file_id as image_url | |
| base_price: basePrice, | |
| features, | |
| addedBy: userId, | |
| }; | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(newChar); | |
| saveDB(db); | |
| await bot.sendMessage( | |
| chatId, | |
| `✅ *${name}* added to the actress pool!\n` + | |
| `💵 Base price: *${formatCoins(basePrice)}*\n` + | |
| `📝 Features recorded: \`${features.length}\``, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } catch (err) { | |
| console.error("❌ Error adding actress:", err); | |
| await bot.sendMessage(chatId, "❌ Failed to add actress due to an internal error."); | |
| } | |
| }); | |
| // /listactress / /listactresses | |
| bot.onText(/\/listactress(?:es)?/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| console.log(`[CMD] "/listactress" requested in chat: ${chatId}`); | |
| const db = loadDB(); | |
| const all = getAllCharacters(db); | |
| const lines = all.map( | |
| (c, i) => `${i + 1}. *${c.name}* ` | |
| ); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `🎴 *Available Actresses (${all.length}):*\n\n${lines.join("\n")}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /actressimages [id] (admin only) | |
| bot.onText(/(?:\/actressimages|\/charimages)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetId = match[1].trim(); | |
| console.log(`[CMD] "/actressimages" invoked by admin ID: ${userId} for actress: ${targetId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/actressimages" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const allChars = getAllCharacters(db); | |
| const char = allChars.find((c) => c.id === targetId); | |
| if (!char) { | |
| return bot.sendMessage(chatId, `❌ Actress with ID \`${targetId}\` not found.`); | |
| } | |
| const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []); | |
| if (imageUrls.length === 0) { | |
| return bot.sendMessage(chatId, `ℹ️ *${char.name}* (\`${targetId}\`) has no images/skins attached yet.`, { parse_mode: "Markdown" }); | |
| } | |
| const media = imageUrls.map((img) => { | |
| let mediaId = img; | |
| let caption = ""; | |
| if (typeof img === "object" && img !== null) { | |
| mediaId = img.file_id; | |
| caption = img.caption || ""; | |
| } | |
| return { | |
| type: "photo", | |
| media: mediaId, | |
| caption: caption || undefined | |
| }; | |
| }); | |
| await bot.sendMessage(chatId, `🖼️ Sending *${imageUrls.length}* images/skins for *${char.name}*...`, { parse_mode: "Markdown" }); | |
| for (let i = 0; i < media.length; i += 10) { | |
| const chunk = media.slice(i, i + 10); | |
| try { | |
| await bot.sendMediaGroup(chatId, chunk); | |
| } catch (err) { | |
| console.error("❌ Failed to send media group chunk:", err.message || err); | |
| for (const item of chunk) { | |
| try { | |
| await bot.sendPhoto(chatId, item.media, { caption: item.caption }); | |
| } catch (photoErr) { | |
| console.warn(`❌ Failed to send photo fallback:`, photoErr.message); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // /cancelsession (admin only) | |
| bot.onText(/\/cancelsession/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/cancelsession" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/cancelsession" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can cancel sessions."); | |
| } | |
| let targetChatId = msg.chat.id; | |
| if (msg.chat.type === "private") { | |
| const db = loadDB(); | |
| targetChatId = (db.connections || {})[userId]; | |
| if (!targetChatId) { | |
| console.log(`[CMD] "/cancelsession" failed: admin ${userId} has no connected group chat.`); | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| "⚠️ You haven't connected any group chat yet. Use `/connect` in the group chat first.", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| } | |
| console.log(`[CMD] Cancelling session in chat ${targetChatId} by admin request.`); | |
| const session = getSession(targetChatId); | |
| clearBidTimer(session); | |
| if (session.lobbyTimer) { | |
| clearTimeout(session.lobbyTimer); | |
| session.lobbyTimer = null; | |
| } | |
| sessions[targetChatId] = { phase: "inactive" }; | |
| // Notify the group chat | |
| try { | |
| await bot.sendMessage(targetChatId, "🛑 Auction session cancelled by admin."); | |
| } catch (_) { } | |
| // Confirm to the admin (if in private DM) | |
| if (msg.chat.type === "private") { | |
| await bot.sendMessage(msg.chat.id, `✅ Cancelled auction session in connected group (\`${targetChatId}\`).`); | |
| } | |
| }); | |
| // /resetbalance [user] | |
| bot.onText(/\/resetbalance\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetInput = match[1].trim(); | |
| console.log(`[CMD] "/resetbalance" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/resetbalance" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const res = findUserByIdOrUsername(db, targetInput); | |
| if (!res) { | |
| console.log(`[CMD] "/resetbalance" failed: target user "${targetInput}" not found.`); | |
| return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`); | |
| } | |
| res.user.coins = 0; | |
| saveDB(db); | |
| console.log(`[ADMIN] Admin ${userId} reset balance of user ${res.userId} (${res.user.username})`); | |
| await bot.sendMessage(chatId, `✅ Reset wallet balance of *${res.user.username}* to 0 coins.`, { parse_mode: "Markdown" }); | |
| }); | |
| // /resetwins [user] | |
| bot.onText(/\/resetwins\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetInput = match[1].trim(); | |
| console.log(`[CMD] "/resetwins" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/resetwins" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const res = findUserByIdOrUsername(db, targetInput); | |
| if (!res) { | |
| console.log(`[CMD] "/resetwins" failed: target user "${targetInput}" not found.`); | |
| return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`); | |
| } | |
| res.user.totalWins = 0; | |
| saveDB(db); | |
| console.log(`[ADMIN] Admin ${userId} reset wins of user ${res.userId} (${res.user.username})`); | |
| await bot.sendMessage(chatId, `✅ Reset total win count of *${res.user.username}* to 0.`, { parse_mode: "Markdown" }); | |
| }); | |
| // /resetactresses [user] | |
| bot.onText(/(?:\/resetactresses|\/resetchars)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetInput = match[1].trim(); | |
| console.log(`[CMD] "/resetactresses" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/resetactresses" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const res = findUserByIdOrUsername(db, targetInput); | |
| if (!res) { | |
| console.log(`[CMD] "/resetactresses" failed: target user "${targetInput}" not found.`); | |
| return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`); | |
| } | |
| res.user.characters = []; | |
| saveDB(db); | |
| console.log(`[ADMIN] Admin ${userId} cleared actresses owned by user ${res.userId} (${res.user.username})`); | |
| await bot.sendMessage(chatId, `✅ Cleared all owned actresses for *${res.user.username}*.`, { parse_mode: "Markdown" }); | |
| }); | |
| // /givemoney [user] [amount] | |
| bot.onText(/\/givemoney\s+(.+?)\s+(\d+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetInput = match[1].trim(); | |
| const amount = parseInt(match[2].trim(), 10); | |
| console.log(`[CMD] "/givemoney" invoked by admin ID: ${userId} for target: ${targetInput}, amount: ${amount} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/givemoney" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| if (isNaN(amount) || amount <= 0) { | |
| return bot.sendMessage(chatId, "❌ Invalid amount. Must be a positive number."); | |
| } | |
| const db = loadDB(); | |
| const res = findUserByIdOrUsername(db, targetInput); | |
| if (!res) { | |
| console.log(`[CMD] "/givemoney" failed: target user "${targetInput}" not found.`); | |
| return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`); | |
| } | |
| res.user.coins = (res.user.coins || 0) + amount; | |
| saveDB(db); | |
| console.log(`[ADMIN] Admin ${userId} gave ${amount} coins to user ${res.userId} (${res.user.username})`); | |
| await bot.sendMessage( | |
| chatId, | |
| `✅ Added *${amount} coins* to *${res.user.username}*'s wallet.\n` + | |
| `💰 New Balance: *${res.user.coins} coins*`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /resetserver | |
| bot.onText(/\/resetserver/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/resetserver" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/resetserver" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const userCount = Object.keys(db.users).length; | |
| for (const uid of Object.keys(db.users)) { | |
| db.users[uid].coins = 0; | |
| db.users[uid].totalWins = 0; | |
| db.users[uid].characters = []; | |
| } | |
| saveDB(db); | |
| console.log(`[ADMIN] Admin ${userId} executed full server reset. Cleared records for ${userCount} users.`); | |
| await bot.sendMessage( | |
| chatId, | |
| `⚠️ *Server Reset Complete!*\n\n` + | |
| `Cleared wallet balances, win counts, and owned actresses for all *${userCount}* users.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /setminplayers [number] (admin only) | |
| bot.onText(/\/setminplayers\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const valStr = match[1].trim(); | |
| console.log(`[CMD] "/setminplayers" invoked by admin ID: ${userId} with value: ${valStr}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/setminplayers" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| const num = parseInt(valStr, 10); | |
| if (isNaN(num) || num <= 0) { | |
| return bot.sendMessage(chatId, "❌ Minimum player count must be a valid positive number."); | |
| } | |
| REQUIRED_PLAYERS = num; | |
| const db = loadDB(); | |
| db.requiredPlayers = num; | |
| saveDB(db); | |
| await bot.sendMessage( | |
| chatId, | |
| `✅ *Minimum players required for auction has been set to:* \`${num}\``, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /how | |
| bot.onText(/\/how/, async (msg) => { | |
| console.log(`[CMD] "/how" requested in chat: ${msg.chat.id}`); | |
| const howText = | |
| `🎴 *HOW TO PLAY (Layman's Guide)* 🎴\n\n` + | |
| `Welcome to the Auction Bot! Here's a simple breakdown of how the game works, along with the main rules and constraints:\n\n` + | |
| `📖 *Gameplay Flow:*\n\n` + | |
| `1️⃣ *Join/Start*: When you're ready to play, type \`bid start\`. A lobby starts. Once *${REQUIRED_PLAYERS}* or more players join and finalize their choices, the auction begins!\n\n` + | |
| `2️⃣ *Starting Coins*: Each player gets a temporary starting allowance of *${STARTING_COINS} coins* for the session. In addition, you get to spend whatever coins you already have in your permanent global wallet!\n\n` + | |
| `3️⃣ *Bidding*: Actresses appear one by one. To place a bid, simply type a number in the chat (e.g. \`120\`). Your bid must be:\n\n` + | |
| ` • Higher than the base price of the actress.\n\n` + | |
| ` • Higher than the current highest bid.\n\n` + | |
| ` • Within your total available coins (Session Allowance + Global Wallet balance).\n\n` + | |
| `4️⃣ *The Timer*: When a new bid is placed, the countdown timer resets to *${BID_TIMER_SECONDS} seconds*. If the timer hits \`0\` with no new bids, the actress is sold to the highest bidder!\n\n` + | |
| `5️⃣ *Deductions*: Winning bid amount is first deducted from your session allowance. If the bid is larger than your session allowance, the remaining amount is deducted from your global wallet!\n\n` + | |
| `⚖️ *Rules & Constraints:*\n\n` + | |
| `• *No Self Outbidding*: You cannot outbid yourself. You must wait for another player to place a higher bid before you can bid again.\n\n` + | |
| `• *No Overdrafts*: If your bid amount is higher than your combined coins (Allowance + Global Wallet balance), your bid will be rejected.\n\n` + | |
| `• *Group Sessions*: Auctions happen inside the group chat. Only players who joined the lobby via \`bid start\` can place bids.\n\n` + | |
| `• *Daily Coins*: Type \`/daily\` once every 24 hours to claim a free \`+10 coins\` boost to your global wallet!\n\n` + | |
| `• *Actress ownership*: actress can be won multiple times. Your profile \`/mystats\` groups them together and lists how many copies you own along with total spent on them!\n\n`; | |
| await bot.sendMessage(msg.chat.id, howText, { parse_mode: "Markdown" }); | |
| }); | |
| // /help | |
| bot.onText(/\/help|\/start/, async (msg) => { | |
| console.log(`[CMD] "/help" or "/start" requested in chat: ${msg.chat.id}`); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `🎴 *Auction Bot — Commands*\n\n` + | |
| `• \`bid start\` — Join/start an auction session (need ${REQUIRED_PLAYERS} players)\n` + | |
| `• \`[amount]\` — Place a bid during an active auction (e.g. \`150\`)\n` + | |
| `• /how — Simple guide on how to play (rules & constraints)\n` + | |
| `• /daily — Claim daily allowance (+10 coins)\n` + | |
| `• /owners — View actress ownership leaderboard\n` + | |
| `• /leaderboard — All-time top players (wins & titles)\n` + | |
| `• /mystats — Your personal stats, wins, and owned actresses\n` + | |
| `• /listactresses — See all actresses in the pool\n` + | |
| `• /help — Show this message`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /helpadmin (admin only) | |
| bot.onText(/\/helpadmin/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/helpadmin" requested by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/helpadmin" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can access this command."); | |
| } | |
| await bot.sendMessage( | |
| chatId, | |
| `🎴 *Auction Bot — Admin Commands*\n\n` + | |
| `• /connect — Connect group to your private DM (run in group chat)\n` + | |
| `• /addactress — Add a new actress\n` + | |
| `• /setminplayers [num] — Set minimum player count for auction\n` + | |
| `• /cancelsession — Cancel current session\n` + | |
| `• /givemoney [user] [amt] — Add coins to user wallet\n` + | |
| `• /resetbalance [user] — Set user wallet coins to 0\n` + | |
| `• /resetwins [user] — Set user wins count to 0\n` + | |
| `• /resetactresses [user] — Clear user owned actresses\n` + | |
| `• /resetserver — Clear wins, balance, and owned actresses for all users\n` + | |
| `• /adminstatus — Check bot system status\n` + | |
| `• /adminsession — Check current active bid details\n` + | |
| `• /adminlistactresses — List all actresses (IDs & Details)\n` + | |
| `• /actressimages [id] — View all images & skins of an actress\n` + | |
| `• /updateactress [id] , [field] , [value] — Update actress\n` + | |
| `• /deleteactress [id] — Delete an actress\n` + | |
| `• /deletemultiactress [id1], [id2]... — Delete multiple actresses\n` + | |
| `• /addimage [id] — Add an extra photo (send photo with caption)\n` + | |
| `• /editcaption [text] — Edit caption of an image (reply to the target image)\n` + | |
| `• /deleteimage — Delete a specific image (reply to the target image)`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /adminstatus (admin only) | |
| bot.onText(/\/adminstatus/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/adminstatus" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/adminstatus" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const uptime = Math.round(process.uptime()); | |
| const activeSessionChats = Object.keys(sessions).filter( | |
| (chatId) => sessions[chatId] && sessions[chatId].phase !== "inactive" | |
| ); | |
| const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`; | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `⚙️ *Admin Bot Status*\n\n` + | |
| `• *Uptime:* \`${uptimeStr}\`\n` + | |
| `• *Active Sessions:* \`${activeSessionChats.length}\` chat(s)\n` + | |
| `• *Registered Users:* \`${Object.keys(db.users || {}).length}\`\n` + | |
| `• *Custom Characters:* \`${(db.customCharacters || []).length}\`\n` + | |
| `• *Memory Usage:* \`${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)} MB\`\n` + | |
| `• *Node Version:* \`${process.version}\``, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /adminsession (admin only) | |
| bot.onText(/\/adminsession/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/adminsession" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/adminsession" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| let targetChatId = msg.chat.id; | |
| if (msg.chat.type === "private") { | |
| const db = loadDB(); | |
| targetChatId = (db.connections || {})[userId]; | |
| if (!targetChatId) { | |
| console.log(`[CMD] "/adminsession" failed: admin ${userId} has no connected group chat.`); | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| "⚠️ You haven't connected any group chat yet. Use `/connect` in the group chat first.", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| } | |
| const session = getSession(targetChatId); | |
| if (!session || session.phase === "inactive") { | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| `ℹ️ *Session Status:* Inactive in connected chat (\`${targetChatId}\`).\nType \`bid start\` in the group to initialize.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| let details = `📊 *Session Details for chat:* \`${targetChatId}\`\n`; | |
| details += `• *Phase:* \`${session.phase}\`\n`; | |
| if (session.phase === "waiting") { | |
| const ready = Array.from(session.readyUsers || []); | |
| details += `• *Ready Users:* \`${ready.length}/${REQUIRED_PLAYERS}\`\n`; | |
| ready.forEach(([uid, uname]) => { | |
| details += ` - ${uname} (\`${uid}\`)\n`; | |
| }); | |
| } else if (session.phase === "bidding") { | |
| const totalChars = session.characters.length; | |
| const curIdx = session.currentCharacterIndex; | |
| const char = session.characters[curIdx] || { name: "N/A" }; | |
| details += `• *Characters Processed:* \`${curIdx}/${totalChars}\` (Current: *${char.name}*)\n`; | |
| const highBid = session.currentHighestBid; | |
| if (highBid) { | |
| details += `• *Highest Bid:* ${highBid.username} → \`${highBid.amount} coins\`\n`; | |
| } else { | |
| details += `• *Highest Bid:* \`None\`\n`; | |
| } | |
| details += `• *Timer:* \`${session.secondsLeft}s\` remaining\n`; | |
| details += `• *Participants:* \n`; | |
| Object.entries(session.participants).forEach(([uid, p]) => { | |
| details += ` - ${p.username} (\`${uid}\`): \`${p.coins} coins\`\n`; | |
| }); | |
| } | |
| await bot.sendMessage(msg.chat.id, details, { parse_mode: "Markdown" }); | |
| }); | |
| // /adminlistactresses (admin only) | |
| bot.onText(/\/adminlistactresses|\/adminlistchars/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/adminlistactresses" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/adminlistactresses" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| const deletedIds = db.deletedDefaultIds || []; | |
| let listText = `🎴 *All Actresses (Admin View)*\n`; | |
| const custom = db.customCharacters || []; | |
| if (custom.length === 0) { | |
| listText += `_No custom actresses yet._\n`; | |
| } else { | |
| custom.forEach((c) => { | |
| listText += `• \`${c.id}\`: *${c.name}* (Price: ${c.base_price}) \n`; | |
| }); | |
| } | |
| await bot.sendMessage(msg.chat.id, listText, { parse_mode: "Markdown" }); | |
| }); | |
| // /deleteactress [id] (admin only) | |
| bot.onText(/(?:\/deleteactress|\/deletechar)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetId = match[1].trim(); | |
| console.log(`[CMD] "/deleteactress" invoked by admin ID: ${userId} in chat: ${chatId} for ID: ${targetId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/deleteactress" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| const db = loadDB(); | |
| // Check if it is a custom actress | |
| const customIndex = (db.customCharacters || []).findIndex((c) => c.id === targetId); | |
| // Check if it is a default actress | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId); | |
| if (customIndex === -1 && !defaultChar) { | |
| console.log(`[CMD] "/deleteactress" failed: ID ${targetId} not found.`); | |
| return bot.sendMessage(msg.chat.id, `❌ Actress with ID \`${targetId}\` not found.`); | |
| } | |
| if (customIndex !== -1) { | |
| // Delete custom actress | |
| const deleted = db.customCharacters.splice(customIndex, 1)[0]; | |
| saveDB(db); | |
| console.log(`[CMD] Custom actress deleted: "${deleted.name}" (ID: ${targetId})`); | |
| return bot.sendMessage(msg.chat.id, `✅ Custom actress *${deleted.name}* (\`${targetId}\`) has been deleted.`); | |
| } | |
| if (defaultChar) { | |
| // Delete default actress (hide it) | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| if (db.deletedDefaultIds.includes(targetId)) { | |
| console.log(`[CMD] Default actress ${targetId} was already hidden.`); | |
| return bot.sendMessage(msg.chat.id, `ℹ️ Actress \`${targetId}\` was already deleted.`); | |
| } | |
| db.deletedDefaultIds.push(targetId); | |
| saveDB(db); | |
| console.log(`[CMD] Default actress hidden: "${defaultChar.name}" (ID: ${targetId})`); | |
| return bot.sendMessage(msg.chat.id, `✅ Default actress *${defaultChar.name}* (\`${targetId}\`) has been deleted (hidden).`); | |
| } | |
| }); | |
| // /updateactress [id] , [field] , [value] (admin only) | |
| bot.onText(/(?:\/updateactress|\/updatechar)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/updateactress" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/updateactress" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| const parts = match[1].split(",").map((s) => s.trim()); | |
| if (parts.length < 3) { | |
| return bot.sendMessage( | |
| msg.chat.id, | |
| "❌ Format: \`/updateactress ID , FIELD , VALUE\`\n\n" + | |
| "Fields: \`name\`, \`price\`, \`features\`\n" + | |
| "Example:\n\`/updateactress disha , price , 95\`\n" + | |
| "Example:\n\`/updateactress disha , features , Series: Bollywood , Size: Petite\`", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| const targetId = parts[0]; | |
| const fieldRaw = parts[1]; | |
| const field = fieldRaw.toLowerCase(); | |
| if (!["name", "price", "features"].includes(field)) { | |
| return bot.sendMessage(msg.chat.id, "❌ Field must be one of: \`name\`, \`price\`, \`features\`.格式: `/updateactress ID , FIELD , VALUE`"); | |
| } | |
| const db = loadDB(); | |
| let char = (db.customCharacters || []).find((c) => c.id === targetId); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId); | |
| if (!char && !defaultChar) { | |
| console.log(`[CMD] "/updateactress" failed: ID ${targetId} not found.`); | |
| return bot.sendMessage(msg.chat.id, `❌ Actress with ID \`${targetId}\` not found.`); | |
| } | |
| // If it's a default actress, copy it to customCharacters (Copy-On-Write) | |
| if (!char && defaultChar) { | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| if (!db.deletedDefaultIds.includes(targetId)) { | |
| db.deletedDefaultIds.push(targetId); | |
| } | |
| char = JSON.parse(JSON.stringify(defaultChar)); | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(char); | |
| } | |
| // Apply updates to the actress object | |
| const value = parts.slice(2).join(", "); | |
| console.log(`[CMD] Admin updating ID ${targetId}: field="${field}", old_val="${char[field] || char.base_price || ""}", new_val="${value}"`); | |
| if (field === "name") { | |
| char.name = parts[2]; | |
| } else if (field === "price") { | |
| const price = parseInt(parts[2], 10); | |
| if (isNaN(price) || price <= 0) { | |
| return bot.sendMessage(msg.chat.id, "❌ Price must be a valid positive number."); | |
| } | |
| char.base_price = price; | |
| } else if (field === "features") { | |
| char.features = parts.slice(2).filter(Boolean); | |
| } | |
| saveDB(db); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `✅ Actress \`${targetId}\` updated successfully!\n\n` + | |
| `• *Name:* ${char.name}\n` + | |
| `• *Price:* ${char.base_price} coins\n` + | |
| `• *Features:* ${char.features.join(", ")}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /deletemultiactress [id1], [id2]... (admin only) | |
| bot.onText(/(?:\/deletemultiactress|\/deletemultichar)\s+(.+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/deletemultiactress" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/deletemultiactress" by user: ${userId}`); | |
| return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command."); | |
| } | |
| const ids = match[1].split(",").map((s) => s.trim()).filter(Boolean); | |
| if (ids.length === 0) { | |
| return bot.sendMessage(msg.chat.id, "❌ Please specify one or more comma-separated IDs."); | |
| } | |
| console.log(`[CMD] Bulk deleting/hiding IDs:`, ids); | |
| const db = loadDB(); | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| const results = []; | |
| for (const targetId of ids) { | |
| const customIndex = (db.customCharacters || []).findIndex((c) => c.id === targetId); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId); | |
| if (customIndex !== -1) { | |
| const deleted = db.customCharacters.splice(customIndex, 1)[0]; | |
| results.push(`🗑️ Custom *${deleted.name}* (\`${targetId}\`) deleted.`); | |
| } else if (defaultChar) { | |
| if (!db.deletedDefaultIds.includes(targetId)) { | |
| db.deletedDefaultIds.push(targetId); | |
| } | |
| results.push(`🙈 Default *${defaultChar.name}* (\`${targetId}\`) hidden.`); | |
| } else { | |
| results.push(`❓ ID \`${targetId}\` not found.`); | |
| } | |
| } | |
| saveDB(db); | |
| await bot.sendMessage( | |
| msg.chat.id, | |
| `📝 *Bulk Delete Results:*\n\n${results.join("\n")}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /editcaption [new caption] (admin only, replied to a photo) | |
| bot.onText(/\/editcaption(?:\s+(.+))?/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/editcaption" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/editcaption" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can use this command."); | |
| } | |
| if (!msg.reply_to_message || !msg.reply_to_message.photo) { | |
| return bot.sendMessage(chatId, "⚠️ Please reply to the specific image message you want to edit."); | |
| } | |
| const replyPhoto = msg.reply_to_message.photo; | |
| const fileId = replyPhoto[replyPhoto.length - 1].file_id; | |
| const newCaption = match[1] ? match[1].trim() : ""; | |
| const db = loadDB(); | |
| let actress = (db.customCharacters || []).find((c) => { | |
| const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []); | |
| return images.some((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| return imgId === fileId; | |
| }); | |
| }); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => { | |
| const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []); | |
| return images.some((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| return imgId === fileId; | |
| }); | |
| }); | |
| if (!actress && defaultChar) { | |
| // Copy-on-Write: clone to db.customCharacters and delete from defaults | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| if (!db.deletedDefaultIds.includes(defaultChar.id)) { | |
| db.deletedDefaultIds.push(defaultChar.id); | |
| } | |
| actress = JSON.parse(JSON.stringify(defaultChar)); | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(actress); | |
| } | |
| if (actress) { | |
| if (!Array.isArray(actress.image_url)) { | |
| actress.image_url = [actress.image_url]; | |
| } | |
| let found = false; | |
| actress.image_url = actress.image_url.map((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| if (imgId === fileId) { | |
| found = true; | |
| return { file_id: fileId, caption: newCaption }; | |
| } | |
| return img; | |
| }); | |
| if (found) { | |
| saveDB(db); | |
| return bot.sendMessage( | |
| chatId, | |
| `✅ Caption updated successfully for *${actress.name}*!\n` + | |
| `📝 *New Caption:* ${newCaption || "_(cleared)_"}`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| } | |
| return bot.sendMessage(chatId, "❌ Could not find this image in the database."); | |
| }); | |
| // /deleteimage (admin only, replied to a photo) | |
| bot.onText(/\/deleteimage/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/deleteimage" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/deleteimage" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can use this command."); | |
| } | |
| if (!msg.reply_to_message || !msg.reply_to_message.photo) { | |
| return bot.sendMessage(chatId, "⚠️ Please reply to the specific image message you want to delete."); | |
| } | |
| const replyPhoto = msg.reply_to_message.photo; | |
| const fileId = replyPhoto[replyPhoto.length - 1].file_id; | |
| const db = loadDB(); | |
| let actress = (db.customCharacters || []).find((c) => { | |
| const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []); | |
| return images.some((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| return imgId === fileId; | |
| }); | |
| }); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => { | |
| const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []); | |
| return images.some((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| return imgId === fileId; | |
| }); | |
| }); | |
| if (!actress && defaultChar) { | |
| // Copy-on-Write: clone to db.customCharacters and delete from defaults | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| if (!db.deletedDefaultIds.includes(defaultChar.id)) { | |
| db.deletedDefaultIds.push(defaultChar.id); | |
| } | |
| actress = JSON.parse(JSON.stringify(defaultChar)); | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(actress); | |
| } | |
| if (actress) { | |
| if (!Array.isArray(actress.image_url)) { | |
| actress.image_url = [actress.image_url]; | |
| } | |
| let found = false; | |
| actress.image_url = actress.image_url.filter((img) => { | |
| const imgId = typeof img === "object" && img !== null ? img.file_id : img; | |
| if (imgId === fileId) { | |
| found = true; | |
| return false; | |
| } | |
| return true; | |
| }); | |
| if (found) { | |
| saveDB(db); | |
| return bot.sendMessage( | |
| chatId, | |
| `✅ Image deleted successfully from *${actress.name}*!\n` + | |
| `🖼️ Remaining images: *${actress.image_url.length}*`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| } | |
| return bot.sendMessage(chatId, "❌ Could not find this image in the database."); | |
| }); | |
| // /addimage / /addimages (admin only, sent as a photo caption) | |
| bot.on("photo", async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const caption = msg.caption; | |
| const mediaGroupId = msg.media_group_id; | |
| // Detect which command is invoked | |
| const isAddImages = caption && caption.toLowerCase().startsWith("/addimages"); | |
| const isAddImage = caption && caption.toLowerCase().startsWith("/addimage") && !isAddImages; | |
| if (!isAddImage && !isAddImages) { | |
| // If this is a photo without command, check if it's part of an active album upload | |
| if (mediaGroupId) { | |
| const largestPhoto = msg.photo[msg.photo.length - 1]; | |
| const photoObj = { fileId: largestPhoto.file_id, messageId: msg.message_id, caption: caption || "" }; | |
| if (mediaGroupAccumulators[mediaGroupId]) { | |
| mediaGroupAccumulators[mediaGroupId].photos.push(photoObj); | |
| setupAccumulatorTimer(mediaGroupId); | |
| } else { | |
| mediaGroupBuffer[mediaGroupId] = mediaGroupBuffer[mediaGroupId] || []; | |
| mediaGroupBuffer[mediaGroupId].push(photoObj); | |
| // Clear buffer after 5s to avoid leak | |
| setTimeout(() => { | |
| if (mediaGroupBuffer[mediaGroupId]) { | |
| delete mediaGroupBuffer[mediaGroupId]; | |
| } | |
| }, 5000); | |
| } | |
| } | |
| return; | |
| } | |
| // Admin access check | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for photo command by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can add images."); | |
| } | |
| const lines = caption | |
| .split("\n") | |
| .map((s) => s.trim()) | |
| .filter(Boolean); | |
| const commandPrefix = isAddImages ? /^\/addimages\s*/i : /^\/addimage\s*/i; | |
| const targetId = lines[0].replace(commandPrefix, "").trim(); | |
| const captionList = isAddImages ? lines.slice(1) : []; | |
| const firstPhotoCaption = isAddImages | |
| ? (captionList[0] || "") | |
| : lines.slice(1).join("\n").trim(); | |
| if (!targetId) { | |
| return bot.sendMessage( | |
| chatId, | |
| `❌ Please specify the character ID, e.g. \`${isAddImages ? "/addimages" : "/addimage"} goku\` in the first line of the caption.` | |
| ); | |
| } | |
| const db = loadDB(); | |
| let char = (db.customCharacters || []).find((c) => c.id === targetId); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId); | |
| if (!char && !defaultChar) { | |
| console.log(`[CMD] Photo command failed: character ID ${targetId} not found.`); | |
| return bot.sendMessage(chatId, `❌ Character with ID \`${targetId}\` not found.`); | |
| } | |
| const largestPhoto = msg.photo[msg.photo.length - 1]; | |
| const firstPhotoObj = { fileId: largestPhoto.file_id, messageId: msg.message_id, caption: firstPhotoCaption }; | |
| if (isAddImage || !mediaGroupId) { | |
| // Single image add or legacy /addimage or /addimages without album | |
| await saveImagesToCharacter(db, char, defaultChar, [firstPhotoObj], chatId); | |
| } else { | |
| // Multiple images (media group) using /addimages | |
| mediaGroupAccumulators[mediaGroupId] = { | |
| chatId, | |
| userId, | |
| targetId, | |
| captionList, | |
| photos: [firstPhotoObj] | |
| }; | |
| // Merge any pre-buffered images that arrived earlier | |
| if (mediaGroupBuffer[mediaGroupId]) { | |
| mediaGroupAccumulators[mediaGroupId].photos.push(...mediaGroupBuffer[mediaGroupId]); | |
| delete mediaGroupBuffer[mediaGroupId]; | |
| } | |
| setupAccumulatorTimer(mediaGroupId); | |
| } | |
| }); | |
| function setupAccumulatorTimer(mediaGroupId) { | |
| const acc = mediaGroupAccumulators[mediaGroupId]; | |
| if (!acc) return; | |
| if (acc.timer) clearTimeout(acc.timer); | |
| acc.timer = setTimeout(async () => { | |
| const { chatId, targetId, photos, captionList } = acc; | |
| delete mediaGroupAccumulators[mediaGroupId]; | |
| const db = loadDB(); | |
| let char = (db.customCharacters || []).find((c) => c.id === targetId); | |
| const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId); | |
| if (char || defaultChar) { | |
| // Sort photos by messageId to match album order | |
| photos.sort((a, b) => a.messageId - b.messageId); | |
| // Distribute caption list lines sequentially to photos | |
| if (captionList && captionList.length > 0) { | |
| for (let i = 0; i < photos.length; i++) { | |
| if (i < captionList.length) { | |
| photos[i].caption = captionList[i]; | |
| } | |
| } | |
| } | |
| await saveImagesToCharacter(db, char, defaultChar, photos, chatId); | |
| } | |
| }, 1500); | |
| } | |
| async function saveImagesToCharacter(db, char, defaultChar, photos, chatId) { | |
| if (!char && defaultChar) { | |
| db.deletedDefaultIds = db.deletedDefaultIds || []; | |
| if (!db.deletedDefaultIds.includes(defaultChar.id)) { | |
| db.deletedDefaultIds.push(defaultChar.id); | |
| } | |
| char = JSON.parse(JSON.stringify(defaultChar)); | |
| db.customCharacters = db.customCharacters || []; | |
| db.customCharacters.push(char); | |
| } | |
| if (!char) return; | |
| if (!char.image_url) { | |
| char.image_url = []; | |
| } else if (!Array.isArray(char.image_url)) { | |
| char.image_url = [char.image_url]; | |
| } | |
| for (const p of photos) { | |
| const imageObj = p.caption ? { file_id: p.fileId, caption: p.caption } : p.fileId; | |
| char.image_url.push(imageObj); | |
| } | |
| saveDB(db); | |
| console.log(`[CMD] Added ${photos.length} images to ${char.name} (ID: ${char.id}). Total images: ${char.image_url.length}`); | |
| await bot.sendMessage( | |
| chatId, | |
| `✅ Added *${photos.length}* new images/skins to *${char.name}* (\`${char.id}\`)!\n` + | |
| `🖼️ Total images: *${char.image_url.length}*`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| // /connect (in group chat or private chat) | |
| bot.onText(/\/connect$/, async (msg) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| console.log(`[CMD] "/connect" invoked by user ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/connect" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can connect chats."); | |
| } | |
| if (msg.chat.type === "private") { | |
| return bot.sendMessage( | |
| chatId, | |
| "ℹ️ *How to connect a group chat:*\n\n" + | |
| "1. Go to your Telegram group where the bot is added.\n" + | |
| "2. Type the `/connect` command in that group.\n" + | |
| "3. Copy the `/connect -100xxxxxxxxxx` command the bot gives you.\n" + | |
| "4. Paste it here in my private chat!", | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| const botUser = await bot.getMe(); | |
| const botUsername = botUser.username; | |
| await bot.sendMessage( | |
| chatId, | |
| `🔌 *Connect Chat (Admin)*\n\n` + | |
| `To connect this group to your private DM and manage it cleanly:\n\n` + | |
| `1. Click here: @${botUsername}\n` + | |
| `2. Send this command in the private chat:\n` + | |
| `\`/connect ${chatId}\``, | |
| { parse_mode: "Markdown" } | |
| ); | |
| }); | |
| // /connect [groupId] (in private DM) | |
| bot.onText(/\/connect\s+(-\d+)/, async (msg, match) => { | |
| const chatId = msg.chat.id; | |
| const userId = String(msg.from.id); | |
| const targetGroupId = match[1].trim(); | |
| console.log(`[CMD] "/connect ${targetGroupId}" invoked by admin ID: ${userId} in chat: ${chatId}`); | |
| if (!isAdmin(userId)) { | |
| console.log(`[CMD] Permission denied for "/connect ${targetGroupId}" by user: ${userId}`); | |
| return bot.sendMessage(chatId, "⛔ Only admins can connect chats."); | |
| } | |
| if (msg.chat.type !== "private") { | |
| return bot.sendMessage(chatId, "⚠️ Please run this command in my private chat."); | |
| } | |
| try { | |
| // Verify bot membership in the chat | |
| await bot.getChat(targetGroupId); | |
| const db = loadDB(); | |
| db.connections = db.connections || {}; | |
| db.connections[userId] = targetGroupId; | |
| saveDB(db); | |
| console.log(`[CMD] Successfully connected group chat ID ${targetGroupId} to admin ID: ${userId}`); | |
| await bot.sendMessage( | |
| chatId, | |
| `✅ *Chat Connected successfully!*\n\n` + | |
| `• *Admin:* @${msg.from.username || msg.from.first_name}\n` + | |
| `• *Connected Group Chat ID:* \`${targetGroupId}\`\n\n` + | |
| `From now on, all group-specific admin operations run in this DM (like \`/cancelsession\` and \`/adminsession\`) will apply to that group chat!`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } catch (err) { | |
| console.error("❌ Error verifying group chat connection:", err); | |
| await bot.sendMessage( | |
| chatId, | |
| `❌ *Connection Failed!*\n\n` + | |
| `Make sure that:\n` + | |
| `1. The group ID \`${targetGroupId}\` is correct.\n` + | |
| `2. The bot is already added to that group as an administrator.`, | |
| { parse_mode: "Markdown" } | |
| ); | |
| } | |
| }); | |
| // Handle in-group character selection callbacks | |
| bot.on("callback_query", async (query) => { | |
| const data = query.data; | |
| const clickerId = String(query.from.id); | |
| const clickerUsername = query.from.username | |
| ? `@${query.from.username}` | |
| : query.from.first_name || `User${clickerId}`; | |
| const chatId = query.message.chat.id; | |
| if (data.startsWith("sel:")) { | |
| // Format: sel:[groupId]:[ownerId]:[characterId] | |
| const parts = data.split(":"); | |
| const groupId = parts[1]; | |
| const ownerId = parts[2]; | |
| const charId = parts[3]; | |
| console.log(`[CALLBACK] Selection toggle callback query: user=${clickerUsername} (ID: ${clickerId}), target_player_menu=${ownerId}, character_id=${charId}, group=${groupId}`); | |
| if (clickerId !== ownerId) { | |
| return bot.answerCallbackQuery(query.id, { | |
| text: `⚠️ This selection menu is only for the player who requested it!`, | |
| show_alert: true | |
| }); | |
| } | |
| const session = getSession(groupId); | |
| if (!session || session.phase !== "waiting") { | |
| return bot.answerCallbackQuery(query.id, { text: "⚠️ No active signup lobby in that group." }); | |
| } | |
| session.userSelections = session.userSelections || {}; | |
| session.userSelections[ownerId] = session.userSelections[ownerId] || new Set(); | |
| const userSet = session.userSelections[ownerId]; | |
| if (userSet.has(charId)) { | |
| userSet.delete(charId); | |
| console.log(`[CALLBACK] Deselected character: ${charId} for user ${clickerUsername}`); | |
| } else { | |
| // Check if character is already selected by another user | |
| let alreadySelectedBy = null; | |
| for (const [uid, charSet] of Object.entries(session.userSelections)) { | |
| if (uid !== ownerId && charSet instanceof Set && charSet.has(charId)) { | |
| alreadySelectedBy = session.readyUsers.get(uid) || `User ${uid}`; | |
| break; | |
| } | |
| } | |
| if (alreadySelectedBy) { | |
| console.log(`[CALLBACK] Selection rejected: Character ${charId} already taken by user ${alreadySelectedBy}`); | |
| return bot.answerCallbackQuery(query.id, { | |
| text: `⚠️ This actress is already selected by ${alreadySelectedBy}! Please select another.`, | |
| show_alert: true | |
| }); | |
| } | |
| // Clear previous selection as each player can select only one actress | |
| userSet.clear(); | |
| userSet.add(charId); | |
| console.log(`[CALLBACK] Selected character: ${charId} for user ${clickerUsername}`); | |
| } | |
| // Rebuild updated inline keyboard with new checkmarks | |
| const db = loadDB(); | |
| const allChars = getAllCharacters(db); | |
| const updatedKeyboard = buildSelectionKeyboard(groupId, ownerId, allChars, userSet); | |
| try { | |
| await bot.editMessageReplyMarkup( | |
| { inline_keyboard: updatedKeyboard }, | |
| { chat_id: chatId, message_id: query.message.message_id } | |
| ); | |
| } catch (_) { } | |
| bot.answerCallbackQuery(query.id, { text: "Toggled selection!" }); | |
| } | |
| if (data.startsWith("done:")) { | |
| // Format: done:[groupId]:[ownerId] | |
| const parts = data.split(":"); | |
| const groupId = parts[1]; | |
| const ownerId = parts[2]; | |
| console.log(`[CALLBACK] Finalize selections callback query: user=${clickerUsername} (ID: ${clickerId}), target_player_menu=${ownerId}, group=${groupId}`); | |
| if (clickerId !== ownerId) { | |
| return bot.answerCallbackQuery(query.id, { | |
| text: `⚠️ This selection menu is only for the player who requested it!`, | |
| show_alert: true | |
| }); | |
| } | |
| const session = getSession(groupId); | |
| if (!session || session.phase !== "waiting") { | |
| return bot.answerCallbackQuery(query.id, { text: "⚠️ No active signup lobby in that group." }); | |
| } | |
| const userSet = session.userSelections && session.userSelections[ownerId]; | |
| if (!userSet || userSet.size === 0) { | |
| return bot.answerCallbackQuery(query.id, { | |
| text: `⚠️ You must select at least one actress before clicking Done!`, | |
| show_alert: true | |
| }); | |
| } | |
| session.userSelectionsDone = session.userSelectionsDone || {}; | |
| session.userSelectionsDone[ownerId] = true; | |
| console.log(`[CALLBACK] User ${clickerUsername} finalized selections:`, Array.from(userSet)); | |
| try { | |
| await bot.editMessageText( | |
| `✅ ${clickerUsername} has finalized their actress selections!`, | |
| { | |
| chat_id: chatId, | |
| message_id: query.message.message_id, | |
| parse_mode: "Markdown" | |
| } | |
| ); | |
| } catch (_) { } | |
| bot.answerCallbackQuery(query.id, { text: "Selections saved!" }); | |
| // Check if lobby is ready and start bidding! | |
| await checkLobbyReadyAndStart(groupId); | |
| } | |
| }); | |
| // Update group member tags for top 5 players based on global wins | |
| async function updateGroupTagsForTopUsers(chatId) { | |
| try { | |
| console.log(`[TAGS] Updating group tags for chat: ${chatId}`); | |
| const db = loadDB(); | |
| const users = Object.entries(db.users || {}); | |
| if (users.length === 0) { | |
| console.log(`[TAGS] No registered users in DB.`); | |
| return; | |
| } | |
| // Sort globally by wins | |
| const sorted = users.sort((a, b) => b[1].totalWins - a[1].totalWins); | |
| const promises = sorted.map(async ([uid, userObj], idx) => { | |
| const rank = idx + 1; | |
| const isTop5 = rank <= 5; | |
| const targetTag = isTop5 ? `Rank ${rank}` : ""; | |
| try { | |
| await bot.setChatMemberTag(chatId, Number(uid), { tag: targetTag }); | |
| } catch (err) { | |
| // Silently ignore failures (e.g. USER_NOT_PARTICIPANT or CHAT_ADMIN_REQUIRED) | |
| } | |
| }); | |
| await Promise.allSettled(promises); | |
| console.log(`[TAGS] Rank tags updated successfully for group: ${chatId}`); | |
| } catch (err) { | |
| console.error(`[TAGS] Error in updateGroupTagsForTopUsers:`, err); | |
| } | |
| } | |
| console.log("🤖 Auction Bot is running..."); | |