teleAuctionBot / index.js
amanfor18's picture
Update index.js
e87780c verified
Raw
History Blame Contribute Delete
93.2 kB
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...");