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...");