Spaces:
Sleeping
Sleeping
Upload 15 files
Browse files- Dockerfile +8 -0
- README.md +102 -8
- characters.csv +2 -0
- characters.js +7 -0
- db.js +97 -0
- db.json +544 -0
- docker-compose.yml +6 -0
- documentation.md +189 -0
- import_characters.js +213 -0
- index.js +2583 -0
- logger.js +64 -0
- migrate_to_mongo.js +54 -0
- package-lock.json +186 -0
- package.json +17 -0
- response.html +30 -0
Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package*.json ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY . .
|
| 6 |
+
EXPOSE 7860
|
| 7 |
+
ENV PORT=7860
|
| 8 |
+
CMD ["node", "index.js"]
|
README.md
CHANGED
|
@@ -1,11 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎴 Telegram Auction Bot
|
| 2 |
+
|
| 3 |
+
A fun real-time bidding/auction bot for Telegram groups featuring Bollywood actresses!
|
| 4 |
+
|
| 5 |
+
## ✨ Features
|
| 6 |
+
|
| 7 |
+
- Dynamic number of players (configured by admin, default 1) type `bid start` to launch a session
|
| 8 |
+
- Each player gets **500 coins** session allowance to spend
|
| 9 |
+
- **5 actresses** auctioned per session (shuffled randomly)
|
| 10 |
+
- **15-second reset timer** — any new bid resets the clock (auction-house style!)
|
| 11 |
+
- Persistent leaderboard & player stats
|
| 12 |
+
- Admins can add custom actresses via bot commands
|
| 13 |
+
|
| 14 |
+
## 🚀 Setup
|
| 15 |
+
|
| 16 |
+
### 1. Create your bot
|
| 17 |
+
|
| 18 |
+
1. Open Telegram and message **@BotFather**
|
| 19 |
+
2. Send `/newbot` and follow the prompts
|
| 20 |
+
3. Copy the **API token** you receive
|
| 21 |
+
|
| 22 |
+
### 2. Install dependencies
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
npm install
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### 3. Configure environment
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
cp .env.example .env
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Edit `.env`:
|
| 35 |
+
```
|
| 36 |
+
BOT_TOKEN=your_token_from_botfather
|
| 37 |
+
ADMIN_IDS=your_telegram_user_id
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
> To find your Telegram user ID, message **@userinfobot** on Telegram.
|
| 41 |
+
|
| 42 |
+
### 4. Run the bot
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
node index.js
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 5. Add bot to your group
|
| 49 |
+
|
| 50 |
+
- Add the bot to your Telegram group
|
| 51 |
+
- Give it permission to send messages
|
| 52 |
+
- Make sure **Group Privacy** is disabled (ask BotFather: `/setprivacy` → Disable) so the bot can read messages
|
| 53 |
+
|
| 54 |
---
|
| 55 |
+
|
| 56 |
+
## 💬 Commands
|
| 57 |
+
|
| 58 |
+
| Command | Who | Description |
|
| 59 |
+
|---|---|---|
|
| 60 |
+
| `bid start` | Everyone | Join / start a session |
|
| 61 |
+
| `150` (any number) | Participants | Place a bid |
|
| 62 |
+
| `/leaderboard` | Everyone | All-time top players |
|
| 63 |
+
| `/mystats` | Everyone | Your personal stats |
|
| 64 |
+
| `/listactresses` | Everyone | View all actresses |
|
| 65 |
+
| `/how` | Everyone | Simple guide on how to play |
|
| 66 |
+
| `/help` | Everyone | Show help |
|
| 67 |
+
| `/addactress` | Admin | Instructions to add an actress |
|
| 68 |
+
| `/submitactress ...` | Admin | Submit a new actress |
|
| 69 |
+
| `/cancelsession` | Admin | Cancel the current session |
|
| 70 |
+
| `/helpadmin` | Admin | Show admin help |
|
| 71 |
+
|
| 72 |
---
|
| 73 |
|
| 74 |
+
## 🏆 Coin System
|
| 75 |
+
|
| 76 |
+
- Each session: **500 coins** allowance per player
|
| 77 |
+
- Coins are tracked lifetime in the leaderboard and profile stats
|
| 78 |
+
- Bids are deducted from session allowance first, then from the player's global wallet.
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## 🗂️ File Structure
|
| 83 |
+
|
| 84 |
+
```
|
| 85 |
+
telegram-bid-bot/
|
| 86 |
+
├── index.js # Main bot logic
|
| 87 |
+
├── characters.js # Default hardcoded actresses list
|
| 88 |
+
├── db.js # JSON database helpers
|
| 89 |
+
├── db.json # Auto-created on first run (persistent data)
|
| 90 |
+
├── .env # Your bot token & admin IDs
|
| 91 |
+
└── README.md
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 🌐 Running 24/7
|
| 97 |
+
|
| 98 |
+
To keep the bot running permanently, use **PM2**:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
npm install -g pm2
|
| 102 |
+
pm2 start index.js --name auction-bot
|
| 103 |
+
pm2 save
|
| 104 |
+
pm2 startup
|
| 105 |
+
```
|
characters.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Name,Base price,features,Image URL 1,Caption 1,Image URL 2,Caption 2,Image URL 3,Caption 3,Category
|
| 2 |
+
Kiara Advani,80,Mommy figure; Perfect boobs; Bhabhi material,https://www.telugu360.com/wp-content/uploads/2026/03/Kiara-1-632x1024.jpg,Mommy made for rough sex,https://www.kollywoodzone.com/boxoffice/wp-content/uploads/2021/02/Kiara-Advani-Hot-and-Seductive-in-Yellow-Saree-Photos-01.jpg,Sanskari Kiara ready to have suhagraat sex,https://i.pinimg.com/736x/d0/db/53/d0db530e9a295cc2f7510028ba5c71ac.jpg,Kiara ki choot mei dalega 12 inch ka loda,MILF
|
characters.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Default hardcoded characters
|
| 2 |
+
// Replace image_url with actual image URLs or local file paths
|
| 3 |
+
const DEFAULT_CHARACTERS = [
|
| 4 |
+
|
| 5 |
+
];
|
| 6 |
+
|
| 7 |
+
module.exports = { DEFAULT_CHARACTERS };
|
db.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { MongoClient } = require("mongodb");
|
| 2 |
+
|
| 3 |
+
let dbClient = null;
|
| 4 |
+
let dbInstance = null;
|
| 5 |
+
let cache = null;
|
| 6 |
+
let isSaving = false;
|
| 7 |
+
let needsSave = false;
|
| 8 |
+
|
| 9 |
+
const DEFAULT_DB = {
|
| 10 |
+
users: {}, // { userId: { username, coins, characters: [], totalWins } }
|
| 11 |
+
customCharacters: [], // admin-added characters
|
| 12 |
+
sessions: {}, // { chatId: session data }
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
async function initDB() {
|
| 16 |
+
const uri = process.env.MONGODB_URI;
|
| 17 |
+
if (!uri) {
|
| 18 |
+
console.error("❌ MONGODB_URI not set in environment!");
|
| 19 |
+
process.exit(1);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
console.log("🔌 Connecting to MongoDB...");
|
| 24 |
+
dbClient = new MongoClient(uri);
|
| 25 |
+
await dbClient.connect();
|
| 26 |
+
dbInstance = dbClient.db();
|
| 27 |
+
console.log("✅ Successfully connected to MongoDB.");
|
| 28 |
+
|
| 29 |
+
const collection = dbInstance.collection("bot_state");
|
| 30 |
+
const record = await collection.findOne({ _id: "state" });
|
| 31 |
+
if (record) {
|
| 32 |
+
cache = record.data;
|
| 33 |
+
console.log("✅ Loaded bot state from MongoDB cache.");
|
| 34 |
+
} else {
|
| 35 |
+
cache = JSON.parse(JSON.stringify(DEFAULT_DB));
|
| 36 |
+
await collection.insertOne({ _id: "state", data: cache });
|
| 37 |
+
console.log("✅ Created initial bot state in MongoDB.");
|
| 38 |
+
}
|
| 39 |
+
} catch (err) {
|
| 40 |
+
console.error("❌ Failed to connect or initialize MongoDB:", err);
|
| 41 |
+
process.exit(1);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function loadDB() {
|
| 46 |
+
if (!cache) {
|
| 47 |
+
// Return temporary default during fallback
|
| 48 |
+
return JSON.parse(JSON.stringify(DEFAULT_DB));
|
| 49 |
+
}
|
| 50 |
+
return cache;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function saveDB(db) {
|
| 54 |
+
cache = db;
|
| 55 |
+
queueSave();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function queueSave() {
|
| 59 |
+
if (isSaving) {
|
| 60 |
+
needsSave = true;
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
isSaving = true;
|
| 65 |
+
const collection = dbInstance.collection("bot_state");
|
| 66 |
+
collection.updateOne(
|
| 67 |
+
{ _id: "state" },
|
| 68 |
+
{ $set: { data: cache } },
|
| 69 |
+
{ upsert: true }
|
| 70 |
+
).then(() => {
|
| 71 |
+
isSaving = false;
|
| 72 |
+
console.log("💾 Bot state successfully synchronized to MongoDB.");
|
| 73 |
+
if (needsSave) {
|
| 74 |
+
needsSave = false;
|
| 75 |
+
queueSave();
|
| 76 |
+
}
|
| 77 |
+
}).catch((err) => {
|
| 78 |
+
console.error("❌ MongoDB background sync error:", err);
|
| 79 |
+
isSaving = false;
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function getUser(db, userId, username) {
|
| 84 |
+
if (!db.users[userId]) {
|
| 85 |
+
db.users[userId] = {
|
| 86 |
+
username: username || `User${userId}`,
|
| 87 |
+
coins: 0,
|
| 88 |
+
characters: [],
|
| 89 |
+
totalWins: 0,
|
| 90 |
+
};
|
| 91 |
+
} else if (username) {
|
| 92 |
+
db.users[userId].username = username;
|
| 93 |
+
}
|
| 94 |
+
return db.users[userId];
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
module.exports = { initDB, loadDB, saveDB, getUser };
|
db.json
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"users": {
|
| 3 |
+
"6260798528": {
|
| 4 |
+
"username": "@the_punisher_1717",
|
| 5 |
+
"coins": 0,
|
| 6 |
+
"characters": [
|
| 7 |
+
{
|
| 8 |
+
"characterId": "custom_1782800690111",
|
| 9 |
+
"characterName": "Disha Patani",
|
| 10 |
+
"paidAmount": 81,
|
| 11 |
+
"wonAt": "2026-06-30T06:31:21.942Z"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"characterId": "custom_1782802831441",
|
| 15 |
+
"characterName": "Janhvi Kapoor",
|
| 16 |
+
"paidAmount": 80,
|
| 17 |
+
"wonAt": "2026-06-30T07:03:26.090Z"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"characterId": "custom_1782801935897",
|
| 21 |
+
"characterName": "Alia Bhatt",
|
| 22 |
+
"paidAmount": 130,
|
| 23 |
+
"wonAt": "2026-06-30T07:07:55.560Z"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"characterId": "custom_1782802831441",
|
| 27 |
+
"characterName": "Janhvi Kapoor",
|
| 28 |
+
"paidAmount": 68,
|
| 29 |
+
"wonAt": "2026-06-30T07:11:07.381Z"
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"characterId": "custom_1782800690111",
|
| 33 |
+
"characterName": "Disha Patani",
|
| 34 |
+
"paidAmount": 90,
|
| 35 |
+
"wonAt": "2026-06-30T07:13:30.875Z"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"characterId": "custom_1782802831441",
|
| 39 |
+
"characterName": "Janhvi Kapoor",
|
| 40 |
+
"paidAmount": 120,
|
| 41 |
+
"wonAt": "2026-06-30T13:14:04.087Z"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"characterId": "custom_1782802831441",
|
| 45 |
+
"characterName": "Janhvi Kapoor",
|
| 46 |
+
"paidAmount": 200,
|
| 47 |
+
"wonAt": "2026-07-01T06:16:05.915Z"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"characterId": "custom_1782801420201",
|
| 51 |
+
"characterName": "Kriti Sanon",
|
| 52 |
+
"paidAmount": 120,
|
| 53 |
+
"wonAt": "2026-07-01T06:19:35.122Z"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"characterId": "custom_1782837223614",
|
| 57 |
+
"characterName": "Kareena Kapoor",
|
| 58 |
+
"paidAmount": 121,
|
| 59 |
+
"wonAt": "2026-07-01T06:20:27.269Z"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"characterId": "custom_1782800690111",
|
| 63 |
+
"characterName": "Disha Patani",
|
| 64 |
+
"paidAmount": 90,
|
| 65 |
+
"wonAt": "2026-07-01T06:43:58.135Z"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"characterId": "custom_1782802831441",
|
| 69 |
+
"characterName": "Janhvi Kapoor",
|
| 70 |
+
"paidAmount": 88,
|
| 71 |
+
"wonAt": "2026-07-01T06:48:53.055Z"
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"characterId": "custom_1782713375498",
|
| 75 |
+
"characterName": "Sai Pallavi",
|
| 76 |
+
"paidAmount": 88,
|
| 77 |
+
"wonAt": "2026-07-01T06:49:37.041Z"
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"totalWins": 12,
|
| 81 |
+
"lastDaily": "2026-07-01T06:16:17.892Z"
|
| 82 |
+
},
|
| 83 |
+
"8793548696": {
|
| 84 |
+
"username": "@MajorSharafat",
|
| 85 |
+
"coins": 0,
|
| 86 |
+
"characters": [
|
| 87 |
+
{
|
| 88 |
+
"characterId": "custom_1782801420201",
|
| 89 |
+
"characterName": "Kriti Sanon",
|
| 90 |
+
"paidAmount": 101,
|
| 91 |
+
"wonAt": "2026-06-30T15:46:08.072Z"
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"totalWins": 1
|
| 95 |
+
},
|
| 96 |
+
"8648163105": {
|
| 97 |
+
"username": "Bxl",
|
| 98 |
+
"coins": 10,
|
| 99 |
+
"characters": [
|
| 100 |
+
{
|
| 101 |
+
"characterId": "custom_1782727049007_7717",
|
| 102 |
+
"characterName": "Kiara Advani",
|
| 103 |
+
"paidAmount": 100,
|
| 104 |
+
"wonAt": "2026-06-30T06:07:26.342Z"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"characterId": "custom_1782827886063",
|
| 108 |
+
"characterName": "Ananya Pandey",
|
| 109 |
+
"paidAmount": 100,
|
| 110 |
+
"wonAt": "2026-07-01T06:12:49.163Z"
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"characterId": "custom_1782727049007_7717",
|
| 114 |
+
"characterName": "Kiara Advani",
|
| 115 |
+
"paidAmount": 150,
|
| 116 |
+
"wonAt": "2026-07-01T06:13:46.545Z"
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"characterId": "custom_1782727049007_7717",
|
| 120 |
+
"characterName": "Kiara Advani",
|
| 121 |
+
"paidAmount": 230,
|
| 122 |
+
"wonAt": "2026-07-01T06:17:07.030Z"
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"characterId": "custom_1782827886063",
|
| 126 |
+
"characterName": "Ananya Pandey",
|
| 127 |
+
"paidAmount": 200,
|
| 128 |
+
"wonAt": "2026-07-01T06:18:34.062Z"
|
| 129 |
+
}
|
| 130 |
+
],
|
| 131 |
+
"totalWins": 5,
|
| 132 |
+
"lastDaily": "2026-07-01T06:18:51.197Z"
|
| 133 |
+
},
|
| 134 |
+
"5268661964": {
|
| 135 |
+
"username": "@Nooooneew",
|
| 136 |
+
"coins": 10,
|
| 137 |
+
"characters": [
|
| 138 |
+
{
|
| 139 |
+
"characterId": "custom_1782827189434",
|
| 140 |
+
"characterName": "Shraddha Kapoor",
|
| 141 |
+
"paidAmount": 201,
|
| 142 |
+
"wonAt": "2026-07-01T06:46:40.410Z"
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"characterId": "custom_1782827886063",
|
| 146 |
+
"characterName": "Ananya Pandey",
|
| 147 |
+
"paidAmount": 211,
|
| 148 |
+
"wonAt": "2026-07-01T06:48:06.315Z"
|
| 149 |
+
}
|
| 150 |
+
],
|
| 151 |
+
"totalWins": 2,
|
| 152 |
+
"lastDaily": "2026-07-01T06:50:23.468Z"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"customCharacters": [
|
| 156 |
+
{
|
| 157 |
+
"id": "custom_1782713375498",
|
| 158 |
+
"name": "Sai Pallavi",
|
| 159 |
+
"image_url": [
|
| 160 |
+
"AgACAgUAAyEFAATtFlP2AANuakIMILxuoRgAAemmXMpykUDePeB9AAK2D2sbbw4RVg44DoLOqTFrAQADAgADeQADPAQ",
|
| 161 |
+
{
|
| 162 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAN9akINSYNrok_U1BSAinH5q5NJv-AAArcPaxtvDhFWTE3sk6IWPHkBAAMCAAN5AAM8BA",
|
| 163 |
+
"caption": "Pyaari Sai ki chut"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAOBakINotqc-GaqnaRSF9kicgVT53oAArgPaxtvDhFWTFRV4hk244UBAAMCAAN5AAM8BA",
|
| 167 |
+
"caption": "Perfect face fuck material"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAODakINvLe9pf49wrEsT2UL2FrmVEQAArkPaxtvDhFWSXog3jzXVm8BAAMCAAN5AAM8BA",
|
| 171 |
+
"caption": "Treat for Sanskari chut lovers"
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"file_id": "AgACAgUAAxkBAAMPakIV7fiBLiI-RPWC28zx9zFiNEoAAg8Paxu8JRBWeZVeOGcCguABAAMCAAN5AAM8BA",
|
| 175 |
+
"caption": "Made for admiring"
|
| 176 |
+
}
|
| 177 |
+
],
|
| 178 |
+
"base_price": 50,
|
| 179 |
+
"features": [
|
| 180 |
+
"Speciality: Cuteness",
|
| 181 |
+
"Age: 25",
|
| 182 |
+
"Body Count: 10"
|
| 183 |
+
],
|
| 184 |
+
"addedBy": "6260798528"
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"id": "custom_1782727049007_7717",
|
| 188 |
+
"name": "Kiara Advani",
|
| 189 |
+
"image_url": [
|
| 190 |
+
{
|
| 191 |
+
"file_id": "https://www.telugu360.com/wp-content/uploads/2026/03/Kiara-1-632x1024.jpg",
|
| 192 |
+
"caption": "Mommy made for rough sex"
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"file_id": "https://www.kollywoodzone.com/boxoffice/wp-content/uploads/2021/02/Kiara-Advani-Hot-and-Seductive-in-Yellow-Saree-Photos-01.jpg",
|
| 196 |
+
"caption": "Sanskari Kiara ready to have suhagraat sex"
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"file_id": "https://i.pinimg.com/736x/d0/db/53/d0db530e9a295cc2f7510028ba5c71ac.jpg",
|
| 200 |
+
"caption": "Kiara ki choot mei dalega 12 inch ka loda"
|
| 201 |
+
}
|
| 202 |
+
],
|
| 203 |
+
"base_price": 80,
|
| 204 |
+
"features": [
|
| 205 |
+
"Mommy figure",
|
| 206 |
+
"Perfect boobs",
|
| 207 |
+
"Bhabhi material"
|
| 208 |
+
],
|
| 209 |
+
"category": "MILF",
|
| 210 |
+
"addedBy": "bulk_import"
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"id": "custom_1782800690111",
|
| 214 |
+
"name": "Disha Patani",
|
| 215 |
+
"image_url": [
|
| 216 |
+
{
|
| 217 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDPWpDXGsz6IH-5lzOaFiqjc8VhN2OAAI5D2sbbw4ZVtADLPkoRRI6AQADAgADeQADPAQ",
|
| 218 |
+
"caption": "Disha's boobs made for sucking"
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDPmpDXGuan_H68FjMTu_sWeOSES8SAAI6D2sbbw4ZVkb-_y0MZGXKAQADAgADeQADPAQ",
|
| 222 |
+
"caption": "Ass so juicy all we want to grab and bite"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDP2pDXGtF-H8-jxChINY-C1Jgt-_cAAI7D2sbbw4ZVuxwlUCRsTNlAQADAgADeQADPAQ",
|
| 226 |
+
"caption": "Abs so toned jsut grab and lick it whole"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"base_price": 80,
|
| 230 |
+
"features": [
|
| 231 |
+
"Big boobs",
|
| 232 |
+
"Slender body",
|
| 233 |
+
"Juicy Ass"
|
| 234 |
+
],
|
| 235 |
+
"addedBy": "6260798528"
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"id": "custom_1782801420201",
|
| 239 |
+
"name": "Kriti Sanon",
|
| 240 |
+
"image_url": [
|
| 241 |
+
{
|
| 242 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDnGpDZEEPc8942Hf7hsZxu2hAAAEeFgACSQ9rG28OGVbLrKfV8c8byAEAAwIAA3kAAzwE",
|
| 243 |
+
"caption": "Lick this deep sexy navel of Kriti"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDnWpDZEEq_h0ZYfRvKxIQf0Q7IzfeAAJKD2sbbw4ZViazSAABj1JD7QEAAwIAA3kAAzwE",
|
| 247 |
+
"caption": "Kriti's face is perfect to slap dick on"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDnmpDZEFetFD8LrD-9f8mz1Kk67g6AAJLD2sbbw4ZVu8umr2Zd36sAQADAgADeQADPAQ",
|
| 251 |
+
"caption": "Kutti Sanon at peak of her hotness"
|
| 252 |
+
}
|
| 253 |
+
],
|
| 254 |
+
"base_price": 100,
|
| 255 |
+
"features": [
|
| 256 |
+
"Sexy Navel",
|
| 257 |
+
"Slutty legs",
|
| 258 |
+
"Fuck face"
|
| 259 |
+
],
|
| 260 |
+
"addedBy": "6260798528"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"id": "custom_1782801935897",
|
| 264 |
+
"name": "Alia Bhatt",
|
| 265 |
+
"image_url": [
|
| 266 |
+
{
|
| 267 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDpGpDZkz_o2RBYvMo3qEAAQ2fRxYmHgACTw9rG28OGVaBiyfeNscLFAEAAwIAA3gAAzwE",
|
| 268 |
+
"caption": "Alia mommy is ready to suck my dick with her sloppy mouth"
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDpWpDZkxYV80YhCrJquBbXBDY5wABYAACUA9rG28OGVb2lZHOXFHLUAEAAwIAA3kAAzwE",
|
| 272 |
+
"caption": "Made for just sex only and only sex"
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIDpmpDZkwgZsMRHp7VQCY6QMgyZUKTAAJRD2sbbw4ZVvAK7NbHYgHKAQADAgADeAADPAQ",
|
| 276 |
+
"caption": "Meri nangi peeth pe moot do saalo"
|
| 277 |
+
}
|
| 278 |
+
],
|
| 279 |
+
"base_price": 120,
|
| 280 |
+
"features": [
|
| 281 |
+
"Sexy Mommy🥵",
|
| 282 |
+
"Petite figure🫦",
|
| 283 |
+
"Milk lactating cow🍼"
|
| 284 |
+
],
|
| 285 |
+
"addedBy": "6260798528"
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"id": "custom_1782802831441",
|
| 289 |
+
"name": "Janhvi Kapoor",
|
| 290 |
+
"image_url": [
|
| 291 |
+
{
|
| 292 |
+
"file_id": "AgACAgUAAxkBAAMaakNp3LKjl-_hVXaBc6afVtg9HtcAAuIQaxu8JSBWjxpJwgcE8REBAAMCAAN5AAM8BA",
|
| 293 |
+
"caption": "Janhvi ka makhmali geela badan"
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"file_id": "AgACAgUAAxkBAAMbakNp3AABf9f3vDAUk6ZrQJhC9UvpAALjEGsbvCUgVmOwVdF6ZJ2BAQADAgADeQADPAQ",
|
| 297 |
+
"caption": "Saali Janhvi ko kutti bnake kamar pkd ke chodo"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"file_id": "AgACAgUAAxkBAAMcakNp3IUlH64gFZUmiBJspcj8N7MAAuQQaxu8JSBWVJGAhcih_VYBAAMCAAN5AAM8BA",
|
| 301 |
+
"caption": "Gaand marwane ko taiyaar baithi hai Janhvi"
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"file_id": "AgACAgUAAxkBAAMdakNp3Gch7TStxb7jJEShjreCuakAAuUQaxu8JSBWYPSkB9MKIwIBAAMCAAN5AAM8BA",
|
| 305 |
+
"caption": "Janhvi ke choochon ko neechod ke saali ka meetha doodh pee jao"
|
| 306 |
+
}
|
| 307 |
+
],
|
| 308 |
+
"base_price": 60,
|
| 309 |
+
"features": [
|
| 310 |
+
"Fuck Meat 🫦",
|
| 311 |
+
"Busty boobs and ass 🍑",
|
| 312 |
+
"thick juicy thighs🥵"
|
| 313 |
+
],
|
| 314 |
+
"addedBy": "6260798528"
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"id": "custom_1782827189434",
|
| 318 |
+
"name": "Shraddha Kapoor",
|
| 319 |
+
"image_url": [
|
| 320 |
+
{
|
| 321 |
+
"file_id": "AgACAgUAAxkBAAMoakPJH0fUmlRkZs_OPe7WxK53tZcAAosQaxumFCFWem7MFqNh2p8BAAMCAAN5AAM8BA",
|
| 322 |
+
"caption": "Shraddha ke mooh pe mota kala loda rgdo"
|
| 323 |
+
},
|
| 324 |
+
{
|
| 325 |
+
"file_id": "AgACAgUAAxkBAAMpakPJH5mCTcYTN9JWY2Y8atyLm_gAAv8QaxtmTxhWSSQ9WKyx7oEBAAMCAAN5AAM8BA",
|
| 326 |
+
"caption": "Aisi mulayam juicy ass ko chabana hi best hai"
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"file_id": "AgACAgUAAxkBAAMqakPJHw9b9G2TnVrNssUTT30chwsAAowQaxumFCFWhpeFt8E-1QEBAAMCAAN5AAM8BA",
|
| 330 |
+
"caption": "Arey yaar aise sabke saamne tum meri chut mei ungli na ghoosaya karo"
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"file_id": "AgACAgUAAxkBAAMrakPJHyAvAT-yQDdV78RVRhw2GvYAAxFrG2ZPGFbgjuIf0lJt2QEAAwIAA3kAAzwE",
|
| 334 |
+
"caption": "Shraddha nashe mei hai saali ko kamre mei lejake chod do"
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"file_id": "AgACAgUAAxkBAAMsakPJH67LK759FYyEbxfBw1AZsAMAAgERaxtmTxhWjLaJ8PNF3YIBAAMCAAN5AAM8BA",
|
| 338 |
+
"caption": "Aaj raat meri chut mei gudgudi ho rhi hai mujeh tumhara kala mota loda apni chut mei dlwana hai aa jao"
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"file_id": "AgACAgUAAxkBAAMtakPJH1bbUjATFuuYp98p78yfUoAAAgIRaxtmTxhWGGid4NS0FXABAAMCAAN5AAM8BA",
|
| 342 |
+
"caption": "Shraddha ki saari mei chodne ka sukh naseeb walo ko milta hai"
|
| 343 |
+
}
|
| 344 |
+
],
|
| 345 |
+
"base_price": 100,
|
| 346 |
+
"features": [
|
| 347 |
+
"Chikna badan🤤",
|
| 348 |
+
"Chote boobs aur bade nipples🍒🥵",
|
| 349 |
+
"Raseele honth🫦"
|
| 350 |
+
],
|
| 351 |
+
"addedBy": "6260798528"
|
| 352 |
+
},
|
| 353 |
+
{
|
| 354 |
+
"id": "custom_1782827886063",
|
| 355 |
+
"name": "Ananya Pandey",
|
| 356 |
+
"image_url": [
|
| 357 |
+
{
|
| 358 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIEKWpD5FRieDNh_ZBxPEvR4LYD2ZglAALqEGsbI34hVnDL6qVLW3vvAQADAgADeQADPAQ",
|
| 359 |
+
"caption": "Ananya saali ka gora chikna badan"
|
| 360 |
+
},
|
| 361 |
+
{
|
| 362 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIEKmpD5FRJhHJjIKSKBUneW4tzG-GgAALrEGsbI34hVoiJQT9WSHvfAQADAgADeQADPAQ",
|
| 363 |
+
"caption": "Iske chote chote choochon pe chaante marke chodu"
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIEK2pD5FSIRCNKbTsVa-zZ_BkEMB0KAALsEGsbI34hVmRDItUewjGpAQADAgADeQADPAQ",
|
| 367 |
+
"caption": "Ananya ki gaand moti ho rhi hai reason non stop chudai"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIELGpD5FSPJgInOnbasrnZ73iKBp_AAALtEGsbI34hVjndIEz4oJwRAQADAgADeQADPAQ",
|
| 371 |
+
"caption": "Ananya ki geeli kacchi for sale"
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"file_id": "AgACAgUAAyEFAATtFlP2AAIELWpD5FRq1l37aE4iDh0TQeKDwQO9AALuEGsbI34hVpJp1MTThRZBAQADAgADeQADPAQ",
|
| 375 |
+
"caption": "Chudne ke liye bani hai kuttiya aur bas chude gi hi"
|
| 376 |
+
}
|
| 377 |
+
],
|
| 378 |
+
"base_price": 90,
|
| 379 |
+
"features": [
|
| 380 |
+
"Cute face 💋",
|
| 381 |
+
"Sex doll figure 👄",
|
| 382 |
+
"Chote chooche 🍒"
|
| 383 |
+
],
|
| 384 |
+
"addedBy": "6260798528"
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"id": "custom_1782837223614",
|
| 388 |
+
"name": "Kareena Kapoor",
|
| 389 |
+
"image_url": [
|
| 390 |
+
{
|
| 391 |
+
"file_id": "AgACAgUAAxkBAANJakPwRATXfKc72OpU7hnxUHbPRjcAArgNaxtmTyBWsZKGkpJqfO4BAAMCAAN5AAM8BA",
|
| 392 |
+
"caption": "Kareena saali ki gol genri navel mei thuk du"
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"file_id": "AgACAgUAAxkBAANKakPwRAxAlftPIZhv-R41VcJ5mOAAArcNaxtmTyBWSU3VToplvDQBAAMCAAN4AAM8BA",
|
| 396 |
+
"caption": "Saali kareena bc kya mast slutty face hai kuttiya ka"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"file_id": "AgACAgUAAxkBAANLakPwRFgHXtJHInQ3pOAfWlKR_CoAArkNaxtmTyBWMkZk4pbX4coBAAMCAAN5AAM8BA",
|
| 400 |
+
"caption": "Is tandoori murgi ko noch noch ke gha jaun"
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"file_id": "AgACAgUAAxkBAANMakPwRAXE1bJnw9eM9wo7RQXYwrwAAroNaxtmTyBWWKVvAAFHQPohAQADAgADeAADPAQ",
|
| 404 |
+
"caption": "Kareena mommy ko sabke saamne saree utar ke nangi chut mei loda ghosaun"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"file_id": "AgACAgUAAxkBAANNakPwRBblHdS_6EoFmIPdkjmTgwwAArsNaxtmTyBWylBBEu--wBwBAAMCAAN4AAM8BA",
|
| 408 |
+
"caption": "Bebo ke chutado pe chaate marna chahoge?"
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"file_id": "AgACAgUAAxkBAANOakPwRF4VZLw2skTwn4EFn4uRF9AAArwNaxtmTyBWQ3xy6AEdMf0BAAMCAAN4AAM8BA",
|
| 412 |
+
"caption": "Kareena mommy ka meetha poshtik doodh direct mommy ke choochiyon se"
|
| 413 |
+
}
|
| 414 |
+
],
|
| 415 |
+
"base_price": 100,
|
| 416 |
+
"features": [
|
| 417 |
+
"Dudharu Mommy🍼",
|
| 418 |
+
"Full chubby badan 🥵",
|
| 419 |
+
"Deep sexy navel 💦🤤"
|
| 420 |
+
],
|
| 421 |
+
"addedBy": "6260798528"
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"id": "custom_1782887579068",
|
| 425 |
+
"name": "Tamannaahh Bhatia",
|
| 426 |
+
"image_url": [
|
| 427 |
+
{
|
| 428 |
+
"file_id": "AgACAgUAAxkBAAN2akS5dxz_8f0wy4saCX9aP-XT_HwAAgUPaxtmTyBWNcNcaI3Gl5YBAAMCAAN5AAM8BA",
|
| 429 |
+
"caption": "Tamannaah saj ke taiyaar hai kyuni aaj pati ka boss aane wala hai promotion ki baat krne"
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"file_id": "AgACAgUAAxkBAAN4akS5tZrQvlsLKcSk_-RojOk6X4QAAvARaxumFClWsJMn4qnfzGEBAAMCAAN5AAM8BA",
|
| 433 |
+
"caption": "Perfect chubby mommy, made to enjoy her meaty body 🫦🥵"
|
| 434 |
+
},
|
| 435 |
+
{
|
| 436 |
+
"file_id": "AgACAgUAAxkBAAN6akS57-bgKfoGsE8jYQfaX0P_6n8AAu8RaxumFClWCS-j7aKO_20BAAMCAAN5AAM8BA",
|
| 437 |
+
"caption": "Gora chikna thul thula malaidaa pet chaatne ka maza hi kuch aur hai 😋🤤💦"
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"file_id": "AgACAgUAAxkBAAN8akS6QI8IssskTMOL0fGxkVLw238AAgcPaxtmTyBWpxF5CHxSdH4BAAMCAAN5AAM8BA",
|
| 441 |
+
"caption": "Tammu mommy ke ye bade bade raseele doodh se bhare boobs 🥵🤤👅"
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"file_id": "AgACAgUAAxkBAAN-akS6xyftGCmIHUnpEzRrKmwU_SIAAgkPaxtmTyBWIUy0wTxieDYBAAMCAAN4AAM8BA",
|
| 445 |
+
"caption": "park mei mai aur dost ki maa akele ha 😈 soch rha hu jhadiyon mei le jaake gaand mei ghoosa gu 🍆"
|
| 446 |
+
}
|
| 447 |
+
],
|
| 448 |
+
"base_price": 120,
|
| 449 |
+
"features": [
|
| 450 |
+
"Sexy chubby MILF 🫦",
|
| 451 |
+
"Doodhiya Badan 💦🍼",
|
| 452 |
+
"Ek number Raand 👄🥵"
|
| 453 |
+
],
|
| 454 |
+
"addedBy": "6260798528"
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
"id": "custom_1782889862037",
|
| 458 |
+
"name": "Pooja Hegde",
|
| 459 |
+
"image_url": [
|
| 460 |
+
{
|
| 461 |
+
"file_id": "AgACAgUAAxkBAAOFakS-RZNbq3IK26fZfhNmednStiEAAhAPaxtmTyBW8HMw8mBLaQcBAAMCAAN5AAM8BA",
|
| 462 |
+
"caption": "Aise sexy face pe apni moooth nikal ke rgdni chahiye💦🥵"
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"file_id": "AgACAgUAAxkBAAOHakS_LUaW3Qyqxkzl9e2XzXOfaDAAAscQaxuraiBWO_PLpL0J89QBAAMCAAN5AAM8BA",
|
| 466 |
+
"caption": "Pooja randi at her boss pool party👙, ab blowjob deke promotion legi 🍆💦😵"
|
| 467 |
+
},
|
| 468 |
+
{
|
| 469 |
+
"file_id": "AgACAgUAAxkBAAOJakS_0mS-JrCrGv6rzG_5ewKRZVkAAsgQaxuraiBWREpefPAOBeABAAMCAAN4AAM8BA",
|
| 470 |
+
"caption": "Pooja didi jhadiyon mei bula rhi hai, 💦 lgta hai aaj finally cousin ko chodne ka sukh milega🤤🥵"
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"file_id": "AgACAgUAAxkBAAOLakTAHntHRhifyRQyUgcvEYCFgDkAAskQaxuraiBWVObgxhoBYgEBAAMCAAN5AAM8BA",
|
| 474 |
+
"caption": "Pooja saali ki ye gol gehri navel mei loda ghoosedge chodu 🥵💦💦💦"
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"file_id": "AgACAgUAAxkBAAONakTA7dqvDtsW735dwFifCBOPIWgAAhgPaxtmTyBW6KAJXl9eqfkBAAMCAAN5AAM8BA",
|
| 478 |
+
"caption": "In moti moti thighs ko dabakar failake 🍑 pooja ki kaali chut ke darshan karenge sab🍆💦🫦"
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"file_id": "AgACAgUAAxkBAAOPakTFAVaHBjZEFNdaGVTqChe-yWsAAigPaxtmTyBW9YAeXsQz5tkBAAMCAAN5AAM8BA",
|
| 482 |
+
"caption": "Ye dusky moti gaand pe chaate maar maar kar laal krdo🍑✋🏻🥵"
|
| 483 |
+
}
|
| 484 |
+
],
|
| 485 |
+
"base_price": 80,
|
| 486 |
+
"features": [
|
| 487 |
+
"Choco beauty 🍫",
|
| 488 |
+
"Deep sexy navel 👄",
|
| 489 |
+
"Randi figure 🥵💦",
|
| 490 |
+
"Kaale nipples🍼"
|
| 491 |
+
],
|
| 492 |
+
"addedBy": "6260798528"
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"id": "custom_1782898820472",
|
| 496 |
+
"name": "Samantha",
|
| 497 |
+
"image_url": [
|
| 498 |
+
{
|
| 499 |
+
"file_id": "AgACAgUAAxkBAAOVakTg2SkQ27obgEdiLs00Pqixm9kAAh8OaxtmTyhWqZNrKWECdKgBAAMCAAN5AAM8BA",
|
| 500 |
+
"caption": "kuttiya ki ye chikni curvy kamar ahhhh..🥵🥵🥵🫦"
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
"file_id": "AgACAgUAAxkBAAOXakThElFszWlsdgOtcHTzLyEOgjMAAiEOaxtmTyhWjzsBQ9I7VHwBAAMCAAN4AAM8BA",
|
| 504 |
+
"caption": "Samantha aunty taiyaar hai ghodi bnke 🥵 ab bas jaldi se aunty ki gaand mei apna loda ghusa deta hu 🍆💦"
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"file_id": "AgACAgUAAxkBAAOZakThk9LrC3Vc-TA-m08P13YMP9AAAiIOaxtmTyhWKrBbbrycA-YBAAMCAAN5AAM8BA",
|
| 508 |
+
"caption": "Dekh to aise rhi hai jaise abhi lapak ke loda 🥵 mooh mei le legi saali 🍆💦"
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"file_id": "AgACAgUAAxkBAAObakTjd5RGeV40IvihyRYavbOXZ_cAAiQOaxtmTyhW2_ecmKRjWMcBAAMCAAN4AAM8BA",
|
| 512 |
+
"caption": "Sir aapki zip pe ye white white kya laga hai, eww ye to geela hai 💦💦"
|
| 513 |
+
},
|
| 514 |
+
{
|
| 515 |
+
"file_id": "AgACAgUAAxkBAAOdakTkbEki--IPiPSsw2hH9vwXI6cAAikOaxtmTyhW5_z3vNTqJSQBAAMCAAN5AAM8BA",
|
| 516 |
+
"caption": "Randi Samantha ka nanga naach 🥵"
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"file_id": "AgACAgUAAxkBAAOfakUv58A6JkKTbNZS9gcYjmY-kjUAAtMOaxtmTyhWjNt3u2EYCc8BAAMCAAN5AAM8BA",
|
| 520 |
+
"caption": "Samantha ke in raseele honthon 👄🫦ko choos ke saali ke mooh hi thook du 💦"
|
| 521 |
+
}
|
| 522 |
+
],
|
| 523 |
+
"base_price": 60,
|
| 524 |
+
"features": [
|
| 525 |
+
"Ras bhare laal honth 💋",
|
| 526 |
+
"Moti tight ass 🍑",
|
| 527 |
+
"Randi face🥵💋"
|
| 528 |
+
],
|
| 529 |
+
"addedBy": "6260798528"
|
| 530 |
+
}
|
| 531 |
+
],
|
| 532 |
+
"sessions": {},
|
| 533 |
+
"deletedDefaultIds": [
|
| 534 |
+
"naruto",
|
| 535 |
+
"goku",
|
| 536 |
+
"luffy",
|
| 537 |
+
"ichigo",
|
| 538 |
+
"midoriya"
|
| 539 |
+
],
|
| 540 |
+
"connections": {
|
| 541 |
+
"6260798528": "-1003977663478"
|
| 542 |
+
},
|
| 543 |
+
"requiredPlayers": 2
|
| 544 |
+
}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build: .
|
| 4 |
+
ports:
|
| 5 |
+
- "8000:8000"
|
| 6 |
+
restart: always
|
documentation.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎴 Telegram Auction Bot Documentation
|
| 2 |
+
|
| 3 |
+
Welcome to the **Telegram Auction Bot** documentation! This document provides a comprehensive guide to the bot's architecture, setup, commands, and inner workings.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🏗️ Architecture & Flow
|
| 8 |
+
|
| 9 |
+
The bot is built on Node.js using the CommonJS module system and the `node-telegram-bot-api` library. It uses a lightweight, local JSON file (`db.json`) as a persistent database, and manages active bidding sessions dynamically in-memory.
|
| 10 |
+
|
| 11 |
+
```mermaid
|
| 12 |
+
flowchart TD
|
| 13 |
+
A[Telegram Group Chat] -->|"/help" or "bid start"| B[Telegram Bot Listener]
|
| 14 |
+
B -->|Bidding Command| C{Session Manager}
|
| 15 |
+
C -->|No Session / Phase: inactive| D[Initialize Session]
|
| 16 |
+
C -->|Phase: bidding| E[Bidding Engine]
|
| 17 |
+
E -->|Timer ticks / Bid placed| F[Start/Reset Timer]
|
| 18 |
+
F -->|Timer reaches 0| G[Resolve Bid]
|
| 19 |
+
G -->|Update DB & Standings| H[(db.json Database)]
|
| 20 |
+
G -->|Next Actress / End Session| C
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## ⚙️ Configuration & Data Store
|
| 26 |
+
|
| 27 |
+
### 1. Environment Variables (`.env`)
|
| 28 |
+
The bot reads its credentials from a `.env` file at the root of the project:
|
| 29 |
+
* `BOT_TOKEN`: Your secret API token obtained from Telegram's `@BotFather`.
|
| 30 |
+
* `ADMIN_IDS`: A comma-separated list of Telegram User IDs (e.g. `6260798528`) who have access to administrative controls.
|
| 31 |
+
|
| 32 |
+
### 2. Local Database (`db.json`)
|
| 33 |
+
Persistent state is saved to `db.json` whenever records change. The schema contains:
|
| 34 |
+
* `users`: Tracks user statistics, lifetime coins, wins, and won actresses.
|
| 35 |
+
* `customCharacters`: Holds actresses added by admins (including `file_id` references).
|
| 36 |
+
* `deletedDefaultIds`: Tracks hardcoded actress IDs that the admin has deleted (hidden).
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 💬 Command Reference
|
| 41 |
+
|
| 42 |
+
### For Everyone
|
| 43 |
+
These commands can be run by any player in the group chat.
|
| 44 |
+
|
| 45 |
+
| Command | Action / Input | Description |
|
| 46 |
+
| :--- | :--- | :--- |
|
| 47 |
+
| `bid start` | Plain text | Join or start an auction session. |
|
| 48 |
+
| `[amount]` | Plain number (e.g., `120`) | Place a bid during an active auction. |
|
| 49 |
+
| `/leaderboard` | Slash command | Shows the all-time top 10 players by total wins. |
|
| 50 |
+
| `/mystats` | Slash command | Shows your current stats, lifetime coins, and list of won actresses. |
|
| 51 |
+
| `/listactresses` | Slash command | Lists all active actresses in the pool. |
|
| 52 |
+
| `/how` | Slash command | Shows simple guide on how to play (rules & constraints). |
|
| 53 |
+
| `/help` or `/start` | Slash command | Shows the helper text (public commands only). |
|
| 54 |
+
|
| 55 |
+
### For Admins Only
|
| 56 |
+
These commands require the sender's Telegram User ID to be listed in `ADMIN_IDS` in the `.env` file.
|
| 57 |
+
|
| 58 |
+
| Command | Syntax | Description |
|
| 59 |
+
| :--- | :--- | :--- |
|
| 60 |
+
| **Admin Help** | `/helpadmin` | Lists all admin commands and details. |
|
| 61 |
+
| **View Status** | `/adminstatus` | Prints process uptime, memory usage, active sessions, registered users, and system versions. |
|
| 62 |
+
| **Inspect Session** | `/adminsession` | Shows the active phase, ready users, current actress, current highest bid, timer countdown, and participants. |
|
| 63 |
+
| **Set Min Players** | `/setminplayers [num]` | Configures the minimum number of users required to start an auction lobby. |
|
| 64 |
+
| **Add Actress** | Send photo with caption starting with `/newactress` | Adds a new actress using Telegram as a free photo host (uses line-separated caption). |
|
| 65 |
+
| **View skins** | `/actressimages [id]` | Sends a media album containing all skins/images of an actress. |
|
| 66 |
+
| **Add Image / Skin** | Send photo with caption starting with `/addimage` | Adds an alternative artwork/skin to an actress, optionally with a caption on line 2. |
|
| 67 |
+
| **Update Actress** | `/updateactress [id] , [field] , [value]` | Updates actress fields (`name`, `price`, or `features`). |
|
| 68 |
+
| **Delete Actress** | `/deleteactress [id]` | Deletes a custom actress or hides a default actress. |
|
| 69 |
+
| **Bulk Delete** | `/deletemultiactress [id1], [id2]...` | Deletes multiple actress IDs in a single command. |
|
| 70 |
+
| **Cancel Session** | `/cancelsession` | Forcefully terminates the active session immediately, clearing all timers. |
|
| 71 |
+
| **Edit Image Caption** | `/editcaption [new caption]` | Edits the caption of an existing image (must reply to the target photo message). |
|
| 72 |
+
| **Delete Image** | `/deleteimage` | Deletes a specific image from an actress's skin/image pool (must reply to the target photo message). |
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 🎮 Actress Management (CRUD)
|
| 77 |
+
|
| 78 |
+
### 1. Adding an Actress (No Image Required / Direct Photo Upload)
|
| 79 |
+
* **Method A (Direct Photo Upload)**: Send an image to the bot with a caption formatted by **new lines**:
|
| 80 |
+
```text
|
| 81 |
+
/newactress Disha Patani
|
| 82 |
+
80
|
| 83 |
+
🍥 Series: Bollywood
|
| 84 |
+
⚡ Power: Dancer
|
| 85 |
+
```
|
| 86 |
+
* **Line 1**: `/newactress [Actress Name]`
|
| 87 |
+
* **Line 2**: `[Base Price]` (a valid positive number)
|
| 88 |
+
* **Line 3+**: Features (each feature on a new line)
|
| 89 |
+
|
| 90 |
+
* **Method B (Text-Only, No Image)**: Send this text command to add an actress without any photo:
|
| 91 |
+
```text
|
| 92 |
+
/submitactress Disha Patani , 80 , 🍥 Series: Bollywood , ⚡ Power: Dancer
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### 2. Storing Multiple Images / Skins with Captions (Single/Plural)
|
| 96 |
+
* **Method A (Single Image / Caption)**: Send a photo with a caption starting with:
|
| 97 |
+
```text
|
| 98 |
+
/addimage disha
|
| 99 |
+
Alternative fit!
|
| 100 |
+
```
|
| 101 |
+
* **Method B (Album / Sequential Captions)**: Upload multiple photos as a Telegram album (media group). In the main album caption, write `/addimages [actress_id]` followed by the captions for each photo on separate lines:
|
| 102 |
+
```text
|
| 103 |
+
/addimages disha
|
| 104 |
+
Traditional wear
|
| 105 |
+
Western wear
|
| 106 |
+
Casual fit
|
| 107 |
+
```
|
| 108 |
+
* **Behavior**: The bot automatically debounces album uploads, sorts the photos based on their send order, and maps each newline caption sequentially (Photo 1 gets caption line 1, Photo 2 gets caption line 2, etc.).
|
| 109 |
+
* When an actress has multiple images, the bot will pick a **random image** to present initially. During the bidding phase, the bot will cycle through all images and send them to the group chat **every 5 seconds** alongside the live bid status!
|
| 110 |
+
|
| 111 |
+
### 3. Modifying Actresses (Copy-on-Write)
|
| 112 |
+
You can update existing actress attributes by typing:
|
| 113 |
+
`/updateactress [id] , [field] , [value]`
|
| 114 |
+
Supported fields: `name`, `price`, `features` (comma-separated list).
|
| 115 |
+
> [!NOTE]
|
| 116 |
+
> **Copy-on-Write (COW)**: If you update a hardcoded default actress (defined in `characters.js`), the bot automatically duplicates it into the `db.json` database with the edits, and flags the original default actress as hidden. This keeps default actresses editable without changing source code!
|
| 117 |
+
|
| 118 |
+
### 4. Editing Image Captions & Deleting Specific Images (Reply Commands)
|
| 119 |
+
Admins can manage individual images from an actress's skin/image pool directly inside the Telegram chat interface:
|
| 120 |
+
* **Edit Caption**: Reply to the specific photo message (sent by the bot via `/actressimages`) with `/editcaption [new caption text]`. To clear/remove the caption, reply with just `/editcaption`.
|
| 121 |
+
* **Delete Image**: Reply to the specific photo message with `/deleteimage`. This permanently deletes that image skin from the actress's database record.
|
| 122 |
+
* *Note: Both commands fully support Copy-on-Write (COW) for default actresses, keeping your database safe and clean.*
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## 🔌 Private Admin Control (Connect Chat)
|
| 127 |
+
|
| 128 |
+
Admins can link their private direct message (DM) chat with the bot to a specific Telegram group chat. This allows running active session commands (such as cancelling a session or checking live session stats) privately inside the bot DM, keeping the main group chat free of admin logs and status text.
|
| 129 |
+
|
| 130 |
+
### Connection Steps:
|
| 131 |
+
1. **Request Connection String**: In your Telegram group chat, type `/connect`.
|
| 132 |
+
2. **Retrieve Connection ID**: The bot will send connection instructions containing the group chat's ID (e.g. `/connect -100xxxxxxxxxx`).
|
| 133 |
+
3. **Establish Connection**: Copy this command, open a private chat with the bot, and send the copied command `/connect -100xxxxxxxxxx`.
|
| 134 |
+
4. **Usage**:
|
| 135 |
+
- Run `/adminsession` in the private DM to fetch live active bidding statistics of the connected group.
|
| 136 |
+
- Run `/cancelsession` in the private DM to immediately cancel the connected group's session. The bot will notify players in the group and confirm the cancellation to your private DM.
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## ⚙️ Core Engines & Technical Optimizations
|
| 141 |
+
|
| 142 |
+
### 1. Concurrency Lock (`session.resolving`)
|
| 143 |
+
To prevent duplicate messages and database integrity issues, the bot uses a lock flag during actress transitions:
|
| 144 |
+
* When a bid timer hits `0`, `resolveBid()` runs and sets `session.resolving = true` synchronously.
|
| 145 |
+
* Any bids sent while the bot is writing to the database or waiting for `sendMessage` to resolve are safely rejected.
|
| 146 |
+
* The lock is released (`session.resolving = false`) only when the next actress is successfully loaded and sent.
|
| 147 |
+
|
| 148 |
+
### 2. Combined Bid Confirmation and Countdown
|
| 149 |
+
Rather than sending a separate bid confirmation message and a countdown timer message consecutively (which clutters the chat), the bot merges them into a **single active countdown message**.
|
| 150 |
+
* When a player places a valid bid, the previous countdown message is immediately deleted.
|
| 151 |
+
* A new combined message is posted at the bottom of the chat, stating the current highest bid details alongside the active progress bar and remaining seconds.
|
| 152 |
+
* To avoid Telegram rate limits (429) on message editing, the active countdown edits itself only every multiple of 5 seconds and once per second in the final 3 seconds.
|
| 153 |
+
|
| 154 |
+
### 3. Resilient Error Handling
|
| 155 |
+
All asynchronous Telegram API calls are wrapped in robust `try-catch` blocks. Additionally, global listeners for `polling_error`, `unhandledRejection`, and `uncaughtException` are set up. If a temporary connection drop occurs (such as an `ECONNRESET` socket reset), the bot logs the error and recovers automatically instead of crashing Node.js.
|
| 156 |
+
|
| 157 |
+
### 4. Automatic Message Cleanup (De-Clutter)
|
| 158 |
+
To keep group chats clean, the bot logs all auction-related message IDs (such as bid starts, countdown timer messages, bid confirmations, and player bid messages) in-memory during active sessions. When an auction session completes, the bot iterates over these IDs and executes `deleteMessage` API calls to delete them in bulk, leaving only the final scoreboard.
|
| 159 |
+
Additionally, if a user places an invalid bid (e.g. less than the base price or highest bid, or if they are not a participant), both the player's invalid bid message and the bot's warning message are automatically deleted after **3 seconds** to keep the active bidding screen clear of clutter.
|
| 160 |
+
> [!IMPORTANT]
|
| 161 |
+
> The bot requires **Delete Messages** permissions as a group administrator to delete other players' bidding messages. If it lacks this permission, it will only delete its own messages.
|
| 162 |
+
|
| 163 |
+
### 5. Transition Delay
|
| 164 |
+
Between actresses, the bot introduces a mandatory 20-second cooldown period configured via `NEXT_CHARACTER_DELAY_MS`. When an actress is resolved, the bot sends a notification `⏳ Next Bid in 20s — Get ready for [Actress Name]!` to alert players and allow them to prepare before starting the next bidding timer.
|
| 165 |
+
|
| 166 |
+
### 6. In-Group Restricted Actress Selection Lobby
|
| 167 |
+
When players join the session by typing `bid start` in the group chat, the bot sends an interactive inline keyboard menu listing all active actresses directly in the same group chat.
|
| 168 |
+
- **Player Specific Interaction**: Each player's menu is restricted to them. If another user attempts to click a button on player A's keyboard, the bot displays a pop-up alert: `⚠️ This selection menu is only for the player who requested it!`.
|
| 169 |
+
- **Duplicate Prevention**: If a player attempts to select an actress already selected by another player in the lobby, the bot displays a pop-up warning: `⚠️ This actress is already selected by @username! Please select another.`, blocking duplicate selections.
|
| 170 |
+
- **Toggles & Finalization**: Players toggle their preferred actresses and click `Done Selecting` to lock choices.
|
| 171 |
+
- **Empty Selection Prevention**: If a player attempts to click `Done Selecting` without having selected at least one actress, the bot blocks the finalization and displays an alert pop-up: `⚠️ You must select at least one actress before clicking Done!`.
|
| 172 |
+
- **Deferred Start**: The auction bidding session will not begin until the player count reaches `REQUIRED_PLAYERS` AND **all joined players** have finalized their choices by clicking `Done Selecting`.
|
| 173 |
+
- **Auction Pool Construction**: The bot merges selections from all lobby participants to construct the custom bidding pool. Bidding runs exclusively on the actresses chosen by the players (no padding with random actresses, no truncation).
|
| 174 |
+
|
| 175 |
+
### 7. Resolution Outcome Photo Captions
|
| 176 |
+
Rather than sending intermediate actress photos every 5 seconds, the bot now stays quiet during active countdown ticks. When a bidding round is resolved, the bot sends the actress's photo with the outcome (SOLD details or UNSOLD notice) embedded directly as the caption.
|
| 177 |
+
- **Image/Skin Variation**: If an actress has multiple skins/photos in the database, the bot automatically selects a **different** skin for the outcome message than the one displayed at the start of the auction. If only one image is available, it safely falls back to the same image. This ensures a fresh visual presentation upon round completion.
|
| 178 |
+
|
| 179 |
+
### 8. Bulk Actress Import System (CSV)
|
| 180 |
+
To add hundreds of actresses in bulk without adding them one-by-one:
|
| 181 |
+
1. Export your actress data as a CSV file or fill out the automatically generated `characters.csv` file in the project root directory.
|
| 182 |
+
2. Structure the columns as: `Name,Base Price,Features,Image URLs`.
|
| 183 |
+
- **Features**: Semicolon-separated list of details (e.g. `Series: Bollywood; Rarity: SSR`).
|
| 184 |
+
- **Image URLs / File IDs**: Semicolon-separated list of image references. Add a caption directly after any reference using a pipe symbol (e.g. `AgACAg...|Disha Patani fit 1; AgACAg...|Disha Patani fit 2`).
|
| 185 |
+
3. Run the script from the command line:
|
| 186 |
+
```bash
|
| 187 |
+
node import_characters.js
|
| 188 |
+
```
|
| 189 |
+
4. The script will parse the entries, run validation checks (reporting line errors if any format is incorrect), and append the actresses to the active database.
|
import_characters.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require("dotenv").config();
|
| 2 |
+
const fs = require("fs");
|
| 3 |
+
const path = require("path");
|
| 4 |
+
const { MongoClient } = require("mongodb");
|
| 5 |
+
|
| 6 |
+
const DB_FILE = path.join(__dirname, "db.json");
|
| 7 |
+
const CSV_FILE = path.join(__dirname, "characters.csv");
|
| 8 |
+
|
| 9 |
+
// Helper to parse a CSV line, respecting double quotes
|
| 10 |
+
function parseCSVLine(line) {
|
| 11 |
+
const result = [];
|
| 12 |
+
let current = "";
|
| 13 |
+
let inQuotes = false;
|
| 14 |
+
|
| 15 |
+
for (let i = 0; i < line.length; i++) {
|
| 16 |
+
const char = line[i];
|
| 17 |
+
if (char === '"') {
|
| 18 |
+
inQuotes = !inQuotes;
|
| 19 |
+
} else if (char === "," && !inQuotes) {
|
| 20 |
+
result.push(current.trim());
|
| 21 |
+
current = "";
|
| 22 |
+
} else {
|
| 23 |
+
current += char;
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
result.push(current.trim());
|
| 27 |
+
return result;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Generate template if CSV does not exist
|
| 31 |
+
if (!fs.existsSync(CSV_FILE)) {
|
| 32 |
+
const templateContent = `Name,Base price,features,Image URL 1,Caption 1,Image URL 2,Caption 2,Image URL 3,CAption 3,Category
|
| 33 |
+
Goku,100,"Series: Dragon Ball; Rarity: SSR",AgACAg...,Goku Super Saiyan,AgACAg...,Goku Base Form,,,Anime
|
| 34 |
+
Naruto,80,"Series: Naruto; Rarity: SR",AgACAg...,Naruto Sage Mode,,,,,,Anime
|
| 35 |
+
Luffy,90,"Series: One Piece; Rarity: UR",AgACAg...,Luffy Gear 5,,,,,,Anime
|
| 36 |
+
`;
|
| 37 |
+
fs.writeFileSync(CSV_FILE, templateContent, "utf8");
|
| 38 |
+
console.log("📝 No 'characters.csv' file was found.");
|
| 39 |
+
console.log("✅ Created a sample 'characters.csv' with the requested columns!");
|
| 40 |
+
console.log("ℹ️ Open 'characters.csv' in Excel/Google Sheets, add your characters, and run this script again.");
|
| 41 |
+
process.exit(0);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const fileContent = fs.readFileSync(CSV_FILE, "utf8");
|
| 45 |
+
const lines = fileContent.split(/\r?\n/).filter((line) => line.trim() !== "");
|
| 46 |
+
|
| 47 |
+
if (lines.length <= 1) {
|
| 48 |
+
console.log("⚠️ The 'characters.csv' file is empty or only contains the header row.");
|
| 49 |
+
process.exit(0);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Skip header
|
| 53 |
+
const header = lines[0];
|
| 54 |
+
const dataLines = lines.slice(1);
|
| 55 |
+
|
| 56 |
+
let successCount = 0;
|
| 57 |
+
let failCount = 0;
|
| 58 |
+
const errors = [];
|
| 59 |
+
const importedChars = [];
|
| 60 |
+
|
| 61 |
+
dataLines.forEach((line, index) => {
|
| 62 |
+
const lineNum = index + 2; // 1-indexed plus header line offset
|
| 63 |
+
const parts = parseCSVLine(line);
|
| 64 |
+
|
| 65 |
+
// We expect at least the first 3 columns
|
| 66 |
+
if (parts.length < 3) {
|
| 67 |
+
failCount++;
|
| 68 |
+
errors.push(`Line ${lineNum}: Missing core columns (Expected Name, Base price, features)`);
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const name = parts[0];
|
| 73 |
+
const basePriceVal = parts[1];
|
| 74 |
+
const featuresRaw = parts[2];
|
| 75 |
+
const img1 = parts[3];
|
| 76 |
+
const cap1 = parts[4];
|
| 77 |
+
const img2 = parts[5];
|
| 78 |
+
const cap2 = parts[6];
|
| 79 |
+
const img3 = parts[7];
|
| 80 |
+
const cap3 = parts[8];
|
| 81 |
+
const category = parts[9];
|
| 82 |
+
|
| 83 |
+
if (!name) {
|
| 84 |
+
failCount++;
|
| 85 |
+
errors.push(`Line ${lineNum}: Character Name is required.`);
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const base_price = parseInt(basePriceVal, 10);
|
| 90 |
+
if (isNaN(base_price) || base_price < 0) {
|
| 91 |
+
failCount++;
|
| 92 |
+
errors.push(`Line ${lineNum}: Invalid Base Price '${basePriceVal}'. Must be a non-negative number.`);
|
| 93 |
+
return;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Parse features (separated by ;)
|
| 97 |
+
const features = featuresRaw
|
| 98 |
+
? featuresRaw
|
| 99 |
+
.split(";")
|
| 100 |
+
.map((f) => f.trim())
|
| 101 |
+
.filter((f) => f.length > 0)
|
| 102 |
+
: [];
|
| 103 |
+
|
| 104 |
+
// Build image_url list
|
| 105 |
+
const image_url = [];
|
| 106 |
+
|
| 107 |
+
if (img1) {
|
| 108 |
+
if (cap1) {
|
| 109 |
+
image_url.push({ file_id: img1, caption: cap1 });
|
| 110 |
+
} else {
|
| 111 |
+
image_url.push(img1);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
if (img2) {
|
| 116 |
+
if (cap2) {
|
| 117 |
+
image_url.push({ file_id: img2, caption: cap2 });
|
| 118 |
+
} else {
|
| 119 |
+
image_url.push(img2);
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
if (img3) {
|
| 124 |
+
if (cap3) {
|
| 125 |
+
image_url.push({ file_id: img3, caption: cap3 });
|
| 126 |
+
} else {
|
| 127 |
+
image_url.push(img3);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Generate unique character ID
|
| 132 |
+
const id = `custom_${Date.now()}_${Math.floor(1000 + Math.random() * 9000)}`;
|
| 133 |
+
|
| 134 |
+
importedChars.push({
|
| 135 |
+
id,
|
| 136 |
+
name,
|
| 137 |
+
image_url,
|
| 138 |
+
base_price,
|
| 139 |
+
features,
|
| 140 |
+
category: category || "",
|
| 141 |
+
addedBy: "bulk_import"
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
successCount++;
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
async function saveImportedData() {
|
| 148 |
+
if (successCount === 0) {
|
| 149 |
+
console.log(`\n❌ Bulk Import Failed.`);
|
| 150 |
+
return;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
const mongoUri = process.env.MONGODB_URI;
|
| 154 |
+
if (mongoUri) {
|
| 155 |
+
console.log("\n🔌 Connecting to MongoDB for import...");
|
| 156 |
+
let client;
|
| 157 |
+
try {
|
| 158 |
+
client = new MongoClient(mongoUri);
|
| 159 |
+
await client.connect();
|
| 160 |
+
const db = client.db();
|
| 161 |
+
const collection = db.collection("bot_state");
|
| 162 |
+
|
| 163 |
+
const record = await collection.findOne({ _id: "state" });
|
| 164 |
+
let currentDB = record ? record.data : { customCharacters: [] };
|
| 165 |
+
currentDB.customCharacters = currentDB.customCharacters || [];
|
| 166 |
+
currentDB.customCharacters.push(...importedChars);
|
| 167 |
+
|
| 168 |
+
await collection.updateOne(
|
| 169 |
+
{ _id: "state" },
|
| 170 |
+
{ $set: { data: currentDB } },
|
| 171 |
+
{ upsert: true }
|
| 172 |
+
);
|
| 173 |
+
|
| 174 |
+
console.log(`\n🎉 Bulk Import Complete (MongoDB Atlas)!`);
|
| 175 |
+
console.log(`✅ Successfully imported *${successCount}* characters.`);
|
| 176 |
+
} catch (err) {
|
| 177 |
+
console.error("❌ Failed to save changes to MongoDB:", err.message || err);
|
| 178 |
+
process.exit(1);
|
| 179 |
+
} finally {
|
| 180 |
+
if (client) await client.close();
|
| 181 |
+
}
|
| 182 |
+
} else {
|
| 183 |
+
// Fallback to local db.json
|
| 184 |
+
console.log("\n💾 Saving to local db.json...");
|
| 185 |
+
let localDB = { customCharacters: [] };
|
| 186 |
+
if (fs.existsSync(DB_FILE)) {
|
| 187 |
+
try {
|
| 188 |
+
localDB = JSON.parse(fs.readFileSync(DB_FILE, "utf8"));
|
| 189 |
+
} catch (err) {
|
| 190 |
+
console.error("❌ Failed to parse local db.json:", err);
|
| 191 |
+
process.exit(1);
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
localDB.customCharacters = localDB.customCharacters || [];
|
| 195 |
+
localDB.customCharacters.push(...importedChars);
|
| 196 |
+
|
| 197 |
+
try {
|
| 198 |
+
fs.writeFileSync(DB_FILE, JSON.stringify(localDB, null, 2), "utf8");
|
| 199 |
+
console.log(`\n🎉 Bulk Import Complete (local file)!`);
|
| 200 |
+
console.log(`✅ Successfully imported *${successCount}* characters.`);
|
| 201 |
+
} catch (err) {
|
| 202 |
+
console.error("❌ Failed to save changes to db.json:", err);
|
| 203 |
+
process.exit(1);
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
if (failCount > 0) {
|
| 208 |
+
console.log(`⚠️ Failed to import *${failCount}* rows due to formatting/validation errors:`);
|
| 209 |
+
errors.forEach((err) => console.log(` - ${err}`));
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
saveImportedData();
|
index.js
ADDED
|
@@ -0,0 +1,2583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require("./logger");
|
| 2 |
+
require("dotenv").config();
|
| 3 |
+
const { TelegramBot } = require("node-telegram-bot-api");
|
| 4 |
+
const { DEFAULT_CHARACTERS } = require("./characters");
|
| 5 |
+
const { initDB, loadDB, saveDB, getUser } = require("./db");
|
| 6 |
+
|
| 7 |
+
const TOKEN = process.env.BOT_TOKEN;
|
| 8 |
+
const ADMIN_IDS = (process.env.ADMIN_IDS || "").split(",").map((id) => id.trim());
|
| 9 |
+
|
| 10 |
+
if (!TOKEN) {
|
| 11 |
+
console.error("❌ BOT_TOKEN not set in .env file!");
|
| 12 |
+
process.exit(1);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const bot = new TelegramBot(TOKEN, { polling: true });
|
| 16 |
+
|
| 17 |
+
let botUsername = "";
|
| 18 |
+
bot.getMe().then((me) => {
|
| 19 |
+
botUsername = me.username;
|
| 20 |
+
console.log(`🤖 Bot @${botUsername} is ready.`);
|
| 21 |
+
}).catch((err) => {
|
| 22 |
+
console.error("❌ Failed to get bot details:", err.message || err);
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
bot.on("polling_error", (error) => {
|
| 26 |
+
console.error("⚠️ Polling error:", error.message || error);
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
process.on("unhandledRejection", (reason, promise) => {
|
| 30 |
+
console.error("⚠️ Unhandled Rejection at:", promise, "reason:", reason);
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
process.on("uncaughtException", (error) => {
|
| 34 |
+
console.error("⚠️ Uncaught Exception:", error);
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// ─── Constants & Dynamic Configs ──────────────────────────────────────────────
|
| 38 |
+
let REQUIRED_PLAYERS = 1;
|
| 39 |
+
|
| 40 |
+
// Initialize MongoDB and health-check HTTP server
|
| 41 |
+
const http = require("http");
|
| 42 |
+
initDB().then(() => {
|
| 43 |
+
const dbBoot = loadDB();
|
| 44 |
+
if (typeof dbBoot.requiredPlayers === "number") {
|
| 45 |
+
REQUIRED_PLAYERS = dbBoot.requiredPlayers;
|
| 46 |
+
}
|
| 47 |
+
console.log(`🤖 Loaded REQUIRED_PLAYERS configuration: ${REQUIRED_PLAYERS}`);
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const server = http.createServer((req, res) => {
|
| 51 |
+
if (req.url === "/health" || req.url === "/") {
|
| 52 |
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
| 53 |
+
res.end("OK");
|
| 54 |
+
} else {
|
| 55 |
+
res.writeHead(404);
|
| 56 |
+
res.end();
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
const PORT = process.env.PORT || 10000;
|
| 60 |
+
server.listen(PORT, () => {
|
| 61 |
+
console.log(`📡 Health-check web server listening on port ${PORT}`);
|
| 62 |
+
});
|
| 63 |
+
const STARTING_COINS = 500;
|
| 64 |
+
const BID_TIMER_SECONDS = 15;
|
| 65 |
+
const CHARACTERS_PER_SESSION = 5;
|
| 66 |
+
const NEXT_CHARACTER_DELAY_MS = 20000; // 20-second delay between characters
|
| 67 |
+
|
| 68 |
+
// ─── In-memory session state ──────────────────────────────────────────────────
|
| 69 |
+
// sessions[chatId] = {
|
| 70 |
+
// phase: "waiting" | "bidding" | "inactive",
|
| 71 |
+
// readyUsers: Set of userIds,
|
| 72 |
+
// participants: { userId: { username, coins } },
|
| 73 |
+
// characters: [...],
|
| 74 |
+
// currentCharacterIndex: 0,
|
| 75 |
+
// currentHighestBid: null, // { userId, username, amount }
|
| 76 |
+
// timer: null (setTimeout handle),
|
| 77 |
+
// timerMessageId: null,
|
| 78 |
+
// timerInterval: null,
|
| 79 |
+
// secondsLeft: 0,
|
| 80 |
+
// }
|
| 81 |
+
const sessions = {};
|
| 82 |
+
const mediaGroupAccumulators = {};
|
| 83 |
+
const mediaGroupBuffer = {};
|
| 84 |
+
|
| 85 |
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
| 86 |
+
function isAdmin(userId) {
|
| 87 |
+
return ADMIN_IDS.includes(String(userId));
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function getAllCharacters(db) {
|
| 91 |
+
const deletedIds = db.deletedDefaultIds || [];
|
| 92 |
+
const activeDefaults = DEFAULT_CHARACTERS.filter((c) => !deletedIds.includes(c.id));
|
| 93 |
+
return [...activeDefaults, ...(db.customCharacters || [])];
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function shuffleArray(arr) {
|
| 97 |
+
const a = [...arr];
|
| 98 |
+
for (let i = a.length - 1; i > 0; i--) {
|
| 99 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 100 |
+
[a[i], a[j]] = [a[j], a[i]];
|
| 101 |
+
}
|
| 102 |
+
return a;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function getSession(chatId) {
|
| 106 |
+
if (!sessions[chatId]) {
|
| 107 |
+
sessions[chatId] = { phase: "inactive" };
|
| 108 |
+
}
|
| 109 |
+
return sessions[chatId];
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function formatCoins(n) {
|
| 113 |
+
return `💰 ${n} coins`;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
function trackMessage(chatId, message) {
|
| 117 |
+
const session = getSession(chatId);
|
| 118 |
+
if (session && session.phase !== "inactive") {
|
| 119 |
+
session.messagesToDelete = session.messagesToDelete || [];
|
| 120 |
+
if (typeof message === "number" || typeof message === "string") {
|
| 121 |
+
session.messagesToDelete.push(Number(message));
|
| 122 |
+
} else if (message && message.message_id) {
|
| 123 |
+
session.messagesToDelete.push(message.message_id);
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function deleteAfterDelay(chatId, messageId, delayMs = 3000) {
|
| 129 |
+
setTimeout(async () => {
|
| 130 |
+
try {
|
| 131 |
+
await bot.deleteMessage(chatId, messageId);
|
| 132 |
+
} catch (_) { }
|
| 133 |
+
}, delayMs);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
function getPrestigeTitle(db, userId) {
|
| 137 |
+
const users = Object.entries(db.users);
|
| 138 |
+
if (users.length === 0) return "";
|
| 139 |
+
|
| 140 |
+
// Sort by wins to find rankings
|
| 141 |
+
const sorted = [...users].sort((a, b) => b[1].totalWins - a[1].totalWins);
|
| 142 |
+
const rankIndex = sorted.findIndex(([uid]) => uid === userId);
|
| 143 |
+
|
| 144 |
+
// Find all character ownerships
|
| 145 |
+
const allChars = getAllCharacters(db);
|
| 146 |
+
const winsMap = {};
|
| 147 |
+
for (const [uid, user] of users) {
|
| 148 |
+
const uName = user.username || `User${uid}`;
|
| 149 |
+
const charsWon = user.characters || [];
|
| 150 |
+
for (const won of charsWon) {
|
| 151 |
+
if (!winsMap[won.characterId]) winsMap[won.characterId] = [];
|
| 152 |
+
winsMap[won.characterId].push({ userId: uid, username: uName, paidAmount: won.paidAmount || 0 });
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const ownedCharacterNames = [];
|
| 157 |
+
for (const char of allChars) {
|
| 158 |
+
const wins = winsMap[char.id];
|
| 159 |
+
if (wins && wins.length > 0) {
|
| 160 |
+
const counts = {};
|
| 161 |
+
for (const win of wins) {
|
| 162 |
+
if (!counts[win.userId]) counts[win.userId] = { count: 0, maxBid: 0 };
|
| 163 |
+
counts[win.userId].count += 1;
|
| 164 |
+
if (win.paidAmount > counts[win.userId].maxBid) {
|
| 165 |
+
counts[win.userId].maxBid = win.paidAmount;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
const sortedOwners = Object.entries(counts).sort((a, b) => {
|
| 169 |
+
if (b[1].count !== a[1].count) return b[1].count - a[1].count;
|
| 170 |
+
return b[1].maxBid - a[1].maxBid;
|
| 171 |
+
});
|
| 172 |
+
if (sortedOwners[0] && sortedOwners[0][0] === userId) {
|
| 173 |
+
ownedCharacterNames.push(char.name);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
let title = "";
|
| 179 |
+
if (rankIndex === 0) {
|
| 180 |
+
title += "👑 Server Champ #1 | ";
|
| 181 |
+
} else if (rankIndex === 1) {
|
| 182 |
+
title += "🥇 #2 | ";
|
| 183 |
+
} else if (rankIndex === 2) {
|
| 184 |
+
title += "🥈 #3 | ";
|
| 185 |
+
} else if (rankIndex > 2 && rankIndex < 10) {
|
| 186 |
+
title += `🏅 #${rankIndex + 1} | `;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
if (ownedCharacterNames.length > 0) {
|
| 190 |
+
if (ownedCharacterNames.length === 1) {
|
| 191 |
+
title += `[Owner of ${ownedCharacterNames[0]}] `;
|
| 192 |
+
} else {
|
| 193 |
+
title += `[Owner of ${ownedCharacterNames.length} actresses] `;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
return title.trim();
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
function generateTextTable(headers, widths, alignments, rows) {
|
| 201 |
+
let result = "";
|
| 202 |
+
|
| 203 |
+
function formatCell(str, width, align) {
|
| 204 |
+
let cleanStr = String(str);
|
| 205 |
+
if (cleanStr.length > width) {
|
| 206 |
+
cleanStr = cleanStr.substring(0, width - 1) + "…";
|
| 207 |
+
}
|
| 208 |
+
if (align === "right") {
|
| 209 |
+
return cleanStr.padStart(width, " ");
|
| 210 |
+
} else {
|
| 211 |
+
return cleanStr.padEnd(width, " ");
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// Draw Header
|
| 216 |
+
const headerCells = headers.map((h, i) => formatCell(h, widths[i], alignments[i]));
|
| 217 |
+
result += headerCells.join(" | ") + "\n";
|
| 218 |
+
|
| 219 |
+
// Draw Separator line
|
| 220 |
+
const separators = widths.map((w) => "-".repeat(w));
|
| 221 |
+
result += separators.join("-+-") + "\n";
|
| 222 |
+
|
| 223 |
+
// Draw Rows
|
| 224 |
+
for (const row of rows) {
|
| 225 |
+
const rowCells = row.map((cell, i) => formatCell(cell, widths[i], alignments[i]));
|
| 226 |
+
result += rowCells.join(" | ") + "\n";
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
return `\`\`\`\n${result}\`\`\``;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
function findUserByIdOrUsername(db, input) {
|
| 233 |
+
const cleanInput = input.trim().replace(/^@/, "");
|
| 234 |
+
if (db.users[cleanInput]) {
|
| 235 |
+
return { userId: cleanInput, user: db.users[cleanInput] };
|
| 236 |
+
}
|
| 237 |
+
const match = Object.entries(db.users).find(([uid, u]) => {
|
| 238 |
+
const dbUser = u.username ? u.username.replace(/^@/, "") : "";
|
| 239 |
+
return dbUser.toLowerCase() === cleanInput.toLowerCase();
|
| 240 |
+
});
|
| 241 |
+
if (match) {
|
| 242 |
+
return { userId: match[0], user: match[1] };
|
| 243 |
+
}
|
| 244 |
+
return null;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function buildSelectionKeyboard(groupId, ownerId, allChars, userSet) {
|
| 248 |
+
const keyboard = [];
|
| 249 |
+
// 2 characters per row
|
| 250 |
+
for (let i = 0; i < allChars.length; i += 2) {
|
| 251 |
+
const row = [];
|
| 252 |
+
const char1 = allChars[i];
|
| 253 |
+
const isSelected1 = userSet.has(char1.id);
|
| 254 |
+
row.push({
|
| 255 |
+
text: `${isSelected1 ? "✅ " : ""}${char1.name}`,
|
| 256 |
+
callback_data: `sel:${groupId}:${ownerId}:${char1.id}`
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
if (i + 1 < allChars.length) {
|
| 260 |
+
const char2 = allChars[i + 1];
|
| 261 |
+
const isSelected2 = userSet.has(char2.id);
|
| 262 |
+
row.push({
|
| 263 |
+
text: `${isSelected2 ? "✅ " : ""}${char2.name}`,
|
| 264 |
+
callback_data: `sel:${groupId}:${ownerId}:${char2.id}`
|
| 265 |
+
});
|
| 266 |
+
}
|
| 267 |
+
keyboard.push(row);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// Done button at the bottom
|
| 271 |
+
keyboard.push([
|
| 272 |
+
{
|
| 273 |
+
text: "🏁 Done Selecting",
|
| 274 |
+
callback_data: `done:${groupId}:${ownerId}`
|
| 275 |
+
}
|
| 276 |
+
]);
|
| 277 |
+
|
| 278 |
+
return keyboard;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
async function checkLobbyReadyAndStart(chatId) {
|
| 282 |
+
console.log(`[LOBBY] checkLobbyReadyAndStart: Checking lobby state in chat: ${chatId}`);
|
| 283 |
+
const session = getSession(chatId);
|
| 284 |
+
if (!session || session.phase !== "waiting") return;
|
| 285 |
+
|
| 286 |
+
const count = session.readyUsers.size;
|
| 287 |
+
if (count < REQUIRED_PLAYERS) {
|
| 288 |
+
console.log(`[LOBBY] checkLobbyReadyAndStart: Only ${count}/${REQUIRED_PLAYERS} players ready in chat: ${chatId}`);
|
| 289 |
+
return;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Check if everyone who is in readyUsers is done selecting
|
| 293 |
+
let allDone = true;
|
| 294 |
+
for (const uid of session.readyUsers.keys()) {
|
| 295 |
+
if (!session.userSelectionsDone || !session.userSelectionsDone[uid]) {
|
| 296 |
+
allDone = false;
|
| 297 |
+
break;
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
if (allDone) {
|
| 302 |
+
if (session.lobbyTimer) {
|
| 303 |
+
clearTimeout(session.lobbyTimer);
|
| 304 |
+
session.lobbyTimer = null;
|
| 305 |
+
}
|
| 306 |
+
console.log(`[LOBBY] checkLobbyReadyAndStart: All selections done in chat ${chatId}. Initializing bidding session...`);
|
| 307 |
+
const db = loadDB();
|
| 308 |
+
// Start the bidding session!
|
| 309 |
+
session.phase = "bidding";
|
| 310 |
+
session.participants = {};
|
| 311 |
+
session.readyUsers.forEach((uname, uid) => {
|
| 312 |
+
const user = getUser(db, uid, uname);
|
| 313 |
+
const globalCoins = user.coins || 0;
|
| 314 |
+
session.participants[uid] = {
|
| 315 |
+
username: uname,
|
| 316 |
+
coins: STARTING_COINS + globalCoins,
|
| 317 |
+
actressesWonCount: 0,
|
| 318 |
+
amountSpent: 0
|
| 319 |
+
};
|
| 320 |
+
});
|
| 321 |
+
session.readyUsers = new Map();
|
| 322 |
+
|
| 323 |
+
const allChars = getAllCharacters(db);
|
| 324 |
+
|
| 325 |
+
// Gather all unique character selections from lobby participants
|
| 326 |
+
const selectedIds = new Set();
|
| 327 |
+
if (session.userSelections) {
|
| 328 |
+
Object.entries(session.userSelections).forEach(([uid, charSet]) => {
|
| 329 |
+
if (session.participants[uid] && charSet instanceof Set) {
|
| 330 |
+
charSet.forEach((cid) => selectedIds.add(cid));
|
| 331 |
+
}
|
| 332 |
+
});
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
const selectedChars = allChars.filter((c) => selectedIds.has(c.id));
|
| 336 |
+
const remainingChars = allChars.filter((c) => !selectedIds.has(c.id));
|
| 337 |
+
|
| 338 |
+
let pool = [...selectedChars];
|
| 339 |
+
// Pad with random characters if there are less than 5
|
| 340 |
+
if (pool.length < 5) {
|
| 341 |
+
const needed = Math.min(5 - pool.length, remainingChars.length);
|
| 342 |
+
const randomSelection = shuffleArray(remainingChars).slice(0, needed);
|
| 343 |
+
pool.push(...randomSelection);
|
| 344 |
+
console.log(`[LOBBY] Padding session with ${needed} random actresses to reach minimum of 5.`);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
pool = shuffleArray(pool);
|
| 348 |
+
session.characters = pool;
|
| 349 |
+
session.currentCharacterIndex = 0;
|
| 350 |
+
session.currentHighestBid = null;
|
| 351 |
+
session.userSelections = {}; // Clear selections for next session
|
| 352 |
+
session.userSelectionsDone = {}; // Clear done flags
|
| 353 |
+
|
| 354 |
+
const playerList = Object.entries(session.participants)
|
| 355 |
+
.map(([uid, p]) => {
|
| 356 |
+
const title = getPrestigeTitle(db, uid);
|
| 357 |
+
const titleStr = title ? `[${title}] ` : "";
|
| 358 |
+
return `• ${titleStr}${p.username}`;
|
| 359 |
+
})
|
| 360 |
+
.join("\n");
|
| 361 |
+
|
| 362 |
+
console.log(`[SESSION] Starting auction session in chat ${chatId} with participants:`, Object.values(session.participants).map(p => p.username));
|
| 363 |
+
|
| 364 |
+
const startMsg = await bot.sendMessage(
|
| 365 |
+
chatId,
|
| 366 |
+
`🎉 *AUCTION STARTS NOW!*\n\n` +
|
| 367 |
+
`👥 *Players:*\n${playerList}\n\n` +
|
| 368 |
+
`💰 Each player starts with *${STARTING_COINS}$*\n` +
|
| 369 |
+
`🎴 *${pool.length} actresses* up for auction!\n\n` +
|
| 370 |
+
`_Get ready..._ 🥁`,
|
| 371 |
+
{ parse_mode: "Markdown" }
|
| 372 |
+
);
|
| 373 |
+
trackMessage(chatId, startMsg);
|
| 374 |
+
|
| 375 |
+
setTimeout(() => startNextCharacter(chatId), 3000);
|
| 376 |
+
} else {
|
| 377 |
+
console.log(`[LOBBY] checkLobbyReadyAndStart: Waiting for some players to complete their actress selections in chat: ${chatId}`);
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// ─── Timer logic ──────────────────────────────────────────────────────────────
|
| 382 |
+
async function startBidTimer(chatId) {
|
| 383 |
+
try {
|
| 384 |
+
const session = getSession(chatId);
|
| 385 |
+
|
| 386 |
+
// Clear any existing timer
|
| 387 |
+
clearBidTimer(session);
|
| 388 |
+
|
| 389 |
+
// Delete the previous countdown message to keep the active screen clutter-free
|
| 390 |
+
if (session.timerMessageId) {
|
| 391 |
+
try {
|
| 392 |
+
await bot.deleteMessage(chatId, session.timerMessageId);
|
| 393 |
+
} catch (_) { }
|
| 394 |
+
session.timerMessageId = null;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
session.secondsLeft = BID_TIMER_SECONDS;
|
| 398 |
+
|
| 399 |
+
// Post a countdown message
|
| 400 |
+
const char = session.characters[session.currentCharacterIndex];
|
| 401 |
+
const highBid = session.currentHighestBid;
|
| 402 |
+
|
| 403 |
+
console.log(`[TIMER] Starting bid timer for ${BID_TIMER_SECONDS}s in chat: ${chatId} for character: ${char.name}. Highest bid:`, highBid);
|
| 404 |
+
|
| 405 |
+
let timerMsg;
|
| 406 |
+
try {
|
| 407 |
+
timerMsg = await bot.sendMessage(
|
| 408 |
+
chatId,
|
| 409 |
+
buildTimerText(session.secondsLeft, highBid, char)
|
| 410 |
+
);
|
| 411 |
+
session.timerMessageId = timerMsg.message_id;
|
| 412 |
+
trackMessage(chatId, timerMsg);
|
| 413 |
+
} catch (err) {
|
| 414 |
+
console.error("❌ Failed to start bid timer (sendMessage failed):", err.message || err);
|
| 415 |
+
return;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// Update at specific intervals to avoid Telegram rate limits (429) on editing same message
|
| 419 |
+
session.timerInterval = setInterval(async () => {
|
| 420 |
+
session.secondsLeft -= 1;
|
| 421 |
+
|
| 422 |
+
const shouldUpdateText =
|
| 423 |
+
session.secondsLeft % 5 === 0 ||
|
| 424 |
+
session.secondsLeft <= 3;
|
| 425 |
+
|
| 426 |
+
if (shouldUpdateText || session.secondsLeft <= 0) {
|
| 427 |
+
try {
|
| 428 |
+
await bot.editMessageText(
|
| 429 |
+
buildTimerText(session.secondsLeft, session.currentHighestBid, char),
|
| 430 |
+
{ chat_id: chatId, message_id: session.timerMessageId }
|
| 431 |
+
);
|
| 432 |
+
} catch (_) { }
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
if (session.secondsLeft <= 0) {
|
| 436 |
+
console.log(`[TIMER] Timer expired for character: ${char.name} in chat: ${chatId}`);
|
| 437 |
+
clearInterval(session.timerInterval);
|
| 438 |
+
session.timerInterval = null;
|
| 439 |
+
try {
|
| 440 |
+
await resolveBid(chatId);
|
| 441 |
+
} catch (err) {
|
| 442 |
+
console.error("❌ Error resolving bid:", err);
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
}, 1000);
|
| 446 |
+
|
| 447 |
+
} catch (err) {
|
| 448 |
+
console.error("❌ Error starting bid timer:", err);
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
function clearBidTimer(session) {
|
| 453 |
+
if (session.timerInterval) {
|
| 454 |
+
console.log(`[TIMER] Clearing active timer interval`);
|
| 455 |
+
clearInterval(session.timerInterval);
|
| 456 |
+
session.timerInterval = null;
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
function buildTimerText(seconds, highBid, char) {
|
| 461 |
+
const bar = buildProgressBar(seconds, BID_TIMER_SECONDS);
|
| 462 |
+
if (!highBid) {
|
| 463 |
+
return `⏳ *No bids yet!* Place a bid before time runs out!\n${bar} ${seconds}s`;
|
| 464 |
+
}
|
| 465 |
+
const titlePrefix = highBid.title ? `[${highBid.title}] ` : "";
|
| 466 |
+
return (
|
| 467 |
+
`Highest Bid: ${highBid.amount}$ by ${highBid.username}\n` +
|
| 468 |
+
`${bar} ${seconds}s remaining`
|
| 469 |
+
);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
function buildProgressBar(current, total) {
|
| 473 |
+
const filled = Math.round((current / total) * 10);
|
| 474 |
+
return "🟩".repeat(filled) + "⬛".repeat(10 - filled);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
// ─── Bid resolution ───────────────────────────────────────────────────────────
|
| 478 |
+
async function resolveBid(chatId) {
|
| 479 |
+
try {
|
| 480 |
+
const session = getSession(chatId);
|
| 481 |
+
if (session.phase !== "bidding" || session.resolving) return;
|
| 482 |
+
|
| 483 |
+
session.resolving = true; // Lock session during resolution transition
|
| 484 |
+
|
| 485 |
+
const char = session.characters[session.currentCharacterIndex];
|
| 486 |
+
const highBid = session.currentHighestBid;
|
| 487 |
+
|
| 488 |
+
console.log(`[BID] Resolving bid in chat ${chatId} for character: ${char.name}. Highest bid:`, highBid);
|
| 489 |
+
|
| 490 |
+
const db = loadDB();
|
| 491 |
+
|
| 492 |
+
const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []);
|
| 493 |
+
let selectedImage = session.currentCharacterImage || (imageUrls.length > 0 ? char.image_url : null);
|
| 494 |
+
|
| 495 |
+
if (imageUrls.length > 1 && selectedImage) {
|
| 496 |
+
const currentIndex = imageUrls.findIndex((img) => {
|
| 497 |
+
const curFileId = typeof selectedImage === "object" && selectedImage !== null ? selectedImage.file_id : selectedImage;
|
| 498 |
+
const imgFileId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 499 |
+
return curFileId === imgFileId;
|
| 500 |
+
});
|
| 501 |
+
|
| 502 |
+
const otherImages = imageUrls.filter((_, idx) => idx !== currentIndex);
|
| 503 |
+
if (otherImages.length > 0) {
|
| 504 |
+
selectedImage = otherImages[Math.floor(Math.random() * otherImages.length)];
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
let fileId = selectedImage;
|
| 509 |
+
if (typeof selectedImage === "object" && selectedImage !== null) {
|
| 510 |
+
fileId = selectedImage.file_id;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
const hasImage = fileId && imageUrls.length > 0;
|
| 514 |
+
|
| 515 |
+
if (!highBid) {
|
| 516 |
+
console.log(`[BID] UNSOLD: Character "${char.name}" in chat ${chatId}`);
|
| 517 |
+
const unsoldText = `😔 *${char.name}* goes *UNSOLD!* No one placed a bid in time.`;
|
| 518 |
+
|
| 519 |
+
if (hasImage) {
|
| 520 |
+
try {
|
| 521 |
+
const unsoldMsg = await bot.sendPhoto(chatId, fileId, {
|
| 522 |
+
caption: unsoldText,
|
| 523 |
+
parse_mode: "Markdown"
|
| 524 |
+
});
|
| 525 |
+
trackMessage(chatId, unsoldMsg);
|
| 526 |
+
} catch (err) {
|
| 527 |
+
console.warn(`[BID] Failed to send photo for unsold character "${char.name}", sending text backup:`, err.message);
|
| 528 |
+
const unsoldMsg = await bot.sendMessage(chatId, unsoldText, { parse_mode: "Markdown" });
|
| 529 |
+
trackMessage(chatId, unsoldMsg);
|
| 530 |
+
}
|
| 531 |
+
} else {
|
| 532 |
+
const unsoldMsg = await bot.sendMessage(chatId, unsoldText, { parse_mode: "Markdown" });
|
| 533 |
+
trackMessage(chatId, unsoldMsg);
|
| 534 |
+
}
|
| 535 |
+
} else {
|
| 536 |
+
console.log(`[BID] SOLD: Character "${char.name}" sold to ${highBid.username} (ID: ${highBid.userId}) for ${highBid.amount} coins in chat ${chatId}`);
|
| 537 |
+
// Deduct coins from winner's session balance
|
| 538 |
+
session.participants[highBid.userId].coins -= highBid.amount;
|
| 539 |
+
session.participants[highBid.userId].actressesWonCount += 1;
|
| 540 |
+
session.participants[highBid.userId].amountSpent += highBid.amount;
|
| 541 |
+
|
| 542 |
+
// Save to persistent DB
|
| 543 |
+
const winner = getUser(db, highBid.userId, highBid.username);
|
| 544 |
+
winner.totalWins += 1;
|
| 545 |
+
winner.characters.push({
|
| 546 |
+
characterId: char.id,
|
| 547 |
+
characterName: char.name,
|
| 548 |
+
paidAmount: highBid.amount,
|
| 549 |
+
wonAt: new Date().toISOString(),
|
| 550 |
+
});
|
| 551 |
+
|
| 552 |
+
// Deduct bid from global persistent wallet balance
|
| 553 |
+
const oldGlobalCoins = winner.coins || 0;
|
| 554 |
+
winner.coins = Math.max(0, oldGlobalCoins - highBid.amount);
|
| 555 |
+
saveDB(db);
|
| 556 |
+
|
| 557 |
+
console.log(`[BID] Global wallet deduction for ${highBid.username}: ${oldGlobalCoins} -> ${winner.coins}`);
|
| 558 |
+
|
| 559 |
+
const title = getPrestigeTitle(db, highBid.userId);
|
| 560 |
+
const titlePrefix = title ? `[${title}] ` : "";
|
| 561 |
+
|
| 562 |
+
const soldText =
|
| 563 |
+
`🎉 *${char.name}* SOLD to *${titlePrefix}${highBid.username}* for ${formatCoins(highBid.amount)}!\n\n` +
|
| 564 |
+
`💼 Remaining session coins: ${formatCoins(session.participants[highBid.userId].coins)}\n` +
|
| 565 |
+
`💰 Global wallet balance: ${formatCoins(winner.coins)}`;
|
| 566 |
+
|
| 567 |
+
if (hasImage) {
|
| 568 |
+
try {
|
| 569 |
+
const soldMsg = await bot.sendPhoto(chatId, fileId, {
|
| 570 |
+
caption: soldText,
|
| 571 |
+
parse_mode: "Markdown"
|
| 572 |
+
});
|
| 573 |
+
trackMessage(chatId, soldMsg);
|
| 574 |
+
} catch (err) {
|
| 575 |
+
console.warn(`[BID] Failed to send photo for sold character "${char.name}", sending text backup:`, err.message);
|
| 576 |
+
const soldMsg = await bot.sendMessage(chatId, soldText, { parse_mode: "Markdown" });
|
| 577 |
+
trackMessage(chatId, soldMsg);
|
| 578 |
+
}
|
| 579 |
+
} else {
|
| 580 |
+
const soldMsg = await bot.sendMessage(chatId, soldText, { parse_mode: "Markdown" });
|
| 581 |
+
trackMessage(chatId, soldMsg);
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// Move to next character
|
| 586 |
+
session.currentCharacterIndex += 1;
|
| 587 |
+
session.currentHighestBid = null;
|
| 588 |
+
|
| 589 |
+
if (session.currentCharacterIndex >= session.characters.length) {
|
| 590 |
+
console.log(`[SESSION] All characters processed in chat ${chatId}. Ending session...`);
|
| 591 |
+
await endSession(chatId);
|
| 592 |
+
} else {
|
| 593 |
+
const nextChar = session.characters[session.currentCharacterIndex];
|
| 594 |
+
const delayMsg = await bot.sendMessage(
|
| 595 |
+
chatId,
|
| 596 |
+
`⏳ *Next Bid in 20s* — Get ready for *${nextChar.name}*!`,
|
| 597 |
+
{ parse_mode: "Markdown" }
|
| 598 |
+
);
|
| 599 |
+
trackMessage(chatId, delayMsg);
|
| 600 |
+
|
| 601 |
+
console.log(`[SESSION] Queueing next character "${nextChar.name}" in 20 seconds for chat ${chatId}`);
|
| 602 |
+
// 20 seconds delay before next character
|
| 603 |
+
setTimeout(async () => {
|
| 604 |
+
try {
|
| 605 |
+
await startNextCharacter(chatId);
|
| 606 |
+
} catch (err) {
|
| 607 |
+
console.error("❌ Error starting next character:", err);
|
| 608 |
+
}
|
| 609 |
+
}, NEXT_CHARACTER_DELAY_MS);
|
| 610 |
+
}
|
| 611 |
+
} catch (err) {
|
| 612 |
+
console.error("❌ Error resolving bid:", err);
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
// ─── Session flow ─────────────────────────────────────────────────────────────
|
| 617 |
+
async function startNextCharacter(chatId) {
|
| 618 |
+
try {
|
| 619 |
+
const session = getSession(chatId);
|
| 620 |
+
if (session.phase !== "bidding") return;
|
| 621 |
+
|
| 622 |
+
session.resolving = false; // Unlock session for new bids
|
| 623 |
+
|
| 624 |
+
const char = session.characters[session.currentCharacterIndex];
|
| 625 |
+
if (!char) {
|
| 626 |
+
console.warn("⚠️ No character found at index:", session.currentCharacterIndex);
|
| 627 |
+
await endSession(chatId);
|
| 628 |
+
return;
|
| 629 |
+
}
|
| 630 |
+
const total = session.characters.length;
|
| 631 |
+
const idx = session.currentCharacterIndex + 1;
|
| 632 |
+
|
| 633 |
+
console.log(`[SESSION] startNextCharacter: Character ${idx}/${total} ("${char.name}") in chat ${chatId}`);
|
| 634 |
+
|
| 635 |
+
const textCaption =
|
| 636 |
+
`*${char.name}*\n\n` +
|
| 637 |
+
(char.features || []).join("\n") +
|
| 638 |
+
`\n\n💵 *Base Price:* ${formatCoins(char.base_price)}\n` +
|
| 639 |
+
`📢 Minimum bid: ${formatCoins(char.base_price + 1)}\n\n` +
|
| 640 |
+
`_Send your bid amount (e.g. \`120\`) to place a bid!_`;
|
| 641 |
+
|
| 642 |
+
const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []);
|
| 643 |
+
|
| 644 |
+
if (imageUrls.length === 0) {
|
| 645 |
+
session.photoIndex = null;
|
| 646 |
+
session.currentCharacterImage = null;
|
| 647 |
+
try {
|
| 648 |
+
const textMsg = await bot.sendMessage(chatId, textCaption, { parse_mode: "Markdown" });
|
| 649 |
+
trackMessage(chatId, textMsg);
|
| 650 |
+
} catch (err) {
|
| 651 |
+
console.error("❌ Failed to send character text:", err);
|
| 652 |
+
}
|
| 653 |
+
} else {
|
| 654 |
+
const initialIndex = Math.floor(Math.random() * imageUrls.length);
|
| 655 |
+
session.photoIndex = initialIndex;
|
| 656 |
+
const selectedImage = imageUrls[initialIndex];
|
| 657 |
+
session.currentCharacterImage = selectedImage;
|
| 658 |
+
|
| 659 |
+
let fileId = selectedImage;
|
| 660 |
+
let extraCaption = "";
|
| 661 |
+
if (typeof selectedImage === "object" && selectedImage !== null) {
|
| 662 |
+
fileId = selectedImage.file_id;
|
| 663 |
+
if (selectedImage.caption) {
|
| 664 |
+
extraCaption = ` _${selectedImage.caption}_\n\n`;
|
| 665 |
+
}
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
const updatedCaption =
|
| 669 |
+
`*${char.name}*\n\n` +
|
| 670 |
+
extraCaption +
|
| 671 |
+
(char.features || []).join("\n") +
|
| 672 |
+
`\n\n💵 *Base Price:* ${formatCoins(char.base_price)}\n` +
|
| 673 |
+
`📢 Minimum bid: ${formatCoins(char.base_price + 1)}\n\n` +
|
| 674 |
+
`_Send your bid amount (e.g. \`120\`) to place a bid!_`;
|
| 675 |
+
|
| 676 |
+
try {
|
| 677 |
+
const photoMsg = await bot.sendPhoto(chatId, fileId, {
|
| 678 |
+
caption: updatedCaption,
|
| 679 |
+
parse_mode: "Markdown",
|
| 680 |
+
});
|
| 681 |
+
trackMessage(chatId, photoMsg);
|
| 682 |
+
} catch (err) {
|
| 683 |
+
console.warn(`[SESSION] Failed to send character photo for "${char.name}", sending text backup:`, err.message);
|
| 684 |
+
try {
|
| 685 |
+
const textMsg = await bot.sendMessage(chatId, `🖼️ [Image unavailable]\n\n${textCaption}`, {
|
| 686 |
+
parse_mode: "Markdown",
|
| 687 |
+
});
|
| 688 |
+
trackMessage(chatId, textMsg);
|
| 689 |
+
} catch (err) {
|
| 690 |
+
console.error("❌ Failed to send backup text message:", err);
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
session.currentHighestBid = null;
|
| 696 |
+
await startBidTimer(chatId);
|
| 697 |
+
} catch (err) {
|
| 698 |
+
console.error("❌ Error in startNextCharacter:", err);
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
async function endSession(chatId) {
|
| 703 |
+
try {
|
| 704 |
+
const session = getSession(chatId);
|
| 705 |
+
console.log(`[SESSION] endSession: Ending bidding session in chat: ${chatId}`);
|
| 706 |
+
clearBidTimer(session);
|
| 707 |
+
|
| 708 |
+
const db = loadDB();
|
| 709 |
+
|
| 710 |
+
// Build final scoreboard sorted by wins (descending) and tie-broken by amount spent (ascending)
|
| 711 |
+
const sortedParticipants = Object.entries(session.participants)
|
| 712 |
+
.sort((a, b) => {
|
| 713 |
+
const winsA = a[1].actressesWonCount || 0;
|
| 714 |
+
const winsB = b[1].actressesWonCount || 0;
|
| 715 |
+
if (winsB !== winsA) {
|
| 716 |
+
return winsB - winsA;
|
| 717 |
+
}
|
| 718 |
+
const spentA = a[1].amountSpent || 0;
|
| 719 |
+
const spentB = b[1].amountSpent || 0;
|
| 720 |
+
return spentA - spentB; // Option B: less spent wins the tie-breaker
|
| 721 |
+
});
|
| 722 |
+
|
| 723 |
+
const lines = sortedParticipants.map(([uid, p], i) => {
|
| 724 |
+
const medal = ["🥇", "🥈", "🥉"][i] || `${i + 1}.`;
|
| 725 |
+
const title = getPrestigeTitle(db, uid);
|
| 726 |
+
const titlePrefix = title ? `[${title}] ` : "";
|
| 727 |
+
return `${medal} *${titlePrefix}${p.username}* — ${p.actressesWonCount} wins (${formatCoins(p.amountSpent)} spent) | ${formatCoins(p.coins)} left`;
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
// Display the result first
|
| 731 |
+
await bot.sendMessage(
|
| 732 |
+
chatId,
|
| 733 |
+
`🏁 *Auction Session Complete!*\n\n` +
|
| 734 |
+
`📊 *Final Standings:*\n${lines.join("\n")}\n\n` +
|
| 735 |
+
`Type \`bid start\` to begin a new session!`,
|
| 736 |
+
{ parse_mode: "Markdown" }
|
| 737 |
+
);
|
| 738 |
+
|
| 739 |
+
// Delete all tracked messages to clear chat clutter after showing results
|
| 740 |
+
if (session.messagesToDelete && session.messagesToDelete.length > 0) {
|
| 741 |
+
console.log(`[SESSION] Cleaning up ${session.messagesToDelete.length} temporary messages from chat ${chatId}...`);
|
| 742 |
+
for (const msgId of session.messagesToDelete) {
|
| 743 |
+
try {
|
| 744 |
+
await bot.deleteMessage(chatId, msgId);
|
| 745 |
+
} catch (_) { }
|
| 746 |
+
}
|
| 747 |
+
session.messagesToDelete = [];
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
// Update member tags for top 5 users based on global wins
|
| 751 |
+
await updateGroupTagsForTopUsers(chatId);
|
| 752 |
+
} catch (err) {
|
| 753 |
+
console.error("❌ Error in endSession:", err);
|
| 754 |
+
} finally {
|
| 755 |
+
sessions[chatId] = { phase: "inactive" };
|
| 756 |
+
console.log(`[SESSION] Session state reset to inactive for chat: ${chatId}`);
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// ─── Command Handlers ─────────────────────────────────────────────────────────
|
| 761 |
+
|
| 762 |
+
// ─── Command Handlers ─────────────────────────────────────────────────────────
|
| 763 |
+
|
| 764 |
+
// BID START
|
| 765 |
+
bot.onText(/^bid start$/i, async (msg) => {
|
| 766 |
+
const chatId = msg.chat.id;
|
| 767 |
+
const userId = String(msg.from.id);
|
| 768 |
+
const username = msg.from.username
|
| 769 |
+
? `@${msg.from.username}`
|
| 770 |
+
: msg.from.first_name || `User${userId}`;
|
| 771 |
+
|
| 772 |
+
console.log(`[CMD] "bid start" invoked by ${username} (ID: ${userId}) in chat: ${chatId}`);
|
| 773 |
+
|
| 774 |
+
const session = getSession(chatId);
|
| 775 |
+
|
| 776 |
+
// Track user's bid start message
|
| 777 |
+
trackMessage(chatId, msg);
|
| 778 |
+
|
| 779 |
+
if (session.phase === "bidding") {
|
| 780 |
+
console.log(`[CMD] "bid start" ignored: bidding session already active in chat: ${chatId}`);
|
| 781 |
+
const activeMsg = await bot.sendMessage(chatId, "⚠️ A bidding session is already in progress!");
|
| 782 |
+
trackMessage(chatId, activeMsg);
|
| 783 |
+
return;
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
if (session.phase !== "waiting") {
|
| 787 |
+
session.phase = "waiting";
|
| 788 |
+
session.readyUsers = new Map(); // userId -> username
|
| 789 |
+
session.userSelections = {}; // userId -> Set of charIds
|
| 790 |
+
session.userSelectionsDone = {}; // userId -> boolean
|
| 791 |
+
|
| 792 |
+
// Start 5-minute lobby timer
|
| 793 |
+
if (session.lobbyTimer) clearTimeout(session.lobbyTimer);
|
| 794 |
+
session.lobbyTimer = setTimeout(async () => {
|
| 795 |
+
console.log(`[LOBBY] Lobby timeout expired for chat: ${chatId}`);
|
| 796 |
+
const currentSession = getSession(chatId);
|
| 797 |
+
if (currentSession && currentSession.phase === "waiting") {
|
| 798 |
+
if (currentSession.messagesToDelete && currentSession.messagesToDelete.length > 0) {
|
| 799 |
+
for (const msgId of currentSession.messagesToDelete) {
|
| 800 |
+
try {
|
| 801 |
+
await bot.deleteMessage(chatId, msgId);
|
| 802 |
+
} catch (_) { }
|
| 803 |
+
}
|
| 804 |
+
currentSession.messagesToDelete = [];
|
| 805 |
+
}
|
| 806 |
+
sessions[chatId] = { phase: "inactive" };
|
| 807 |
+
try {
|
| 808 |
+
await bot.sendMessage(
|
| 809 |
+
chatId,
|
| 810 |
+
`⏱️ *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.`,
|
| 811 |
+
{ parse_mode: "Markdown" }
|
| 812 |
+
);
|
| 813 |
+
} catch (_) {}
|
| 814 |
+
}
|
| 815 |
+
}, 5 * 60 * 1000);
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
if (session.readyUsers.has(userId)) {
|
| 819 |
+
console.log(`[CMD] "bid start" duplicate sign-up by ${username} in chat: ${chatId}`);
|
| 820 |
+
const dupMsg = await bot.sendMessage(chatId, `✅ ${username}, you already signed up for this session!`);
|
| 821 |
+
trackMessage(chatId, dupMsg);
|
| 822 |
+
return;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
session.readyUsers.set(userId, username);
|
| 826 |
+
session.userSelections[userId] = new Set();
|
| 827 |
+
session.userSelectionsDone[userId] = false;
|
| 828 |
+
|
| 829 |
+
// Send the character selection in the group chat, but customized for this user
|
| 830 |
+
const db = loadDB();
|
| 831 |
+
const allChars = getAllCharacters(db);
|
| 832 |
+
const selectionKeyboard = buildSelectionKeyboard(chatId, userId, allChars, new Set());
|
| 833 |
+
|
| 834 |
+
console.log(`[CMD] Sending selection menu to player ${username} (ID: ${userId}) in chat: ${chatId}`);
|
| 835 |
+
const selMsg = await bot.sendMessage(
|
| 836 |
+
chatId,
|
| 837 |
+
`🎮 *Character Selection for* ${username}\n` +
|
| 838 |
+
`Please select the characters you want to include in this session, then click *Done Selecting*!`,
|
| 839 |
+
{
|
| 840 |
+
parse_mode: "Markdown",
|
| 841 |
+
reply_markup: { inline_keyboard: selectionKeyboard }
|
| 842 |
+
}
|
| 843 |
+
);
|
| 844 |
+
trackMessage(chatId, selMsg);
|
| 845 |
+
|
| 846 |
+
const count = session.readyUsers.size;
|
| 847 |
+
const remaining = REQUIRED_PLAYERS - count;
|
| 848 |
+
|
| 849 |
+
if (remaining > 0) {
|
| 850 |
+
const countMsg = await bot.sendMessage(
|
| 851 |
+
chatId,
|
| 852 |
+
`✋ ${username} wants to bid!\n📋 *${count}/${REQUIRED_PLAYERS}* players ready.\n` +
|
| 853 |
+
`Need *${remaining}* more — type \`bid start\` to join!`,
|
| 854 |
+
{ parse_mode: "Markdown" }
|
| 855 |
+
);
|
| 856 |
+
trackMessage(chatId, countMsg);
|
| 857 |
+
} else {
|
| 858 |
+
const countMsg = await bot.sendMessage(
|
| 859 |
+
chatId,
|
| 860 |
+
`📋 *${count}/${REQUIRED_PLAYERS}* players ready.\n` +
|
| 861 |
+
`⏱️ Waiting for all players to complete their actress selections!`,
|
| 862 |
+
{ parse_mode: "Markdown" }
|
| 863 |
+
);
|
| 864 |
+
trackMessage(chatId, countMsg);
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
// Defer start until selections are finalized
|
| 868 |
+
await checkLobbyReadyAndStart(chatId);
|
| 869 |
+
});
|
| 870 |
+
|
| 871 |
+
// BID AMOUNT (plain number)
|
| 872 |
+
bot.on("message", async (msg) => {
|
| 873 |
+
if (!msg.text) return;
|
| 874 |
+
const chatId = msg.chat.id;
|
| 875 |
+
const userId = String(msg.from.id);
|
| 876 |
+
const username = msg.from.username
|
| 877 |
+
? `@${msg.from.username}`
|
| 878 |
+
: msg.from.first_name || `User${userId}`;
|
| 879 |
+
|
| 880 |
+
const session = getSession(chatId);
|
| 881 |
+
if (session.phase !== "bidding" || session.resolving) return;
|
| 882 |
+
|
| 883 |
+
// Skip commands
|
| 884 |
+
if (msg.text.startsWith("/") || /^bid /i.test(msg.text)) return;
|
| 885 |
+
|
| 886 |
+
const amount = parseInt(msg.text.trim(), 10);
|
| 887 |
+
if (isNaN(amount) || amount <= 0) return;
|
| 888 |
+
|
| 889 |
+
console.log(`[BID] Received bid request: user=${username} (ID: ${userId}), amount=${amount}, chat=${chatId}`);
|
| 890 |
+
|
| 891 |
+
// Only participants can bid
|
| 892 |
+
if (!session.participants[userId]) {
|
| 893 |
+
console.log(`[BID] Bid rejected: user ${username} is not a participant in this session.`);
|
| 894 |
+
const err = await bot.sendMessage(
|
| 895 |
+
chatId,
|
| 896 |
+
`⛔ ${username}, you didn't join this session. Wait for the next one and type \`bid start\`!`,
|
| 897 |
+
{ parse_mode: "Markdown" }
|
| 898 |
+
);
|
| 899 |
+
deleteAfterDelay(chatId, msg.message_id);
|
| 900 |
+
deleteAfterDelay(chatId, err.message_id);
|
| 901 |
+
return;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
const participant = session.participants[userId];
|
| 905 |
+
const char = session.characters[session.currentCharacterIndex];
|
| 906 |
+
const highBid = session.currentHighestBid;
|
| 907 |
+
|
| 908 |
+
// Must exceed base price
|
| 909 |
+
if (amount <= char.base_price) {
|
| 910 |
+
console.log(`[BID] Bid rejected: amount ${amount} <= base price ${char.base_price} for character: ${char.name}`);
|
| 911 |
+
const err = await bot.sendMessage(
|
| 912 |
+
chatId,
|
| 913 |
+
`❌ Minimum bid is *${char.base_price + 1} coins* (base price: ${char.base_price})`,
|
| 914 |
+
{ parse_mode: "Markdown" }
|
| 915 |
+
);
|
| 916 |
+
deleteAfterDelay(chatId, msg.message_id);
|
| 917 |
+
deleteAfterDelay(chatId, err.message_id);
|
| 918 |
+
return;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
// Must be higher than current highest bid
|
| 922 |
+
if (highBid && amount <= highBid.amount) {
|
| 923 |
+
console.log(`[BID] Bid rejected: amount ${amount} <= current highest bid ${highBid.amount}`);
|
| 924 |
+
const err = await bot.sendMessage(
|
| 925 |
+
chatId,
|
| 926 |
+
`❌ You must bid higher than the current bid of *${highBid.amount} coins*!`,
|
| 927 |
+
{ parse_mode: "Markdown" }
|
| 928 |
+
);
|
| 929 |
+
deleteAfterDelay(chatId, msg.message_id);
|
| 930 |
+
deleteAfterDelay(chatId, err.message_id);
|
| 931 |
+
return;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
// Same person can't outbid themselves
|
| 935 |
+
if (highBid && highBid.userId === userId) {
|
| 936 |
+
console.log(`[BID] Bid rejected: user ${username} is already the highest bidder.`);
|
| 937 |
+
const err = await bot.sendMessage(chatId, `⚠️ You are already the highest bidder!`);
|
| 938 |
+
deleteAfterDelay(chatId, msg.message_id);
|
| 939 |
+
deleteAfterDelay(chatId, err.message_id);
|
| 940 |
+
return;
|
| 941 |
+
}
|
| 942 |
+
|
| 943 |
+
// Must have enough coins
|
| 944 |
+
if (amount > participant.coins) {
|
| 945 |
+
console.log(`[BID] Bid rejected: amount ${amount} > user coins balance ${participant.coins}`);
|
| 946 |
+
const err = await bot.sendMessage(
|
| 947 |
+
chatId,
|
| 948 |
+
`❌ You only have *${participant.coins} coins*! You can't bid *${amount}*.`,
|
| 949 |
+
{ parse_mode: "Markdown" }
|
| 950 |
+
);
|
| 951 |
+
deleteAfterDelay(chatId, msg.message_id);
|
| 952 |
+
deleteAfterDelay(chatId, err.message_id);
|
| 953 |
+
return;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
// Valid bid!
|
| 957 |
+
console.log(`[BID] Bid accepted: user ${username} is now highest bidder with ${amount} coins.`);
|
| 958 |
+
const db = loadDB();
|
| 959 |
+
const title = getPrestigeTitle(db, userId);
|
| 960 |
+
session.currentHighestBid = { userId, username, amount, title };
|
| 961 |
+
|
| 962 |
+
// Track the user's incoming valid bid message for final session cleanup
|
| 963 |
+
trackMessage(chatId, msg);
|
| 964 |
+
|
| 965 |
+
// Reset timer (sends the combined countdown/highest bid message)
|
| 966 |
+
await startBidTimer(chatId);
|
| 967 |
+
});
|
| 968 |
+
|
| 969 |
+
// /leaderboard
|
| 970 |
+
bot.onText(/\/leaderboard/, async (msg) => {
|
| 971 |
+
const chatId = msg.chat.id;
|
| 972 |
+
const username = msg.from.username || msg.from.first_name;
|
| 973 |
+
console.log(`[CMD] "/leaderboard" requested by ${username} in chat: ${chatId}`);
|
| 974 |
+
|
| 975 |
+
const db = loadDB();
|
| 976 |
+
const users = Object.entries(db.users);
|
| 977 |
+
|
| 978 |
+
if (users.length === 0) {
|
| 979 |
+
return bot.sendMessage(msg.chat.id, "📊 No data yet! Start a session with `bid start`", {
|
| 980 |
+
parse_mode: "Markdown",
|
| 981 |
+
});
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
const sorted = users.sort((a, b) => b[1].totalWins - a[1].totalWins).slice(0, 10);
|
| 985 |
+
|
| 986 |
+
const rows = sorted.map(([uid, u], i) => {
|
| 987 |
+
const rankStr = ["1st", "2nd", "3rd"][i] || `${i + 1}th`;
|
| 988 |
+
const title = getPrestigeTitle(db, uid);
|
| 989 |
+
const playerStr = title ? `${u.username} (${title})` : u.username;
|
| 990 |
+
const winsStr = String(u.totalWins);
|
| 991 |
+
const coinsStr = String(u.coins || 0);
|
| 992 |
+
return [rankStr, playerStr, winsStr, coinsStr];
|
| 993 |
+
});
|
| 994 |
+
|
| 995 |
+
const tableText = generateTextTable(
|
| 996 |
+
['R', 'Player', 'W', 'Coins'],
|
| 997 |
+
[3, 17, 2, 5],
|
| 998 |
+
['left', 'left', 'right', 'right'],
|
| 999 |
+
rows
|
| 1000 |
+
);
|
| 1001 |
+
|
| 1002 |
+
await bot.sendMessage(
|
| 1003 |
+
msg.chat.id,
|
| 1004 |
+
`🏆 *All-Time Leaderboard*\n\n${tableText}`,
|
| 1005 |
+
{ parse_mode: "Markdown" }
|
| 1006 |
+
);
|
| 1007 |
+
});
|
| 1008 |
+
|
| 1009 |
+
// /mystats
|
| 1010 |
+
bot.onText(/\/mystats/, async (msg) => {
|
| 1011 |
+
const chatId = msg.chat.id;
|
| 1012 |
+
const userId = String(msg.from.id);
|
| 1013 |
+
const username = msg.from.username || msg.from.first_name;
|
| 1014 |
+
console.log(`[CMD] "/mystats" requested by ${username} (ID: ${userId}) in chat: ${chatId}`);
|
| 1015 |
+
|
| 1016 |
+
const db = loadDB();
|
| 1017 |
+
const user = db.users[userId];
|
| 1018 |
+
|
| 1019 |
+
if (!user) {
|
| 1020 |
+
return bot.sendMessage(msg.chat.id, "You haven't participated in any sessions yet!");
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
let charList = "_None yet_";
|
| 1024 |
+
if (user.characters && user.characters.length > 0) {
|
| 1025 |
+
const grouped = {};
|
| 1026 |
+
for (const c of user.characters) {
|
| 1027 |
+
const key = c.characterId || c.characterName;
|
| 1028 |
+
if (!grouped[key]) {
|
| 1029 |
+
grouped[key] = { name: c.characterName, count: 0, totalSpent: 0 };
|
| 1030 |
+
}
|
| 1031 |
+
grouped[key].count += 1;
|
| 1032 |
+
grouped[key].totalSpent += c.paidAmount;
|
| 1033 |
+
}
|
| 1034 |
+
charList = Object.values(grouped)
|
| 1035 |
+
.map((gc) => `• ${gc.name} (x${gc.count}) (Total Spent: ${gc.totalSpent}$)`)
|
| 1036 |
+
.join("\n");
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
const title = getPrestigeTitle(db, userId);
|
| 1040 |
+
const titleStr = title ? title : "None";
|
| 1041 |
+
|
| 1042 |
+
const statsMessage =
|
| 1043 |
+
`👤 *YOUR PROFILE STATS* 👤\n` +
|
| 1044 |
+
`━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
| 1045 |
+
`🎖️ *Prestige Title:* _${titleStr}_\n` +
|
| 1046 |
+
`🏆 *Total Wins:* \`${user.totalWins}\`\n` +
|
| 1047 |
+
`💰 *Wallet Balance:* \`${user.coins || 0} coins\`\n\n` +
|
| 1048 |
+
`🎴 *Actresses Owned:*\n${charList}\n` +
|
| 1049 |
+
`━━━━━━━━━━━━━━━━━━━━━━━━`;
|
| 1050 |
+
|
| 1051 |
+
await bot.sendMessage(
|
| 1052 |
+
msg.chat.id,
|
| 1053 |
+
statsMessage,
|
| 1054 |
+
{ parse_mode: "Markdown" }
|
| 1055 |
+
);
|
| 1056 |
+
});
|
| 1057 |
+
|
| 1058 |
+
// /daily
|
| 1059 |
+
bot.onText(/\/daily/, async (msg) => {
|
| 1060 |
+
const chatId = msg.chat.id;
|
| 1061 |
+
const userId = String(msg.from.id);
|
| 1062 |
+
const username = msg.from.username
|
| 1063 |
+
? `@${msg.from.username}`
|
| 1064 |
+
: msg.from.first_name || `User${userId}`;
|
| 1065 |
+
|
| 1066 |
+
console.log(`[CMD] "/daily" requested by ${username} (ID: ${userId}) in chat: ${chatId}`);
|
| 1067 |
+
|
| 1068 |
+
const db = loadDB();
|
| 1069 |
+
const user = getUser(db, userId, username);
|
| 1070 |
+
|
| 1071 |
+
const now = Date.now();
|
| 1072 |
+
const lastDaily = user.lastDaily ? new Date(user.lastDaily).getTime() : 0;
|
| 1073 |
+
const cooldown = 24 * 60 * 60 * 1000; // 24 hours
|
| 1074 |
+
|
| 1075 |
+
if (now - lastDaily < cooldown) {
|
| 1076 |
+
const timeLeft = cooldown - (now - lastDaily);
|
| 1077 |
+
const hours = Math.floor(timeLeft / (3600 * 1000));
|
| 1078 |
+
const minutes = Math.floor((timeLeft % (3600 * 1000)) / (60 * 1000));
|
| 1079 |
+
const seconds = Math.floor((timeLeft % (60 * 1000)) / 1000);
|
| 1080 |
+
console.log(`[CMD] "/daily" claim rejected: cooldown active for user ${username}. Time left: ${hours}h ${minutes}m ${seconds}s`);
|
| 1081 |
+
return bot.sendMessage(
|
| 1082 |
+
msg.chat.id,
|
| 1083 |
+
`⏳ You have already claimed your daily allowance!\n` +
|
| 1084 |
+
`Please wait *${hours}h ${minutes}m ${seconds}s* before claiming again.`,
|
| 1085 |
+
{ parse_mode: "Markdown" }
|
| 1086 |
+
);
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
user.coins = (user.coins || 0) + 10;
|
| 1090 |
+
user.lastDaily = new Date().toISOString();
|
| 1091 |
+
saveDB(db);
|
| 1092 |
+
|
| 1093 |
+
console.log(`[CMD] "/daily" claim successful for user ${username}. Added 10 coins. New balance: ${user.coins}`);
|
| 1094 |
+
await bot.sendMessage(
|
| 1095 |
+
msg.chat.id,
|
| 1096 |
+
`💰 *Daily Allowance Claimed!*\n\n` +
|
| 1097 |
+
`Added: *10 coins*\n` +
|
| 1098 |
+
`Total Wallet Balance: *${user.coins} coins*`,
|
| 1099 |
+
{ parse_mode: "Markdown" }
|
| 1100 |
+
);
|
| 1101 |
+
});
|
| 1102 |
+
|
| 1103 |
+
// /owners
|
| 1104 |
+
bot.onText(/\/owners/, async (msg) => {
|
| 1105 |
+
const chatId = msg.chat.id;
|
| 1106 |
+
const username = msg.from.username || msg.from.first_name;
|
| 1107 |
+
console.log(`[CMD] "/owners" requested by ${username} in chat: ${chatId}`);
|
| 1108 |
+
|
| 1109 |
+
const db = loadDB();
|
| 1110 |
+
const allChars = getAllCharacters(db);
|
| 1111 |
+
|
| 1112 |
+
// Map of charId -> list of wins
|
| 1113 |
+
const winsMap = {};
|
| 1114 |
+
for (const [userId, user] of Object.entries(db.users)) {
|
| 1115 |
+
const uName = user.username || `User${userId}`;
|
| 1116 |
+
const charsWon = user.characters || [];
|
| 1117 |
+
for (const won of charsWon) {
|
| 1118 |
+
if (!winsMap[won.characterId]) {
|
| 1119 |
+
winsMap[won.characterId] = [];
|
| 1120 |
+
}
|
| 1121 |
+
winsMap[won.characterId].push({
|
| 1122 |
+
userId,
|
| 1123 |
+
username: uName,
|
| 1124 |
+
paidAmount: won.paidAmount || 0,
|
| 1125 |
+
wonAt: won.wonAt || ""
|
| 1126 |
+
});
|
| 1127 |
+
}
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
const rows = [];
|
| 1131 |
+
for (const char of allChars) {
|
| 1132 |
+
const wins = winsMap[char.id];
|
| 1133 |
+
if (!wins || wins.length === 0) {
|
| 1134 |
+
rows.push([char.name, 'No owner', '0']);
|
| 1135 |
+
continue;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
// Aggregate wins by user
|
| 1139 |
+
const counts = {};
|
| 1140 |
+
for (const win of wins) {
|
| 1141 |
+
if (!counts[win.userId]) {
|
| 1142 |
+
counts[win.userId] = { count: 0, maxBid: 0, username: win.username };
|
| 1143 |
+
}
|
| 1144 |
+
counts[win.userId].count += 1;
|
| 1145 |
+
if (win.paidAmount > counts[win.userId].maxBid) {
|
| 1146 |
+
counts[win.userId].maxBid = win.paidAmount;
|
| 1147 |
+
}
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
// Sort by win count, then highest paid amount
|
| 1151 |
+
const sorted = Object.entries(counts).sort((a, b) => {
|
| 1152 |
+
if (b[1].count !== a[1].count) {
|
| 1153 |
+
return b[1].count - a[1].count;
|
| 1154 |
+
}
|
| 1155 |
+
return b[1].maxBid - a[1].maxBid;
|
| 1156 |
+
});
|
| 1157 |
+
|
| 1158 |
+
const [, ownerData] = sorted[0];
|
| 1159 |
+
rows.push([char.name, ownerData.username, String(ownerData.count)]);
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
const tableText = generateTextTable(
|
| 1163 |
+
['Character', 'Owner', 'Wins'],
|
| 1164 |
+
[13, 14, 4],
|
| 1165 |
+
['left', 'left', 'right'],
|
| 1166 |
+
rows
|
| 1167 |
+
);
|
| 1168 |
+
|
| 1169 |
+
await bot.sendMessage(
|
| 1170 |
+
msg.chat.id,
|
| 1171 |
+
`🎴 *Character Ownership* 🏆\n\n${tableText}`,
|
| 1172 |
+
{ parse_mode: "Markdown" }
|
| 1173 |
+
);
|
| 1174 |
+
});
|
| 1175 |
+
|
| 1176 |
+
// /addactress (admin only)
|
| 1177 |
+
bot.onText(/\/addactress|\/addcharacter/, async (msg) => {
|
| 1178 |
+
const chatId = msg.chat.id;
|
| 1179 |
+
const userId = String(msg.from.id);
|
| 1180 |
+
console.log(`[CMD] "/addactress" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1181 |
+
|
| 1182 |
+
if (!isAdmin(userId)) {
|
| 1183 |
+
console.log(`[CMD] Permission denied for "/addactress" by user: ${userId}`);
|
| 1184 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can add actresses.");
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
await bot.sendMessage(
|
| 1188 |
+
msg.chat.id,
|
| 1189 |
+
`📝 *Add an Actress* (Admin)\n\n` +
|
| 1190 |
+
`Choose one of the two methods:\n\n` +
|
| 1191 |
+
`*Method 1: Send with a Photo (Simpler & Recommended)*\n` +
|
| 1192 |
+
`Send a photo to the bot with a caption in this simple format:\n` +
|
| 1193 |
+
`\`\`\`\n` +
|
| 1194 |
+
`/newactress Actress Name\n` +
|
| 1195 |
+
`Base Price\n` +
|
| 1196 |
+
`Feature 1\n` +
|
| 1197 |
+
`Feature 2\n` +
|
| 1198 |
+
`\`\`\`\n\n` +
|
| 1199 |
+
`*Method 2: Add by Text (No Image Required)*\n` +
|
| 1200 |
+
`Send this text command:\n` +
|
| 1201 |
+
`\`/submitactress NAME , BASE_PRICE , Feature 1 , Feature 2 , Feature 3\``,
|
| 1202 |
+
{ parse_mode: "Markdown" }
|
| 1203 |
+
);
|
| 1204 |
+
});
|
| 1205 |
+
|
| 1206 |
+
// /submitactress (admin only)
|
| 1207 |
+
bot.onText(/(?:\/submitactress|\/submitchar)\s+(.+)/, async (msg, match) => {
|
| 1208 |
+
const chatId = msg.chat.id;
|
| 1209 |
+
const userId = String(msg.from.id);
|
| 1210 |
+
console.log(`[CMD] "/submitactress" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1211 |
+
|
| 1212 |
+
if (!isAdmin(userId)) {
|
| 1213 |
+
console.log(`[CMD] Permission denied for "/submitactress" by user: ${userId}`);
|
| 1214 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can add actresses.");
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
const parts = match[1].split(",").map((s) => s.trim());
|
| 1218 |
+
if (parts.length < 2) {
|
| 1219 |
+
return bot.sendMessage(
|
| 1220 |
+
msg.chat.id,
|
| 1221 |
+
"❌ Format: `/submitactress NAME , BASE_PRICE , Feature1 , Feature2 ...`",
|
| 1222 |
+
{ parse_mode: "Markdown" }
|
| 1223 |
+
);
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
const [name, basePriceStr, ...features] = parts;
|
| 1227 |
+
const basePrice = parseInt(basePriceStr, 10);
|
| 1228 |
+
|
| 1229 |
+
if (!name || isNaN(basePrice)) {
|
| 1230 |
+
return bot.sendMessage(msg.chat.id, "❌ Invalid format. Check NAME and BASE_PRICE.");
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
console.log(`[CMD] Admin ${userId} adding actress via text: name="${name}", price=${basePrice}`);
|
| 1234 |
+
|
| 1235 |
+
const db = loadDB();
|
| 1236 |
+
const newChar = {
|
| 1237 |
+
id: `custom_${Date.now()}`,
|
| 1238 |
+
name,
|
| 1239 |
+
image_url: [], // Text additions start with no images/skins
|
| 1240 |
+
base_price: basePrice,
|
| 1241 |
+
features,
|
| 1242 |
+
addedBy: userId,
|
| 1243 |
+
};
|
| 1244 |
+
|
| 1245 |
+
db.customCharacters = db.customCharacters || [];
|
| 1246 |
+
db.customCharacters.push(newChar);
|
| 1247 |
+
saveDB(db);
|
| 1248 |
+
|
| 1249 |
+
await bot.sendMessage(
|
| 1250 |
+
msg.chat.id,
|
| 1251 |
+
`✅ *${name}* added to the actress pool!\n💵 Base price: ${basePrice} coins`,
|
| 1252 |
+
{ parse_mode: "Markdown" }
|
| 1253 |
+
);
|
| 1254 |
+
});
|
| 1255 |
+
|
| 1256 |
+
// /newactress (admin only, sent as a photo caption)
|
| 1257 |
+
bot.on("photo", async (msg) => {
|
| 1258 |
+
const chatId = msg.chat.id;
|
| 1259 |
+
const userId = String(msg.from.id);
|
| 1260 |
+
|
| 1261 |
+
const caption = msg.caption;
|
| 1262 |
+
const isNewActress = caption && caption.toLowerCase().startsWith("/newactress");
|
| 1263 |
+
const isNewChar = caption && caption.toLowerCase().startsWith("/newchar");
|
| 1264 |
+
if (!isNewActress && !isNewChar) return;
|
| 1265 |
+
|
| 1266 |
+
console.log(`[CMD] "/newactress" photo flow invoked by user ID: ${userId} in chat: ${chatId}`);
|
| 1267 |
+
|
| 1268 |
+
if (!isAdmin(userId)) {
|
| 1269 |
+
console.log(`[CMD] Permission denied for "/newactress" by user: ${userId}`);
|
| 1270 |
+
return bot.sendMessage(chatId, "⛔ Only admins can add actresses.");
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
// Split by newline and clean empty lines
|
| 1274 |
+
const lines = caption
|
| 1275 |
+
.split("\n")
|
| 1276 |
+
.map((s) => s.trim())
|
| 1277 |
+
.filter(Boolean);
|
| 1278 |
+
|
| 1279 |
+
// Expected:
|
| 1280 |
+
// Line 1: /newactress Actress Name
|
| 1281 |
+
// Line 2: Base Price
|
| 1282 |
+
// Line 3+: Features
|
| 1283 |
+
if (lines.length < 2) {
|
| 1284 |
+
return bot.sendMessage(
|
| 1285 |
+
chatId,
|
| 1286 |
+
"❌ *Invalid format!*\n\n" +
|
| 1287 |
+
"Please send a photo with a caption using this simple format:\n\n" +
|
| 1288 |
+
"`/newactress Actress Name`\n" +
|
| 1289 |
+
"`Base Price`\n" +
|
| 1290 |
+
"`Feature 1`\n" +
|
| 1291 |
+
"`Feature 2`\n" +
|
| 1292 |
+
"`...`",
|
| 1293 |
+
{ parse_mode: "Markdown" }
|
| 1294 |
+
);
|
| 1295 |
+
}
|
| 1296 |
+
|
| 1297 |
+
const commandPrefix = isNewActress ? /^\/newactress\s*/i : /^\/newchar\s*/i;
|
| 1298 |
+
const name = lines[0].replace(commandPrefix, "").trim();
|
| 1299 |
+
const basePrice = parseInt(lines[1], 10);
|
| 1300 |
+
const features = lines.slice(2);
|
| 1301 |
+
|
| 1302 |
+
if (!name || isNaN(basePrice) || basePrice <= 0) {
|
| 1303 |
+
return bot.sendMessage(
|
| 1304 |
+
chatId,
|
| 1305 |
+
"❌ *Parsing Error!*\n\n" +
|
| 1306 |
+
"Make sure the first line has the actress name and the second line is a valid number for the base price.",
|
| 1307 |
+
{ parse_mode: "Markdown" }
|
| 1308 |
+
);
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
console.log(`[CMD] Admin ${userId} parsed new actress from photo caption: name="${name}", price=${basePrice}`);
|
| 1312 |
+
|
| 1313 |
+
const photo = msg.photo;
|
| 1314 |
+
const largestPhoto = photo[photo.length - 1];
|
| 1315 |
+
const fileId = largestPhoto.file_id;
|
| 1316 |
+
|
| 1317 |
+
try {
|
| 1318 |
+
const db = loadDB();
|
| 1319 |
+
const newChar = {
|
| 1320 |
+
id: `custom_${Date.now()}`,
|
| 1321 |
+
name,
|
| 1322 |
+
image_url: fileId, // Store Telegram file_id as image_url
|
| 1323 |
+
base_price: basePrice,
|
| 1324 |
+
features,
|
| 1325 |
+
addedBy: userId,
|
| 1326 |
+
};
|
| 1327 |
+
|
| 1328 |
+
db.customCharacters = db.customCharacters || [];
|
| 1329 |
+
db.customCharacters.push(newChar);
|
| 1330 |
+
saveDB(db);
|
| 1331 |
+
|
| 1332 |
+
await bot.sendMessage(
|
| 1333 |
+
chatId,
|
| 1334 |
+
`✅ *${name}* added to the actress pool!\n` +
|
| 1335 |
+
`💵 Base price: *${formatCoins(basePrice)}*\n` +
|
| 1336 |
+
`📝 Features recorded: \`${features.length}\``,
|
| 1337 |
+
{ parse_mode: "Markdown" }
|
| 1338 |
+
);
|
| 1339 |
+
} catch (err) {
|
| 1340 |
+
console.error("❌ Error adding actress:", err);
|
| 1341 |
+
await bot.sendMessage(chatId, "❌ Failed to add actress due to an internal error.");
|
| 1342 |
+
}
|
| 1343 |
+
});
|
| 1344 |
+
|
| 1345 |
+
// /listactress / /listactresses
|
| 1346 |
+
bot.onText(/\/listactress(?:es)?/, async (msg) => {
|
| 1347 |
+
const chatId = msg.chat.id;
|
| 1348 |
+
console.log(`[CMD] "/listactress" requested in chat: ${chatId}`);
|
| 1349 |
+
|
| 1350 |
+
const db = loadDB();
|
| 1351 |
+
const all = getAllCharacters(db);
|
| 1352 |
+
|
| 1353 |
+
const lines = all.map(
|
| 1354 |
+
(c, i) => `${i + 1}. *${c.name}* `
|
| 1355 |
+
);
|
| 1356 |
+
|
| 1357 |
+
await bot.sendMessage(
|
| 1358 |
+
msg.chat.id,
|
| 1359 |
+
`🎴 *Available Actresses (${all.length}):*\n\n${lines.join("\n")}`,
|
| 1360 |
+
{ parse_mode: "Markdown" }
|
| 1361 |
+
);
|
| 1362 |
+
});
|
| 1363 |
+
|
| 1364 |
+
// /actressimages [id] (admin only)
|
| 1365 |
+
bot.onText(/(?:\/actressimages|\/charimages)\s+(.+)/, async (msg, match) => {
|
| 1366 |
+
const chatId = msg.chat.id;
|
| 1367 |
+
const userId = String(msg.from.id);
|
| 1368 |
+
const targetId = match[1].trim();
|
| 1369 |
+
console.log(`[CMD] "/actressimages" invoked by admin ID: ${userId} for actress: ${targetId} in chat: ${chatId}`);
|
| 1370 |
+
|
| 1371 |
+
if (!isAdmin(userId)) {
|
| 1372 |
+
console.log(`[CMD] Permission denied for "/actressimages" by user: ${userId}`);
|
| 1373 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
const db = loadDB();
|
| 1377 |
+
const allChars = getAllCharacters(db);
|
| 1378 |
+
const char = allChars.find((c) => c.id === targetId);
|
| 1379 |
+
|
| 1380 |
+
if (!char) {
|
| 1381 |
+
return bot.sendMessage(chatId, `❌ Actress with ID \`${targetId}\` not found.`);
|
| 1382 |
+
}
|
| 1383 |
+
|
| 1384 |
+
const imageUrls = Array.isArray(char.image_url) ? char.image_url : (char.image_url ? [char.image_url] : []);
|
| 1385 |
+
if (imageUrls.length === 0) {
|
| 1386 |
+
return bot.sendMessage(chatId, `ℹ️ *${char.name}* (\`${targetId}\`) has no images/skins attached yet.`, { parse_mode: "Markdown" });
|
| 1387 |
+
}
|
| 1388 |
+
|
| 1389 |
+
const media = imageUrls.map((img) => {
|
| 1390 |
+
let mediaId = img;
|
| 1391 |
+
let caption = "";
|
| 1392 |
+
if (typeof img === "object" && img !== null) {
|
| 1393 |
+
mediaId = img.file_id;
|
| 1394 |
+
caption = img.caption || "";
|
| 1395 |
+
}
|
| 1396 |
+
return {
|
| 1397 |
+
type: "photo",
|
| 1398 |
+
media: mediaId,
|
| 1399 |
+
caption: caption || undefined
|
| 1400 |
+
};
|
| 1401 |
+
});
|
| 1402 |
+
|
| 1403 |
+
await bot.sendMessage(chatId, `🖼️ Sending *${imageUrls.length}* images/skins for *${char.name}*...`, { parse_mode: "Markdown" });
|
| 1404 |
+
|
| 1405 |
+
for (let i = 0; i < media.length; i += 10) {
|
| 1406 |
+
const chunk = media.slice(i, i + 10);
|
| 1407 |
+
try {
|
| 1408 |
+
await bot.sendMediaGroup(chatId, chunk);
|
| 1409 |
+
} catch (err) {
|
| 1410 |
+
console.error("❌ Failed to send media group chunk:", err.message || err);
|
| 1411 |
+
for (const item of chunk) {
|
| 1412 |
+
try {
|
| 1413 |
+
await bot.sendPhoto(chatId, item.media, { caption: item.caption });
|
| 1414 |
+
} catch (photoErr) {
|
| 1415 |
+
console.warn(`❌ Failed to send photo fallback:`, photoErr.message);
|
| 1416 |
+
}
|
| 1417 |
+
}
|
| 1418 |
+
}
|
| 1419 |
+
}
|
| 1420 |
+
});
|
| 1421 |
+
|
| 1422 |
+
// /cancelsession (admin only)
|
| 1423 |
+
bot.onText(/\/cancelsession/, async (msg) => {
|
| 1424 |
+
const chatId = msg.chat.id;
|
| 1425 |
+
const userId = String(msg.from.id);
|
| 1426 |
+
console.log(`[CMD] "/cancelsession" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1427 |
+
|
| 1428 |
+
if (!isAdmin(userId)) {
|
| 1429 |
+
console.log(`[CMD] Permission denied for "/cancelsession" by user: ${userId}`);
|
| 1430 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can cancel sessions.");
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
let targetChatId = msg.chat.id;
|
| 1434 |
+
|
| 1435 |
+
if (msg.chat.type === "private") {
|
| 1436 |
+
const db = loadDB();
|
| 1437 |
+
targetChatId = (db.connections || {})[userId];
|
| 1438 |
+
if (!targetChatId) {
|
| 1439 |
+
console.log(`[CMD] "/cancelsession" failed: admin ${userId} has no connected group chat.`);
|
| 1440 |
+
return bot.sendMessage(
|
| 1441 |
+
msg.chat.id,
|
| 1442 |
+
"��️ You haven't connected any group chat yet. Use `/connect` in the group chat first.",
|
| 1443 |
+
{ parse_mode: "Markdown" }
|
| 1444 |
+
);
|
| 1445 |
+
}
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
console.log(`[CMD] Cancelling session in chat ${targetChatId} by admin request.`);
|
| 1449 |
+
const session = getSession(targetChatId);
|
| 1450 |
+
clearBidTimer(session);
|
| 1451 |
+
if (session.lobbyTimer) {
|
| 1452 |
+
clearTimeout(session.lobbyTimer);
|
| 1453 |
+
session.lobbyTimer = null;
|
| 1454 |
+
}
|
| 1455 |
+
sessions[targetChatId] = { phase: "inactive" };
|
| 1456 |
+
|
| 1457 |
+
// Notify the group chat
|
| 1458 |
+
try {
|
| 1459 |
+
await bot.sendMessage(targetChatId, "🛑 Auction session cancelled by admin.");
|
| 1460 |
+
} catch (_) { }
|
| 1461 |
+
|
| 1462 |
+
// Confirm to the admin (if in private DM)
|
| 1463 |
+
if (msg.chat.type === "private") {
|
| 1464 |
+
await bot.sendMessage(msg.chat.id, `✅ Cancelled auction session in connected group (\`${targetChatId}\`).`);
|
| 1465 |
+
}
|
| 1466 |
+
});
|
| 1467 |
+
|
| 1468 |
+
// /resetbalance [user]
|
| 1469 |
+
bot.onText(/\/resetbalance\s+(.+)/, async (msg, match) => {
|
| 1470 |
+
const chatId = msg.chat.id;
|
| 1471 |
+
const userId = String(msg.from.id);
|
| 1472 |
+
const targetInput = match[1].trim();
|
| 1473 |
+
console.log(`[CMD] "/resetbalance" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`);
|
| 1474 |
+
|
| 1475 |
+
if (!isAdmin(userId)) {
|
| 1476 |
+
console.log(`[CMD] Permission denied for "/resetbalance" by user: ${userId}`);
|
| 1477 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
const db = loadDB();
|
| 1481 |
+
const res = findUserByIdOrUsername(db, targetInput);
|
| 1482 |
+
if (!res) {
|
| 1483 |
+
console.log(`[CMD] "/resetbalance" failed: target user "${targetInput}" not found.`);
|
| 1484 |
+
return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`);
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
res.user.coins = 0;
|
| 1488 |
+
saveDB(db);
|
| 1489 |
+
|
| 1490 |
+
console.log(`[ADMIN] Admin ${userId} reset balance of user ${res.userId} (${res.user.username})`);
|
| 1491 |
+
await bot.sendMessage(chatId, `✅ Reset wallet balance of *${res.user.username}* to 0 coins.`, { parse_mode: "Markdown" });
|
| 1492 |
+
});
|
| 1493 |
+
|
| 1494 |
+
// /resetwins [user]
|
| 1495 |
+
bot.onText(/\/resetwins\s+(.+)/, async (msg, match) => {
|
| 1496 |
+
const chatId = msg.chat.id;
|
| 1497 |
+
const userId = String(msg.from.id);
|
| 1498 |
+
const targetInput = match[1].trim();
|
| 1499 |
+
console.log(`[CMD] "/resetwins" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`);
|
| 1500 |
+
|
| 1501 |
+
if (!isAdmin(userId)) {
|
| 1502 |
+
console.log(`[CMD] Permission denied for "/resetwins" by user: ${userId}`);
|
| 1503 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
const db = loadDB();
|
| 1507 |
+
const res = findUserByIdOrUsername(db, targetInput);
|
| 1508 |
+
if (!res) {
|
| 1509 |
+
console.log(`[CMD] "/resetwins" failed: target user "${targetInput}" not found.`);
|
| 1510 |
+
return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`);
|
| 1511 |
+
}
|
| 1512 |
+
|
| 1513 |
+
res.user.totalWins = 0;
|
| 1514 |
+
saveDB(db);
|
| 1515 |
+
|
| 1516 |
+
console.log(`[ADMIN] Admin ${userId} reset wins of user ${res.userId} (${res.user.username})`);
|
| 1517 |
+
await bot.sendMessage(chatId, `✅ Reset total win count of *${res.user.username}* to 0.`, { parse_mode: "Markdown" });
|
| 1518 |
+
});
|
| 1519 |
+
|
| 1520 |
+
// /resetactresses [user]
|
| 1521 |
+
bot.onText(/(?:\/resetactresses|\/resetchars)\s+(.+)/, async (msg, match) => {
|
| 1522 |
+
const chatId = msg.chat.id;
|
| 1523 |
+
const userId = String(msg.from.id);
|
| 1524 |
+
const targetInput = match[1].trim();
|
| 1525 |
+
console.log(`[CMD] "/resetactresses" invoked by admin ID: ${userId} for target: ${targetInput} in chat: ${chatId}`);
|
| 1526 |
+
|
| 1527 |
+
if (!isAdmin(userId)) {
|
| 1528 |
+
console.log(`[CMD] Permission denied for "/resetactresses" by user: ${userId}`);
|
| 1529 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1530 |
+
}
|
| 1531 |
+
|
| 1532 |
+
const db = loadDB();
|
| 1533 |
+
const res = findUserByIdOrUsername(db, targetInput);
|
| 1534 |
+
if (!res) {
|
| 1535 |
+
console.log(`[CMD] "/resetactresses" failed: target user "${targetInput}" not found.`);
|
| 1536 |
+
return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`);
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
res.user.characters = [];
|
| 1540 |
+
saveDB(db);
|
| 1541 |
+
|
| 1542 |
+
console.log(`[ADMIN] Admin ${userId} cleared actresses owned by user ${res.userId} (${res.user.username})`);
|
| 1543 |
+
await bot.sendMessage(chatId, `✅ Cleared all owned actresses for *${res.user.username}*.`, { parse_mode: "Markdown" });
|
| 1544 |
+
});
|
| 1545 |
+
|
| 1546 |
+
// /givemoney [user] [amount]
|
| 1547 |
+
bot.onText(/\/givemoney\s+(.+?)\s+(\d+)/, async (msg, match) => {
|
| 1548 |
+
const chatId = msg.chat.id;
|
| 1549 |
+
const userId = String(msg.from.id);
|
| 1550 |
+
const targetInput = match[1].trim();
|
| 1551 |
+
const amount = parseInt(match[2].trim(), 10);
|
| 1552 |
+
console.log(`[CMD] "/givemoney" invoked by admin ID: ${userId} for target: ${targetInput}, amount: ${amount} in chat: ${chatId}`);
|
| 1553 |
+
|
| 1554 |
+
if (!isAdmin(userId)) {
|
| 1555 |
+
console.log(`[CMD] Permission denied for "/givemoney" by user: ${userId}`);
|
| 1556 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
if (isNaN(amount) || amount <= 0) {
|
| 1560 |
+
return bot.sendMessage(chatId, "❌ Invalid amount. Must be a positive number.");
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
const db = loadDB();
|
| 1564 |
+
const res = findUserByIdOrUsername(db, targetInput);
|
| 1565 |
+
if (!res) {
|
| 1566 |
+
console.log(`[CMD] "/givemoney" failed: target user "${targetInput}" not found.`);
|
| 1567 |
+
return bot.sendMessage(chatId, `❌ User "${targetInput}" not found in database.`);
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
res.user.coins = (res.user.coins || 0) + amount;
|
| 1571 |
+
saveDB(db);
|
| 1572 |
+
|
| 1573 |
+
console.log(`[ADMIN] Admin ${userId} gave ${amount} coins to user ${res.userId} (${res.user.username})`);
|
| 1574 |
+
await bot.sendMessage(
|
| 1575 |
+
chatId,
|
| 1576 |
+
`✅ Added *${amount} coins* to *${res.user.username}*'s wallet.\n` +
|
| 1577 |
+
`💰 New Balance: *${res.user.coins} coins*`,
|
| 1578 |
+
{ parse_mode: "Markdown" }
|
| 1579 |
+
);
|
| 1580 |
+
});
|
| 1581 |
+
|
| 1582 |
+
// /resetserver
|
| 1583 |
+
bot.onText(/\/resetserver/, async (msg) => {
|
| 1584 |
+
const chatId = msg.chat.id;
|
| 1585 |
+
const userId = String(msg.from.id);
|
| 1586 |
+
console.log(`[CMD] "/resetserver" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1587 |
+
|
| 1588 |
+
if (!isAdmin(userId)) {
|
| 1589 |
+
console.log(`[CMD] Permission denied for "/resetserver" by user: ${userId}`);
|
| 1590 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1591 |
+
}
|
| 1592 |
+
|
| 1593 |
+
const db = loadDB();
|
| 1594 |
+
const userCount = Object.keys(db.users).length;
|
| 1595 |
+
for (const uid of Object.keys(db.users)) {
|
| 1596 |
+
db.users[uid].coins = 0;
|
| 1597 |
+
db.users[uid].totalWins = 0;
|
| 1598 |
+
db.users[uid].characters = [];
|
| 1599 |
+
}
|
| 1600 |
+
saveDB(db);
|
| 1601 |
+
|
| 1602 |
+
console.log(`[ADMIN] Admin ${userId} executed full server reset. Cleared records for ${userCount} users.`);
|
| 1603 |
+
await bot.sendMessage(
|
| 1604 |
+
chatId,
|
| 1605 |
+
`⚠️ *Server Reset Complete!*\n\n` +
|
| 1606 |
+
`Cleared wallet balances, win counts, and owned actresses for all *${userCount}* users.`,
|
| 1607 |
+
{ parse_mode: "Markdown" }
|
| 1608 |
+
);
|
| 1609 |
+
});
|
| 1610 |
+
|
| 1611 |
+
// /setminplayers [number] (admin only)
|
| 1612 |
+
bot.onText(/\/setminplayers\s+(.+)/, async (msg, match) => {
|
| 1613 |
+
const chatId = msg.chat.id;
|
| 1614 |
+
const userId = String(msg.from.id);
|
| 1615 |
+
const valStr = match[1].trim();
|
| 1616 |
+
console.log(`[CMD] "/setminplayers" invoked by admin ID: ${userId} with value: ${valStr}`);
|
| 1617 |
+
|
| 1618 |
+
if (!isAdmin(userId)) {
|
| 1619 |
+
console.log(`[CMD] Permission denied for "/setminplayers" by user: ${userId}`);
|
| 1620 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1621 |
+
}
|
| 1622 |
+
|
| 1623 |
+
const num = parseInt(valStr, 10);
|
| 1624 |
+
if (isNaN(num) || num <= 0) {
|
| 1625 |
+
return bot.sendMessage(chatId, "❌ Minimum player count must be a valid positive number.");
|
| 1626 |
+
}
|
| 1627 |
+
|
| 1628 |
+
REQUIRED_PLAYERS = num;
|
| 1629 |
+
const db = loadDB();
|
| 1630 |
+
db.requiredPlayers = num;
|
| 1631 |
+
saveDB(db);
|
| 1632 |
+
|
| 1633 |
+
await bot.sendMessage(
|
| 1634 |
+
chatId,
|
| 1635 |
+
`✅ *Minimum players required for auction has been set to:* \`${num}\``,
|
| 1636 |
+
{ parse_mode: "Markdown" }
|
| 1637 |
+
);
|
| 1638 |
+
});
|
| 1639 |
+
|
| 1640 |
+
// /how
|
| 1641 |
+
bot.onText(/\/how/, async (msg) => {
|
| 1642 |
+
console.log(`[CMD] "/how" requested in chat: ${msg.chat.id}`);
|
| 1643 |
+
const howText =
|
| 1644 |
+
`🎴 *HOW TO PLAY (Layman's Guide)* 🎴\n\n` +
|
| 1645 |
+
`Welcome to the Auction Bot! Here's a simple breakdown of how the game works, along with the main rules and constraints:\n\n` +
|
| 1646 |
+
`📖 *Gameplay Flow:*\n\n` +
|
| 1647 |
+
`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` +
|
| 1648 |
+
`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` +
|
| 1649 |
+
`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` +
|
| 1650 |
+
` • Higher than the base price of the actress.\n\n` +
|
| 1651 |
+
` • Higher than the current highest bid.\n\n` +
|
| 1652 |
+
` • Within your total available coins (Session Allowance + Global Wallet balance).\n\n` +
|
| 1653 |
+
`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` +
|
| 1654 |
+
`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` +
|
| 1655 |
+
`⚖️ *Rules & Constraints:*\n\n` +
|
| 1656 |
+
`• *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` +
|
| 1657 |
+
`• *No Overdrafts*: If your bid amount is higher than your combined coins (Allowance + Global Wallet balance), your bid will be rejected.\n\n` +
|
| 1658 |
+
`• *Group Sessions*: Auctions happen inside the group chat. Only players who joined the lobby via \`bid start\` can place bids.\n\n` +
|
| 1659 |
+
`• *Daily Coins*: Type \`/daily\` once every 24 hours to claim a free \`+10 coins\` boost to your global wallet!\n\n` +
|
| 1660 |
+
`• *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`;
|
| 1661 |
+
|
| 1662 |
+
await bot.sendMessage(msg.chat.id, howText, { parse_mode: "Markdown" });
|
| 1663 |
+
});
|
| 1664 |
+
|
| 1665 |
+
// /help
|
| 1666 |
+
bot.onText(/\/help|\/start/, async (msg) => {
|
| 1667 |
+
console.log(`[CMD] "/help" or "/start" requested in chat: ${msg.chat.id}`);
|
| 1668 |
+
await bot.sendMessage(
|
| 1669 |
+
msg.chat.id,
|
| 1670 |
+
`🎴 *Auction Bot — Commands*\n\n` +
|
| 1671 |
+
`• \`bid start\` — Join/start an auction session (need ${REQUIRED_PLAYERS} players)\n` +
|
| 1672 |
+
`• \`[amount]\` — Place a bid during an active auction (e.g. \`150\`)\n` +
|
| 1673 |
+
`• /how — Simple guide on how to play (rules & constraints)\n` +
|
| 1674 |
+
`• /daily — Claim daily allowance (+10 coins)\n` +
|
| 1675 |
+
`• /owners — View actress ownership leaderboard\n` +
|
| 1676 |
+
`• /leaderboard — All-time top players (wins & titles)\n` +
|
| 1677 |
+
`• /mystats — Your personal stats, wins, and owned actresses\n` +
|
| 1678 |
+
`• /listactresses — See all actresses in the pool\n` +
|
| 1679 |
+
`• /help — Show this message`,
|
| 1680 |
+
{ parse_mode: "Markdown" }
|
| 1681 |
+
);
|
| 1682 |
+
});
|
| 1683 |
+
|
| 1684 |
+
// /helpadmin (admin only)
|
| 1685 |
+
bot.onText(/\/helpadmin/, async (msg) => {
|
| 1686 |
+
const chatId = msg.chat.id;
|
| 1687 |
+
const userId = String(msg.from.id);
|
| 1688 |
+
console.log(`[CMD] "/helpadmin" requested by admin ID: ${userId} in chat: ${chatId}`);
|
| 1689 |
+
|
| 1690 |
+
if (!isAdmin(userId)) {
|
| 1691 |
+
console.log(`[CMD] Permission denied for "/helpadmin" by user: ${userId}`);
|
| 1692 |
+
return bot.sendMessage(chatId, "⛔ Only admins can access this command.");
|
| 1693 |
+
}
|
| 1694 |
+
|
| 1695 |
+
await bot.sendMessage(
|
| 1696 |
+
chatId,
|
| 1697 |
+
`🎴 *Auction Bot — Admin Commands*\n\n` +
|
| 1698 |
+
`• /connect — Connect group to your private DM (run in group chat)\n` +
|
| 1699 |
+
`• /addactress — Add a new actress\n` +
|
| 1700 |
+
`• /setminplayers [num] — Set minimum player count for auction\n` +
|
| 1701 |
+
`• /cancelsession — Cancel current session\n` +
|
| 1702 |
+
`• /givemoney [user] [amt] — Add coins to user wallet\n` +
|
| 1703 |
+
`• /resetbalance [user] — Set user wallet coins to 0\n` +
|
| 1704 |
+
`• /resetwins [user] — Set user wins count to 0\n` +
|
| 1705 |
+
`• /resetactresses [user] — Clear user owned actresses\n` +
|
| 1706 |
+
`• /resetserver — Clear wins, balance, and owned actresses for all users\n` +
|
| 1707 |
+
`• /adminstatus — Check bot system status\n` +
|
| 1708 |
+
`• /adminsession — Check current active bid details\n` +
|
| 1709 |
+
`• /adminlistactresses — List all actresses (IDs & Details)\n` +
|
| 1710 |
+
`• /actressimages [id] — View all images & skins of an actress\n` +
|
| 1711 |
+
`• /updateactress [id] , [field] , [value] — Update actress\n` +
|
| 1712 |
+
`• /deleteactress [id] — Delete an actress\n` +
|
| 1713 |
+
`• /deletemultiactress [id1], [id2]... — Delete multiple actresses\n` +
|
| 1714 |
+
`• /addimage [id] — Add an extra photo (send photo with caption)\n` +
|
| 1715 |
+
`• /editcaption [text] — Edit caption of an image (reply to the target image)\n` +
|
| 1716 |
+
`• /deleteimage — Delete a specific image (reply to the target image)`,
|
| 1717 |
+
{ parse_mode: "Markdown" }
|
| 1718 |
+
);
|
| 1719 |
+
});
|
| 1720 |
+
|
| 1721 |
+
// /adminstatus (admin only)
|
| 1722 |
+
bot.onText(/\/adminstatus/, async (msg) => {
|
| 1723 |
+
const chatId = msg.chat.id;
|
| 1724 |
+
const userId = String(msg.from.id);
|
| 1725 |
+
console.log(`[CMD] "/adminstatus" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1726 |
+
|
| 1727 |
+
if (!isAdmin(userId)) {
|
| 1728 |
+
console.log(`[CMD] Permission denied for "/adminstatus" by user: ${userId}`);
|
| 1729 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1730 |
+
}
|
| 1731 |
+
|
| 1732 |
+
const db = loadDB();
|
| 1733 |
+
const uptime = Math.round(process.uptime());
|
| 1734 |
+
const activeSessionChats = Object.keys(sessions).filter(
|
| 1735 |
+
(chatId) => sessions[chatId] && sessions[chatId].phase !== "inactive"
|
| 1736 |
+
);
|
| 1737 |
+
|
| 1738 |
+
const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`;
|
| 1739 |
+
|
| 1740 |
+
await bot.sendMessage(
|
| 1741 |
+
msg.chat.id,
|
| 1742 |
+
`⚙️ *Admin Bot Status*\n\n` +
|
| 1743 |
+
`• *Uptime:* \`${uptimeStr}\`\n` +
|
| 1744 |
+
`• *Active Sessions:* \`${activeSessionChats.length}\` chat(s)\n` +
|
| 1745 |
+
`• *Registered Users:* \`${Object.keys(db.users || {}).length}\`\n` +
|
| 1746 |
+
`• *Custom Characters:* \`${(db.customCharacters || []).length}\`\n` +
|
| 1747 |
+
`• *Memory Usage:* \`${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)} MB\`\n` +
|
| 1748 |
+
`• *Node Version:* \`${process.version}\``,
|
| 1749 |
+
{ parse_mode: "Markdown" }
|
| 1750 |
+
);
|
| 1751 |
+
});
|
| 1752 |
+
|
| 1753 |
+
// /adminsession (admin only)
|
| 1754 |
+
bot.onText(/\/adminsession/, async (msg) => {
|
| 1755 |
+
const chatId = msg.chat.id;
|
| 1756 |
+
const userId = String(msg.from.id);
|
| 1757 |
+
console.log(`[CMD] "/adminsession" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1758 |
+
|
| 1759 |
+
if (!isAdmin(userId)) {
|
| 1760 |
+
console.log(`[CMD] Permission denied for "/adminsession" by user: ${userId}`);
|
| 1761 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1762 |
+
}
|
| 1763 |
+
|
| 1764 |
+
let targetChatId = msg.chat.id;
|
| 1765 |
+
|
| 1766 |
+
if (msg.chat.type === "private") {
|
| 1767 |
+
const db = loadDB();
|
| 1768 |
+
targetChatId = (db.connections || {})[userId];
|
| 1769 |
+
if (!targetChatId) {
|
| 1770 |
+
console.log(`[CMD] "/adminsession" failed: admin ${userId} has no connected group chat.`);
|
| 1771 |
+
return bot.sendMessage(
|
| 1772 |
+
msg.chat.id,
|
| 1773 |
+
"⚠️ You haven't connected any group chat yet. Use `/connect` in the group chat first.",
|
| 1774 |
+
{ parse_mode: "Markdown" }
|
| 1775 |
+
);
|
| 1776 |
+
}
|
| 1777 |
+
}
|
| 1778 |
+
|
| 1779 |
+
const session = getSession(targetChatId);
|
| 1780 |
+
|
| 1781 |
+
if (!session || session.phase === "inactive") {
|
| 1782 |
+
return bot.sendMessage(
|
| 1783 |
+
msg.chat.id,
|
| 1784 |
+
`ℹ️ *Session Status:* Inactive in connected chat (\`${targetChatId}\`).\nType \`bid start\` in the group to initialize.`,
|
| 1785 |
+
{ parse_mode: "Markdown" }
|
| 1786 |
+
);
|
| 1787 |
+
}
|
| 1788 |
+
|
| 1789 |
+
let details = `📊 *Session Details for chat:* \`${targetChatId}\`\n`;
|
| 1790 |
+
details += `• *Phase:* \`${session.phase}\`\n`;
|
| 1791 |
+
|
| 1792 |
+
if (session.phase === "waiting") {
|
| 1793 |
+
const ready = Array.from(session.readyUsers || []);
|
| 1794 |
+
details += `• *Ready Users:* \`${ready.length}/${REQUIRED_PLAYERS}\`\n`;
|
| 1795 |
+
ready.forEach(([uid, uname]) => {
|
| 1796 |
+
details += ` - ${uname} (\`${uid}\`)\n`;
|
| 1797 |
+
});
|
| 1798 |
+
} else if (session.phase === "bidding") {
|
| 1799 |
+
const totalChars = session.characters.length;
|
| 1800 |
+
const curIdx = session.currentCharacterIndex;
|
| 1801 |
+
const char = session.characters[curIdx] || { name: "N/A" };
|
| 1802 |
+
details += `• *Characters Processed:* \`${curIdx}/${totalChars}\` (Current: *${char.name}*)\n`;
|
| 1803 |
+
|
| 1804 |
+
const highBid = session.currentHighestBid;
|
| 1805 |
+
if (highBid) {
|
| 1806 |
+
details += `• *Highest Bid:* ${highBid.username} → \`${highBid.amount} coins\`\n`;
|
| 1807 |
+
} else {
|
| 1808 |
+
details += `• *Highest Bid:* \`None\`\n`;
|
| 1809 |
+
}
|
| 1810 |
+
|
| 1811 |
+
details += `• *Timer:* \`${session.secondsLeft}s\` remaining\n`;
|
| 1812 |
+
details += `• *Participants:* \n`;
|
| 1813 |
+
Object.entries(session.participants).forEach(([uid, p]) => {
|
| 1814 |
+
details += ` - ${p.username} (\`${uid}\`): \`${p.coins} coins\`\n`;
|
| 1815 |
+
});
|
| 1816 |
+
}
|
| 1817 |
+
|
| 1818 |
+
await bot.sendMessage(msg.chat.id, details, { parse_mode: "Markdown" });
|
| 1819 |
+
});
|
| 1820 |
+
|
| 1821 |
+
// /adminlistactresses (admin only)
|
| 1822 |
+
bot.onText(/\/adminlistactresses|\/adminlistchars/, async (msg) => {
|
| 1823 |
+
const chatId = msg.chat.id;
|
| 1824 |
+
const userId = String(msg.from.id);
|
| 1825 |
+
console.log(`[CMD] "/adminlistactresses" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1826 |
+
|
| 1827 |
+
if (!isAdmin(userId)) {
|
| 1828 |
+
console.log(`[CMD] Permission denied for "/adminlistactresses" by user: ${userId}`);
|
| 1829 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1830 |
+
}
|
| 1831 |
+
|
| 1832 |
+
const db = loadDB();
|
| 1833 |
+
const deletedIds = db.deletedDefaultIds || [];
|
| 1834 |
+
|
| 1835 |
+
let listText = `🎴 *All Actresses (Admin View)*\n`;
|
| 1836 |
+
|
| 1837 |
+
|
| 1838 |
+
|
| 1839 |
+
const custom = db.customCharacters || [];
|
| 1840 |
+
if (custom.length === 0) {
|
| 1841 |
+
listText += `_No custom actresses yet._\n`;
|
| 1842 |
+
} else {
|
| 1843 |
+
custom.forEach((c) => {
|
| 1844 |
+
listText += `• \`${c.id}\`: *${c.name}* (Price: ${c.base_price}) \n`;
|
| 1845 |
+
});
|
| 1846 |
+
}
|
| 1847 |
+
|
| 1848 |
+
await bot.sendMessage(msg.chat.id, listText, { parse_mode: "Markdown" });
|
| 1849 |
+
});
|
| 1850 |
+
|
| 1851 |
+
// /deleteactress [id] (admin only)
|
| 1852 |
+
bot.onText(/(?:\/deleteactress|\/deletechar)\s+(.+)/, async (msg, match) => {
|
| 1853 |
+
const chatId = msg.chat.id;
|
| 1854 |
+
const userId = String(msg.from.id);
|
| 1855 |
+
const targetId = match[1].trim();
|
| 1856 |
+
console.log(`[CMD] "/deleteactress" invoked by admin ID: ${userId} in chat: ${chatId} for ID: ${targetId}`);
|
| 1857 |
+
|
| 1858 |
+
if (!isAdmin(userId)) {
|
| 1859 |
+
console.log(`[CMD] Permission denied for "/deleteactress" by user: ${userId}`);
|
| 1860 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1861 |
+
}
|
| 1862 |
+
|
| 1863 |
+
const db = loadDB();
|
| 1864 |
+
|
| 1865 |
+
// Check if it is a custom actress
|
| 1866 |
+
const customIndex = (db.customCharacters || []).findIndex((c) => c.id === targetId);
|
| 1867 |
+
|
| 1868 |
+
// Check if it is a default actress
|
| 1869 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId);
|
| 1870 |
+
|
| 1871 |
+
if (customIndex === -1 && !defaultChar) {
|
| 1872 |
+
console.log(`[CMD] "/deleteactress" failed: ID ${targetId} not found.`);
|
| 1873 |
+
return bot.sendMessage(msg.chat.id, `❌ Actress with ID \`${targetId}\` not found.`);
|
| 1874 |
+
}
|
| 1875 |
+
|
| 1876 |
+
if (customIndex !== -1) {
|
| 1877 |
+
// Delete custom actress
|
| 1878 |
+
const deleted = db.customCharacters.splice(customIndex, 1)[0];
|
| 1879 |
+
saveDB(db);
|
| 1880 |
+
console.log(`[CMD] Custom actress deleted: "${deleted.name}" (ID: ${targetId})`);
|
| 1881 |
+
return bot.sendMessage(msg.chat.id, `✅ Custom actress *${deleted.name}* (\`${targetId}\`) has been deleted.`);
|
| 1882 |
+
}
|
| 1883 |
+
|
| 1884 |
+
if (defaultChar) {
|
| 1885 |
+
// Delete default actress (hide it)
|
| 1886 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 1887 |
+
if (db.deletedDefaultIds.includes(targetId)) {
|
| 1888 |
+
console.log(`[CMD] Default actress ${targetId} was already hidden.`);
|
| 1889 |
+
return bot.sendMessage(msg.chat.id, `ℹ️ Actress \`${targetId}\` was already deleted.`);
|
| 1890 |
+
}
|
| 1891 |
+
db.deletedDefaultIds.push(targetId);
|
| 1892 |
+
saveDB(db);
|
| 1893 |
+
console.log(`[CMD] Default actress hidden: "${defaultChar.name}" (ID: ${targetId})`);
|
| 1894 |
+
return bot.sendMessage(msg.chat.id, `✅ Default actress *${defaultChar.name}* (\`${targetId}\`) has been deleted (hidden).`);
|
| 1895 |
+
}
|
| 1896 |
+
});
|
| 1897 |
+
|
| 1898 |
+
// /updateactress [id] , [field] , [value] (admin only)
|
| 1899 |
+
bot.onText(/(?:\/updateactress|\/updatechar)\s+(.+)/, async (msg, match) => {
|
| 1900 |
+
const chatId = msg.chat.id;
|
| 1901 |
+
const userId = String(msg.from.id);
|
| 1902 |
+
console.log(`[CMD] "/updateactress" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1903 |
+
|
| 1904 |
+
if (!isAdmin(userId)) {
|
| 1905 |
+
console.log(`[CMD] Permission denied for "/updateactress" by user: ${userId}`);
|
| 1906 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1907 |
+
}
|
| 1908 |
+
|
| 1909 |
+
const parts = match[1].split(",").map((s) => s.trim());
|
| 1910 |
+
if (parts.length < 3) {
|
| 1911 |
+
return bot.sendMessage(
|
| 1912 |
+
msg.chat.id,
|
| 1913 |
+
"❌ Format: \`/updateactress ID , FIELD , VALUE\`\n\n" +
|
| 1914 |
+
"Fields: \`name\`, \`price\`, \`features\`\n" +
|
| 1915 |
+
"Example:\n\`/updateactress disha , price , 95\`\n" +
|
| 1916 |
+
"Example:\n\`/updateactress disha , features , Series: Bollywood , Size: Petite\`",
|
| 1917 |
+
{ parse_mode: "Markdown" }
|
| 1918 |
+
);
|
| 1919 |
+
}
|
| 1920 |
+
|
| 1921 |
+
const targetId = parts[0];
|
| 1922 |
+
const fieldRaw = parts[1];
|
| 1923 |
+
const field = fieldRaw.toLowerCase();
|
| 1924 |
+
|
| 1925 |
+
if (!["name", "price", "features"].includes(field)) {
|
| 1926 |
+
return bot.sendMessage(msg.chat.id, "❌ Field must be one of: \`name\`, \`price\`, \`features\`.格式: `/updateactress ID , FIELD , VALUE`");
|
| 1927 |
+
}
|
| 1928 |
+
|
| 1929 |
+
const db = loadDB();
|
| 1930 |
+
let char = (db.customCharacters || []).find((c) => c.id === targetId);
|
| 1931 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId);
|
| 1932 |
+
|
| 1933 |
+
if (!char && !defaultChar) {
|
| 1934 |
+
console.log(`[CMD] "/updateactress" failed: ID ${targetId} not found.`);
|
| 1935 |
+
return bot.sendMessage(msg.chat.id, `❌ Actress with ID \`${targetId}\` not found.`);
|
| 1936 |
+
}
|
| 1937 |
+
|
| 1938 |
+
// If it's a default actress, copy it to customCharacters (Copy-On-Write)
|
| 1939 |
+
if (!char && defaultChar) {
|
| 1940 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 1941 |
+
if (!db.deletedDefaultIds.includes(targetId)) {
|
| 1942 |
+
db.deletedDefaultIds.push(targetId);
|
| 1943 |
+
}
|
| 1944 |
+
|
| 1945 |
+
char = JSON.parse(JSON.stringify(defaultChar));
|
| 1946 |
+
db.customCharacters = db.customCharacters || [];
|
| 1947 |
+
db.customCharacters.push(char);
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
+
// Apply updates to the actress object
|
| 1951 |
+
const value = parts.slice(2).join(", ");
|
| 1952 |
+
console.log(`[CMD] Admin updating ID ${targetId}: field="${field}", old_val="${char[field] || char.base_price || ""}", new_val="${value}"`);
|
| 1953 |
+
|
| 1954 |
+
if (field === "name") {
|
| 1955 |
+
char.name = parts[2];
|
| 1956 |
+
} else if (field === "price") {
|
| 1957 |
+
const price = parseInt(parts[2], 10);
|
| 1958 |
+
if (isNaN(price) || price <= 0) {
|
| 1959 |
+
return bot.sendMessage(msg.chat.id, "❌ Price must be a valid positive number.");
|
| 1960 |
+
}
|
| 1961 |
+
char.base_price = price;
|
| 1962 |
+
} else if (field === "features") {
|
| 1963 |
+
char.features = parts.slice(2).filter(Boolean);
|
| 1964 |
+
}
|
| 1965 |
+
|
| 1966 |
+
saveDB(db);
|
| 1967 |
+
|
| 1968 |
+
await bot.sendMessage(
|
| 1969 |
+
msg.chat.id,
|
| 1970 |
+
`✅ Actress \`${targetId}\` updated successfully!\n\n` +
|
| 1971 |
+
`• *Name:* ${char.name}\n` +
|
| 1972 |
+
`• *Price:* ${char.base_price} coins\n` +
|
| 1973 |
+
`• *Features:* ${char.features.join(", ")}`,
|
| 1974 |
+
{ parse_mode: "Markdown" }
|
| 1975 |
+
);
|
| 1976 |
+
});
|
| 1977 |
+
|
| 1978 |
+
// /deletemultiactress [id1], [id2]... (admin only)
|
| 1979 |
+
bot.onText(/(?:\/deletemultiactress|\/deletemultichar)\s+(.+)/, async (msg, match) => {
|
| 1980 |
+
const chatId = msg.chat.id;
|
| 1981 |
+
const userId = String(msg.from.id);
|
| 1982 |
+
console.log(`[CMD] "/deletemultiactress" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 1983 |
+
|
| 1984 |
+
if (!isAdmin(userId)) {
|
| 1985 |
+
console.log(`[CMD] Permission denied for "/deletemultiactress" by user: ${userId}`);
|
| 1986 |
+
return bot.sendMessage(msg.chat.id, "⛔ Only admins can access this command.");
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
const ids = match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
| 1990 |
+
if (ids.length === 0) {
|
| 1991 |
+
return bot.sendMessage(msg.chat.id, "❌ Please specify one or more comma-separated IDs.");
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
console.log(`[CMD] Bulk deleting/hiding IDs:`, ids);
|
| 1995 |
+
|
| 1996 |
+
const db = loadDB();
|
| 1997 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 1998 |
+
const results = [];
|
| 1999 |
+
|
| 2000 |
+
for (const targetId of ids) {
|
| 2001 |
+
const customIndex = (db.customCharacters || []).findIndex((c) => c.id === targetId);
|
| 2002 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId);
|
| 2003 |
+
|
| 2004 |
+
if (customIndex !== -1) {
|
| 2005 |
+
const deleted = db.customCharacters.splice(customIndex, 1)[0];
|
| 2006 |
+
results.push(`🗑️ Custom *${deleted.name}* (\`${targetId}\`) deleted.`);
|
| 2007 |
+
} else if (defaultChar) {
|
| 2008 |
+
if (!db.deletedDefaultIds.includes(targetId)) {
|
| 2009 |
+
db.deletedDefaultIds.push(targetId);
|
| 2010 |
+
}
|
| 2011 |
+
results.push(`🙈 Default *${defaultChar.name}* (\`${targetId}\`) hidden.`);
|
| 2012 |
+
} else {
|
| 2013 |
+
results.push(`❓ ID \`${targetId}\` not found.`);
|
| 2014 |
+
}
|
| 2015 |
+
}
|
| 2016 |
+
|
| 2017 |
+
saveDB(db);
|
| 2018 |
+
|
| 2019 |
+
await bot.sendMessage(
|
| 2020 |
+
msg.chat.id,
|
| 2021 |
+
`📝 *Bulk Delete Results:*\n\n${results.join("\n")}`,
|
| 2022 |
+
{ parse_mode: "Markdown" }
|
| 2023 |
+
);
|
| 2024 |
+
});
|
| 2025 |
+
|
| 2026 |
+
// /editcaption [new caption] (admin only, replied to a photo)
|
| 2027 |
+
bot.onText(/\/editcaption(?:\s+(.+))?/, async (msg, match) => {
|
| 2028 |
+
const chatId = msg.chat.id;
|
| 2029 |
+
const userId = String(msg.from.id);
|
| 2030 |
+
console.log(`[CMD] "/editcaption" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 2031 |
+
|
| 2032 |
+
if (!isAdmin(userId)) {
|
| 2033 |
+
console.log(`[CMD] Permission denied for "/editcaption" by user: ${userId}`);
|
| 2034 |
+
return bot.sendMessage(chatId, "⛔ Only admins can use this command.");
|
| 2035 |
+
}
|
| 2036 |
+
|
| 2037 |
+
if (!msg.reply_to_message || !msg.reply_to_message.photo) {
|
| 2038 |
+
return bot.sendMessage(chatId, "⚠️ Please reply to the specific image message you want to edit.");
|
| 2039 |
+
}
|
| 2040 |
+
|
| 2041 |
+
const replyPhoto = msg.reply_to_message.photo;
|
| 2042 |
+
const fileId = replyPhoto[replyPhoto.length - 1].file_id;
|
| 2043 |
+
const newCaption = match[1] ? match[1].trim() : "";
|
| 2044 |
+
|
| 2045 |
+
const db = loadDB();
|
| 2046 |
+
let actress = (db.customCharacters || []).find((c) => {
|
| 2047 |
+
const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []);
|
| 2048 |
+
return images.some((img) => {
|
| 2049 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2050 |
+
return imgId === fileId;
|
| 2051 |
+
});
|
| 2052 |
+
});
|
| 2053 |
+
|
| 2054 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => {
|
| 2055 |
+
const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []);
|
| 2056 |
+
return images.some((img) => {
|
| 2057 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2058 |
+
return imgId === fileId;
|
| 2059 |
+
});
|
| 2060 |
+
});
|
| 2061 |
+
|
| 2062 |
+
if (!actress && defaultChar) {
|
| 2063 |
+
// Copy-on-Write: clone to db.customCharacters and delete from defaults
|
| 2064 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 2065 |
+
if (!db.deletedDefaultIds.includes(defaultChar.id)) {
|
| 2066 |
+
db.deletedDefaultIds.push(defaultChar.id);
|
| 2067 |
+
}
|
| 2068 |
+
|
| 2069 |
+
actress = JSON.parse(JSON.stringify(defaultChar));
|
| 2070 |
+
db.customCharacters = db.customCharacters || [];
|
| 2071 |
+
db.customCharacters.push(actress);
|
| 2072 |
+
}
|
| 2073 |
+
|
| 2074 |
+
if (actress) {
|
| 2075 |
+
if (!Array.isArray(actress.image_url)) {
|
| 2076 |
+
actress.image_url = [actress.image_url];
|
| 2077 |
+
}
|
| 2078 |
+
|
| 2079 |
+
let found = false;
|
| 2080 |
+
actress.image_url = actress.image_url.map((img) => {
|
| 2081 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2082 |
+
if (imgId === fileId) {
|
| 2083 |
+
found = true;
|
| 2084 |
+
return { file_id: fileId, caption: newCaption };
|
| 2085 |
+
}
|
| 2086 |
+
return img;
|
| 2087 |
+
});
|
| 2088 |
+
|
| 2089 |
+
if (found) {
|
| 2090 |
+
saveDB(db);
|
| 2091 |
+
return bot.sendMessage(
|
| 2092 |
+
chatId,
|
| 2093 |
+
`✅ Caption updated successfully for *${actress.name}*!\n` +
|
| 2094 |
+
`📝 *New Caption:* ${newCaption || "_(cleared)_"}`,
|
| 2095 |
+
{ parse_mode: "Markdown" }
|
| 2096 |
+
);
|
| 2097 |
+
}
|
| 2098 |
+
}
|
| 2099 |
+
|
| 2100 |
+
return bot.sendMessage(chatId, "❌ Could not find this image in the database.");
|
| 2101 |
+
});
|
| 2102 |
+
|
| 2103 |
+
// /deleteimage (admin only, replied to a photo)
|
| 2104 |
+
bot.onText(/\/deleteimage/, async (msg) => {
|
| 2105 |
+
const chatId = msg.chat.id;
|
| 2106 |
+
const userId = String(msg.from.id);
|
| 2107 |
+
console.log(`[CMD] "/deleteimage" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 2108 |
+
|
| 2109 |
+
if (!isAdmin(userId)) {
|
| 2110 |
+
console.log(`[CMD] Permission denied for "/deleteimage" by user: ${userId}`);
|
| 2111 |
+
return bot.sendMessage(chatId, "⛔ Only admins can use this command.");
|
| 2112 |
+
}
|
| 2113 |
+
|
| 2114 |
+
if (!msg.reply_to_message || !msg.reply_to_message.photo) {
|
| 2115 |
+
return bot.sendMessage(chatId, "⚠️ Please reply to the specific image message you want to delete.");
|
| 2116 |
+
}
|
| 2117 |
+
|
| 2118 |
+
const replyPhoto = msg.reply_to_message.photo;
|
| 2119 |
+
const fileId = replyPhoto[replyPhoto.length - 1].file_id;
|
| 2120 |
+
|
| 2121 |
+
const db = loadDB();
|
| 2122 |
+
let actress = (db.customCharacters || []).find((c) => {
|
| 2123 |
+
const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []);
|
| 2124 |
+
return images.some((img) => {
|
| 2125 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2126 |
+
return imgId === fileId;
|
| 2127 |
+
});
|
| 2128 |
+
});
|
| 2129 |
+
|
| 2130 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => {
|
| 2131 |
+
const images = Array.isArray(c.image_url) ? c.image_url : (c.image_url ? [c.image_url] : []);
|
| 2132 |
+
return images.some((img) => {
|
| 2133 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2134 |
+
return imgId === fileId;
|
| 2135 |
+
});
|
| 2136 |
+
});
|
| 2137 |
+
|
| 2138 |
+
if (!actress && defaultChar) {
|
| 2139 |
+
// Copy-on-Write: clone to db.customCharacters and delete from defaults
|
| 2140 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 2141 |
+
if (!db.deletedDefaultIds.includes(defaultChar.id)) {
|
| 2142 |
+
db.deletedDefaultIds.push(defaultChar.id);
|
| 2143 |
+
}
|
| 2144 |
+
|
| 2145 |
+
actress = JSON.parse(JSON.stringify(defaultChar));
|
| 2146 |
+
db.customCharacters = db.customCharacters || [];
|
| 2147 |
+
db.customCharacters.push(actress);
|
| 2148 |
+
}
|
| 2149 |
+
|
| 2150 |
+
if (actress) {
|
| 2151 |
+
if (!Array.isArray(actress.image_url)) {
|
| 2152 |
+
actress.image_url = [actress.image_url];
|
| 2153 |
+
}
|
| 2154 |
+
|
| 2155 |
+
let found = false;
|
| 2156 |
+
actress.image_url = actress.image_url.filter((img) => {
|
| 2157 |
+
const imgId = typeof img === "object" && img !== null ? img.file_id : img;
|
| 2158 |
+
if (imgId === fileId) {
|
| 2159 |
+
found = true;
|
| 2160 |
+
return false;
|
| 2161 |
+
}
|
| 2162 |
+
return true;
|
| 2163 |
+
});
|
| 2164 |
+
|
| 2165 |
+
if (found) {
|
| 2166 |
+
saveDB(db);
|
| 2167 |
+
return bot.sendMessage(
|
| 2168 |
+
chatId,
|
| 2169 |
+
`✅ Image deleted successfully from *${actress.name}*!\n` +
|
| 2170 |
+
`🖼️ Remaining images: *${actress.image_url.length}*`,
|
| 2171 |
+
{ parse_mode: "Markdown" }
|
| 2172 |
+
);
|
| 2173 |
+
}
|
| 2174 |
+
}
|
| 2175 |
+
|
| 2176 |
+
return bot.sendMessage(chatId, "❌ Could not find this image in the database.");
|
| 2177 |
+
});
|
| 2178 |
+
|
| 2179 |
+
// /addimage / /addimages (admin only, sent as a photo caption)
|
| 2180 |
+
bot.on("photo", async (msg) => {
|
| 2181 |
+
const chatId = msg.chat.id;
|
| 2182 |
+
const userId = String(msg.from.id);
|
| 2183 |
+
const caption = msg.caption;
|
| 2184 |
+
const mediaGroupId = msg.media_group_id;
|
| 2185 |
+
|
| 2186 |
+
// Detect which command is invoked
|
| 2187 |
+
const isAddImages = caption && caption.toLowerCase().startsWith("/addimages");
|
| 2188 |
+
const isAddImage = caption && caption.toLowerCase().startsWith("/addimage") && !isAddImages;
|
| 2189 |
+
|
| 2190 |
+
if (!isAddImage && !isAddImages) {
|
| 2191 |
+
// If this is a photo without command, check if it's part of an active album upload
|
| 2192 |
+
if (mediaGroupId) {
|
| 2193 |
+
const largestPhoto = msg.photo[msg.photo.length - 1];
|
| 2194 |
+
const photoObj = { fileId: largestPhoto.file_id, messageId: msg.message_id, caption: caption || "" };
|
| 2195 |
+
|
| 2196 |
+
if (mediaGroupAccumulators[mediaGroupId]) {
|
| 2197 |
+
mediaGroupAccumulators[mediaGroupId].photos.push(photoObj);
|
| 2198 |
+
setupAccumulatorTimer(mediaGroupId);
|
| 2199 |
+
} else {
|
| 2200 |
+
mediaGroupBuffer[mediaGroupId] = mediaGroupBuffer[mediaGroupId] || [];
|
| 2201 |
+
mediaGroupBuffer[mediaGroupId].push(photoObj);
|
| 2202 |
+
// Clear buffer after 5s to avoid leak
|
| 2203 |
+
setTimeout(() => {
|
| 2204 |
+
if (mediaGroupBuffer[mediaGroupId]) {
|
| 2205 |
+
delete mediaGroupBuffer[mediaGroupId];
|
| 2206 |
+
}
|
| 2207 |
+
}, 5000);
|
| 2208 |
+
}
|
| 2209 |
+
}
|
| 2210 |
+
return;
|
| 2211 |
+
}
|
| 2212 |
+
|
| 2213 |
+
// Admin access check
|
| 2214 |
+
if (!isAdmin(userId)) {
|
| 2215 |
+
console.log(`[CMD] Permission denied for photo command by user: ${userId}`);
|
| 2216 |
+
return bot.sendMessage(chatId, "⛔ Only admins can add images.");
|
| 2217 |
+
}
|
| 2218 |
+
|
| 2219 |
+
const lines = caption
|
| 2220 |
+
.split("\n")
|
| 2221 |
+
.map((s) => s.trim())
|
| 2222 |
+
.filter(Boolean);
|
| 2223 |
+
|
| 2224 |
+
const commandPrefix = isAddImages ? /^\/addimages\s*/i : /^\/addimage\s*/i;
|
| 2225 |
+
const targetId = lines[0].replace(commandPrefix, "").trim();
|
| 2226 |
+
|
| 2227 |
+
const captionList = isAddImages ? lines.slice(1) : [];
|
| 2228 |
+
const firstPhotoCaption = isAddImages
|
| 2229 |
+
? (captionList[0] || "")
|
| 2230 |
+
: lines.slice(1).join("\n").trim();
|
| 2231 |
+
|
| 2232 |
+
if (!targetId) {
|
| 2233 |
+
return bot.sendMessage(
|
| 2234 |
+
chatId,
|
| 2235 |
+
`❌ Please specify the character ID, e.g. \`${isAddImages ? "/addimages" : "/addimage"} goku\` in the first line of the caption.`
|
| 2236 |
+
);
|
| 2237 |
+
}
|
| 2238 |
+
|
| 2239 |
+
const db = loadDB();
|
| 2240 |
+
let char = (db.customCharacters || []).find((c) => c.id === targetId);
|
| 2241 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId);
|
| 2242 |
+
|
| 2243 |
+
if (!char && !defaultChar) {
|
| 2244 |
+
console.log(`[CMD] Photo command failed: character ID ${targetId} not found.`);
|
| 2245 |
+
return bot.sendMessage(chatId, `❌ Character with ID \`${targetId}\` not found.`);
|
| 2246 |
+
}
|
| 2247 |
+
|
| 2248 |
+
const largestPhoto = msg.photo[msg.photo.length - 1];
|
| 2249 |
+
const firstPhotoObj = { fileId: largestPhoto.file_id, messageId: msg.message_id, caption: firstPhotoCaption };
|
| 2250 |
+
|
| 2251 |
+
if (isAddImage || !mediaGroupId) {
|
| 2252 |
+
// Single image add or legacy /addimage or /addimages without album
|
| 2253 |
+
await saveImagesToCharacter(db, char, defaultChar, [firstPhotoObj], chatId);
|
| 2254 |
+
} else {
|
| 2255 |
+
// Multiple images (media group) using /addimages
|
| 2256 |
+
mediaGroupAccumulators[mediaGroupId] = {
|
| 2257 |
+
chatId,
|
| 2258 |
+
userId,
|
| 2259 |
+
targetId,
|
| 2260 |
+
captionList,
|
| 2261 |
+
photos: [firstPhotoObj]
|
| 2262 |
+
};
|
| 2263 |
+
|
| 2264 |
+
// Merge any pre-buffered images that arrived earlier
|
| 2265 |
+
if (mediaGroupBuffer[mediaGroupId]) {
|
| 2266 |
+
mediaGroupAccumulators[mediaGroupId].photos.push(...mediaGroupBuffer[mediaGroupId]);
|
| 2267 |
+
delete mediaGroupBuffer[mediaGroupId];
|
| 2268 |
+
}
|
| 2269 |
+
|
| 2270 |
+
setupAccumulatorTimer(mediaGroupId);
|
| 2271 |
+
}
|
| 2272 |
+
});
|
| 2273 |
+
|
| 2274 |
+
function setupAccumulatorTimer(mediaGroupId) {
|
| 2275 |
+
const acc = mediaGroupAccumulators[mediaGroupId];
|
| 2276 |
+
if (!acc) return;
|
| 2277 |
+
if (acc.timer) clearTimeout(acc.timer);
|
| 2278 |
+
|
| 2279 |
+
acc.timer = setTimeout(async () => {
|
| 2280 |
+
const { chatId, targetId, photos, captionList } = acc;
|
| 2281 |
+
delete mediaGroupAccumulators[mediaGroupId];
|
| 2282 |
+
|
| 2283 |
+
const db = loadDB();
|
| 2284 |
+
let char = (db.customCharacters || []).find((c) => c.id === targetId);
|
| 2285 |
+
const defaultChar = DEFAULT_CHARACTERS.find((c) => c.id === targetId);
|
| 2286 |
+
|
| 2287 |
+
if (char || defaultChar) {
|
| 2288 |
+
// Sort photos by messageId to match album order
|
| 2289 |
+
photos.sort((a, b) => a.messageId - b.messageId);
|
| 2290 |
+
|
| 2291 |
+
// Distribute caption list lines sequentially to photos
|
| 2292 |
+
if (captionList && captionList.length > 0) {
|
| 2293 |
+
for (let i = 0; i < photos.length; i++) {
|
| 2294 |
+
if (i < captionList.length) {
|
| 2295 |
+
photos[i].caption = captionList[i];
|
| 2296 |
+
}
|
| 2297 |
+
}
|
| 2298 |
+
}
|
| 2299 |
+
|
| 2300 |
+
await saveImagesToCharacter(db, char, defaultChar, photos, chatId);
|
| 2301 |
+
}
|
| 2302 |
+
}, 1500);
|
| 2303 |
+
}
|
| 2304 |
+
|
| 2305 |
+
async function saveImagesToCharacter(db, char, defaultChar, photos, chatId) {
|
| 2306 |
+
if (!char && defaultChar) {
|
| 2307 |
+
db.deletedDefaultIds = db.deletedDefaultIds || [];
|
| 2308 |
+
if (!db.deletedDefaultIds.includes(defaultChar.id)) {
|
| 2309 |
+
db.deletedDefaultIds.push(defaultChar.id);
|
| 2310 |
+
}
|
| 2311 |
+
|
| 2312 |
+
char = JSON.parse(JSON.stringify(defaultChar));
|
| 2313 |
+
db.customCharacters = db.customCharacters || [];
|
| 2314 |
+
db.customCharacters.push(char);
|
| 2315 |
+
}
|
| 2316 |
+
|
| 2317 |
+
if (!char) return;
|
| 2318 |
+
|
| 2319 |
+
if (!char.image_url) {
|
| 2320 |
+
char.image_url = [];
|
| 2321 |
+
} else if (!Array.isArray(char.image_url)) {
|
| 2322 |
+
char.image_url = [char.image_url];
|
| 2323 |
+
}
|
| 2324 |
+
|
| 2325 |
+
for (const p of photos) {
|
| 2326 |
+
const imageObj = p.caption ? { file_id: p.fileId, caption: p.caption } : p.fileId;
|
| 2327 |
+
char.image_url.push(imageObj);
|
| 2328 |
+
}
|
| 2329 |
+
|
| 2330 |
+
saveDB(db);
|
| 2331 |
+
console.log(`[CMD] Added ${photos.length} images to ${char.name} (ID: ${char.id}). Total images: ${char.image_url.length}`);
|
| 2332 |
+
|
| 2333 |
+
await bot.sendMessage(
|
| 2334 |
+
chatId,
|
| 2335 |
+
`✅ Added *${photos.length}* new images/skins to *${char.name}* (\`${char.id}\`)!\n` +
|
| 2336 |
+
`🖼️ Total images: *${char.image_url.length}*`,
|
| 2337 |
+
{ parse_mode: "Markdown" }
|
| 2338 |
+
);
|
| 2339 |
+
}
|
| 2340 |
+
|
| 2341 |
+
// /connect (in group chat or private chat)
|
| 2342 |
+
bot.onText(/\/connect$/, async (msg) => {
|
| 2343 |
+
const chatId = msg.chat.id;
|
| 2344 |
+
const userId = String(msg.from.id);
|
| 2345 |
+
console.log(`[CMD] "/connect" invoked by user ID: ${userId} in chat: ${chatId}`);
|
| 2346 |
+
|
| 2347 |
+
if (!isAdmin(userId)) {
|
| 2348 |
+
console.log(`[CMD] Permission denied for "/connect" by user: ${userId}`);
|
| 2349 |
+
return bot.sendMessage(chatId, "⛔ Only admins can connect chats.");
|
| 2350 |
+
}
|
| 2351 |
+
|
| 2352 |
+
if (msg.chat.type === "private") {
|
| 2353 |
+
return bot.sendMessage(
|
| 2354 |
+
chatId,
|
| 2355 |
+
"ℹ️ *How to connect a group chat:*\n\n" +
|
| 2356 |
+
"1. Go to your Telegram group where the bot is added.\n" +
|
| 2357 |
+
"2. Type the `/connect` command in that group.\n" +
|
| 2358 |
+
"3. Copy the `/connect -100xxxxxxxxxx` command the bot gives you.\n" +
|
| 2359 |
+
"4. Paste it here in my private chat!",
|
| 2360 |
+
{ parse_mode: "Markdown" }
|
| 2361 |
+
);
|
| 2362 |
+
}
|
| 2363 |
+
|
| 2364 |
+
const botUser = await bot.getMe();
|
| 2365 |
+
const botUsername = botUser.username;
|
| 2366 |
+
|
| 2367 |
+
await bot.sendMessage(
|
| 2368 |
+
chatId,
|
| 2369 |
+
`🔌 *Connect Chat (Admin)*\n\n` +
|
| 2370 |
+
`To connect this group to your private DM and manage it cleanly:\n\n` +
|
| 2371 |
+
`1. Click here: @${botUsername}\n` +
|
| 2372 |
+
`2. Send this command in the private chat:\n` +
|
| 2373 |
+
`\`/connect ${chatId}\``,
|
| 2374 |
+
{ parse_mode: "Markdown" }
|
| 2375 |
+
);
|
| 2376 |
+
});
|
| 2377 |
+
|
| 2378 |
+
// /connect [groupId] (in private DM)
|
| 2379 |
+
bot.onText(/\/connect\s+(-\d+)/, async (msg, match) => {
|
| 2380 |
+
const chatId = msg.chat.id;
|
| 2381 |
+
const userId = String(msg.from.id);
|
| 2382 |
+
const targetGroupId = match[1].trim();
|
| 2383 |
+
console.log(`[CMD] "/connect ${targetGroupId}" invoked by admin ID: ${userId} in chat: ${chatId}`);
|
| 2384 |
+
|
| 2385 |
+
if (!isAdmin(userId)) {
|
| 2386 |
+
console.log(`[CMD] Permission denied for "/connect ${targetGroupId}" by user: ${userId}`);
|
| 2387 |
+
return bot.sendMessage(chatId, "⛔ Only admins can connect chats.");
|
| 2388 |
+
}
|
| 2389 |
+
|
| 2390 |
+
if (msg.chat.type !== "private") {
|
| 2391 |
+
return bot.sendMessage(chatId, "⚠️ Please run this command in my private chat.");
|
| 2392 |
+
}
|
| 2393 |
+
|
| 2394 |
+
try {
|
| 2395 |
+
// Verify bot membership in the chat
|
| 2396 |
+
await bot.getChat(targetGroupId);
|
| 2397 |
+
|
| 2398 |
+
const db = loadDB();
|
| 2399 |
+
db.connections = db.connections || {};
|
| 2400 |
+
db.connections[userId] = targetGroupId;
|
| 2401 |
+
saveDB(db);
|
| 2402 |
+
|
| 2403 |
+
console.log(`[CMD] Successfully connected group chat ID ${targetGroupId} to admin ID: ${userId}`);
|
| 2404 |
+
|
| 2405 |
+
await bot.sendMessage(
|
| 2406 |
+
chatId,
|
| 2407 |
+
`✅ *Chat Connected successfully!*\n\n` +
|
| 2408 |
+
`• *Admin:* @${msg.from.username || msg.from.first_name}\n` +
|
| 2409 |
+
`• *Connected Group Chat ID:* \`${targetGroupId}\`\n\n` +
|
| 2410 |
+
`From now on, all group-specific admin operations run in this DM (like \`/cancelsession\` and \`/adminsession\`) will apply to that group chat!`,
|
| 2411 |
+
{ parse_mode: "Markdown" }
|
| 2412 |
+
);
|
| 2413 |
+
} catch (err) {
|
| 2414 |
+
console.error("❌ Error verifying group chat connection:", err);
|
| 2415 |
+
await bot.sendMessage(
|
| 2416 |
+
chatId,
|
| 2417 |
+
`❌ *Connection Failed!*\n\n` +
|
| 2418 |
+
`Make sure that:\n` +
|
| 2419 |
+
`1. The group ID \`${targetGroupId}\` is correct.\n` +
|
| 2420 |
+
`2. The bot is already added to that group as an administrator.`,
|
| 2421 |
+
{ parse_mode: "Markdown" }
|
| 2422 |
+
);
|
| 2423 |
+
}
|
| 2424 |
+
});
|
| 2425 |
+
|
| 2426 |
+
// Handle in-group character selection callbacks
|
| 2427 |
+
bot.on("callback_query", async (query) => {
|
| 2428 |
+
const data = query.data;
|
| 2429 |
+
const clickerId = String(query.from.id);
|
| 2430 |
+
const clickerUsername = query.from.username
|
| 2431 |
+
? `@${query.from.username}`
|
| 2432 |
+
: query.from.first_name || `User${clickerId}`;
|
| 2433 |
+
const chatId = query.message.chat.id;
|
| 2434 |
+
|
| 2435 |
+
if (data.startsWith("sel:")) {
|
| 2436 |
+
// Format: sel:[groupId]:[ownerId]:[characterId]
|
| 2437 |
+
const parts = data.split(":");
|
| 2438 |
+
const groupId = parts[1];
|
| 2439 |
+
const ownerId = parts[2];
|
| 2440 |
+
const charId = parts[3];
|
| 2441 |
+
|
| 2442 |
+
console.log(`[CALLBACK] Selection toggle callback query: user=${clickerUsername} (ID: ${clickerId}), target_player_menu=${ownerId}, character_id=${charId}, group=${groupId}`);
|
| 2443 |
+
|
| 2444 |
+
if (clickerId !== ownerId) {
|
| 2445 |
+
return bot.answerCallbackQuery(query.id, {
|
| 2446 |
+
text: `⚠️ This selection menu is only for the player who requested it!`,
|
| 2447 |
+
show_alert: true
|
| 2448 |
+
});
|
| 2449 |
+
}
|
| 2450 |
+
|
| 2451 |
+
const session = getSession(groupId);
|
| 2452 |
+
if (!session || session.phase !== "waiting") {
|
| 2453 |
+
return bot.answerCallbackQuery(query.id, { text: "⚠️ No active signup lobby in that group." });
|
| 2454 |
+
}
|
| 2455 |
+
|
| 2456 |
+
session.userSelections = session.userSelections || {};
|
| 2457 |
+
session.userSelections[ownerId] = session.userSelections[ownerId] || new Set();
|
| 2458 |
+
|
| 2459 |
+
const userSet = session.userSelections[ownerId];
|
| 2460 |
+
if (userSet.has(charId)) {
|
| 2461 |
+
userSet.delete(charId);
|
| 2462 |
+
console.log(`[CALLBACK] Deselected character: ${charId} for user ${clickerUsername}`);
|
| 2463 |
+
} else {
|
| 2464 |
+
// Check if character is already selected by another user
|
| 2465 |
+
let alreadySelectedBy = null;
|
| 2466 |
+
for (const [uid, charSet] of Object.entries(session.userSelections)) {
|
| 2467 |
+
if (uid !== ownerId && charSet instanceof Set && charSet.has(charId)) {
|
| 2468 |
+
alreadySelectedBy = session.readyUsers.get(uid) || `User ${uid}`;
|
| 2469 |
+
break;
|
| 2470 |
+
}
|
| 2471 |
+
}
|
| 2472 |
+
if (alreadySelectedBy) {
|
| 2473 |
+
console.log(`[CALLBACK] Selection rejected: Character ${charId} already taken by user ${alreadySelectedBy}`);
|
| 2474 |
+
return bot.answerCallbackQuery(query.id, {
|
| 2475 |
+
text: `⚠️ This actress is already selected by ${alreadySelectedBy}! Please select another.`,
|
| 2476 |
+
show_alert: true
|
| 2477 |
+
});
|
| 2478 |
+
}
|
| 2479 |
+
// Clear previous selection as each player can select only one actress
|
| 2480 |
+
userSet.clear();
|
| 2481 |
+
userSet.add(charId);
|
| 2482 |
+
console.log(`[CALLBACK] Selected character: ${charId} for user ${clickerUsername}`);
|
| 2483 |
+
}
|
| 2484 |
+
|
| 2485 |
+
// Rebuild updated inline keyboard with new checkmarks
|
| 2486 |
+
const db = loadDB();
|
| 2487 |
+
const allChars = getAllCharacters(db);
|
| 2488 |
+
const updatedKeyboard = buildSelectionKeyboard(groupId, ownerId, allChars, userSet);
|
| 2489 |
+
|
| 2490 |
+
try {
|
| 2491 |
+
await bot.editMessageReplyMarkup(
|
| 2492 |
+
{ inline_keyboard: updatedKeyboard },
|
| 2493 |
+
{ chat_id: chatId, message_id: query.message.message_id }
|
| 2494 |
+
);
|
| 2495 |
+
} catch (_) { }
|
| 2496 |
+
|
| 2497 |
+
bot.answerCallbackQuery(query.id, { text: "Toggled selection!" });
|
| 2498 |
+
}
|
| 2499 |
+
|
| 2500 |
+
if (data.startsWith("done:")) {
|
| 2501 |
+
// Format: done:[groupId]:[ownerId]
|
| 2502 |
+
const parts = data.split(":");
|
| 2503 |
+
const groupId = parts[1];
|
| 2504 |
+
const ownerId = parts[2];
|
| 2505 |
+
|
| 2506 |
+
console.log(`[CALLBACK] Finalize selections callback query: user=${clickerUsername} (ID: ${clickerId}), target_player_menu=${ownerId}, group=${groupId}`);
|
| 2507 |
+
|
| 2508 |
+
if (clickerId !== ownerId) {
|
| 2509 |
+
return bot.answerCallbackQuery(query.id, {
|
| 2510 |
+
text: `⚠️ This selection menu is only for the player who requested it!`,
|
| 2511 |
+
show_alert: true
|
| 2512 |
+
});
|
| 2513 |
+
}
|
| 2514 |
+
|
| 2515 |
+
const session = getSession(groupId);
|
| 2516 |
+
if (!session || session.phase !== "waiting") {
|
| 2517 |
+
return bot.answerCallbackQuery(query.id, { text: "⚠️ No active signup lobby in that group." });
|
| 2518 |
+
}
|
| 2519 |
+
|
| 2520 |
+
const userSet = session.userSelections && session.userSelections[ownerId];
|
| 2521 |
+
if (!userSet || userSet.size === 0) {
|
| 2522 |
+
return bot.answerCallbackQuery(query.id, {
|
| 2523 |
+
text: `⚠️ You must select at least one actress before clicking Done!`,
|
| 2524 |
+
show_alert: true
|
| 2525 |
+
});
|
| 2526 |
+
}
|
| 2527 |
+
|
| 2528 |
+
session.userSelectionsDone = session.userSelectionsDone || {};
|
| 2529 |
+
session.userSelectionsDone[ownerId] = true;
|
| 2530 |
+
console.log(`[CALLBACK] User ${clickerUsername} finalized selections:`, Array.from(userSet));
|
| 2531 |
+
|
| 2532 |
+
try {
|
| 2533 |
+
await bot.editMessageText(
|
| 2534 |
+
`✅ ${clickerUsername} has finalized their actress selections!`,
|
| 2535 |
+
{
|
| 2536 |
+
chat_id: chatId,
|
| 2537 |
+
message_id: query.message.message_id,
|
| 2538 |
+
parse_mode: "Markdown"
|
| 2539 |
+
}
|
| 2540 |
+
);
|
| 2541 |
+
} catch (_) { }
|
| 2542 |
+
|
| 2543 |
+
bot.answerCallbackQuery(query.id, { text: "Selections saved!" });
|
| 2544 |
+
|
| 2545 |
+
// Check if lobby is ready and start bidding!
|
| 2546 |
+
await checkLobbyReadyAndStart(groupId);
|
| 2547 |
+
}
|
| 2548 |
+
});
|
| 2549 |
+
|
| 2550 |
+
// Update group member tags for top 5 players based on global wins
|
| 2551 |
+
async function updateGroupTagsForTopUsers(chatId) {
|
| 2552 |
+
try {
|
| 2553 |
+
console.log(`[TAGS] Updating group tags for chat: ${chatId}`);
|
| 2554 |
+
const db = loadDB();
|
| 2555 |
+
const users = Object.entries(db.users || {});
|
| 2556 |
+
if (users.length === 0) {
|
| 2557 |
+
console.log(`[TAGS] No registered users in DB.`);
|
| 2558 |
+
return;
|
| 2559 |
+
}
|
| 2560 |
+
|
| 2561 |
+
// Sort globally by wins
|
| 2562 |
+
const sorted = users.sort((a, b) => b[1].totalWins - a[1].totalWins);
|
| 2563 |
+
|
| 2564 |
+
const promises = sorted.map(async ([uid, userObj], idx) => {
|
| 2565 |
+
const rank = idx + 1;
|
| 2566 |
+
const isTop5 = rank <= 5;
|
| 2567 |
+
const targetTag = isTop5 ? `Rank ${rank}` : "";
|
| 2568 |
+
|
| 2569 |
+
try {
|
| 2570 |
+
await bot.setChatMemberTag(chatId, Number(uid), { tag: targetTag });
|
| 2571 |
+
} catch (err) {
|
| 2572 |
+
// Silently ignore failures (e.g. USER_NOT_PARTICIPANT or CHAT_ADMIN_REQUIRED)
|
| 2573 |
+
}
|
| 2574 |
+
});
|
| 2575 |
+
|
| 2576 |
+
await Promise.allSettled(promises);
|
| 2577 |
+
console.log(`[TAGS] Rank tags updated successfully for group: ${chatId}`);
|
| 2578 |
+
} catch (err) {
|
| 2579 |
+
console.error(`[TAGS] Error in updateGroupTagsForTopUsers:`, err);
|
| 2580 |
+
}
|
| 2581 |
+
}
|
| 2582 |
+
|
| 2583 |
+
console.log("🤖 Auction Bot is running...");
|
logger.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require("fs");
|
| 2 |
+
const path = require("path");
|
| 3 |
+
|
| 4 |
+
const logDir = path.join(__dirname, "logs");
|
| 5 |
+
if (!fs.existsSync(logDir)) {
|
| 6 |
+
fs.mkdirSync(logDir, { recursive: true });
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const logFile = path.join(logDir, "combined.log");
|
| 10 |
+
const logStream = fs.createWriteStream(logFile, { flags: "a", encoding: "utf8" });
|
| 11 |
+
|
| 12 |
+
function formatMessage(level, args) {
|
| 13 |
+
const timestamp = new Date().toISOString();
|
| 14 |
+
const message = args.map((arg) => {
|
| 15 |
+
if (arg instanceof Error) {
|
| 16 |
+
return arg.stack || arg.message || String(arg);
|
| 17 |
+
}
|
| 18 |
+
if (typeof arg === "object" && arg !== null) {
|
| 19 |
+
try {
|
| 20 |
+
return JSON.stringify(arg, null, 2);
|
| 21 |
+
} catch (e) {
|
| 22 |
+
return String(arg);
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
return String(arg);
|
| 26 |
+
}).join(" ");
|
| 27 |
+
|
| 28 |
+
return `[${timestamp}] [${level}] ${message}\n`;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Keep original references
|
| 32 |
+
const originalLog = console.log;
|
| 33 |
+
const originalError = console.error;
|
| 34 |
+
const originalWarn = console.warn;
|
| 35 |
+
const originalInfo = console.info;
|
| 36 |
+
|
| 37 |
+
console.log = function (...args) {
|
| 38 |
+
originalLog.apply(console, args);
|
| 39 |
+
logStream.write(formatMessage("INFO", args));
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
console.error = function (...args) {
|
| 43 |
+
originalError.apply(console, args);
|
| 44 |
+
logStream.write(formatMessage("ERROR", args));
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
console.warn = function (...args) {
|
| 48 |
+
originalWarn.apply(console, args);
|
| 49 |
+
logStream.write(formatMessage("WARN", args));
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
console.info = function (...args) {
|
| 53 |
+
originalInfo.apply(console, args);
|
| 54 |
+
logStream.write(formatMessage("INFO", args));
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
console.debug = function (...args) {
|
| 58 |
+
originalLog.apply(console, ["DEBUG:", ...args]);
|
| 59 |
+
logStream.write(formatMessage("DEBUG", args));
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
module.exports = {
|
| 63 |
+
logStream,
|
| 64 |
+
};
|
migrate_to_mongo.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require("dotenv").config();
|
| 2 |
+
const fs = require("fs");
|
| 3 |
+
const path = require("path");
|
| 4 |
+
const { MongoClient } = require("mongodb");
|
| 5 |
+
|
| 6 |
+
const DB_FILE = path.join(__dirname, "db.json");
|
| 7 |
+
|
| 8 |
+
async function migrate() {
|
| 9 |
+
const uri = process.env.MONGODB_URI;
|
| 10 |
+
if (!uri) {
|
| 11 |
+
console.error("❌ MONGODB_URI not set in .env file!");
|
| 12 |
+
process.exit(1);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
if (!fs.existsSync(DB_FILE)) {
|
| 16 |
+
console.log("ℹ️ No local db.json file found. Nothing to migrate.");
|
| 17 |
+
process.exit(0);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
let localData;
|
| 21 |
+
try {
|
| 22 |
+
localData = JSON.parse(fs.readFileSync(DB_FILE, "utf8"));
|
| 23 |
+
console.log("✅ Loaded local db.json successfully.");
|
| 24 |
+
} catch (err) {
|
| 25 |
+
console.error("❌ Failed to read or parse local db.json:", err.message);
|
| 26 |
+
process.exit(1);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
let client;
|
| 30 |
+
try {
|
| 31 |
+
console.log("Connecting to MongoDB...");
|
| 32 |
+
client = new MongoClient(uri);
|
| 33 |
+
await client.connect();
|
| 34 |
+
const db = client.db();
|
| 35 |
+
const collection = db.collection("bot_state");
|
| 36 |
+
|
| 37 |
+
console.log("Migrating state to MongoDB...");
|
| 38 |
+
await collection.updateOne(
|
| 39 |
+
{ _id: "state" },
|
| 40 |
+
{ $set: { data: localData } },
|
| 41 |
+
{ upsert: true }
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
console.log("🎉 Migration completed successfully!");
|
| 45 |
+
} catch (err) {
|
| 46 |
+
console.error("❌ Migration failed:", err.message || err);
|
| 47 |
+
} finally {
|
| 48 |
+
if (client) {
|
| 49 |
+
await client.close();
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
migrate();
|
package-lock.json
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "telegram-bid-bot",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "telegram-bid-bot",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"license": "ISC",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"dotenv": "^17.4.2",
|
| 13 |
+
"mongodb": "^6.8.0",
|
| 14 |
+
"node-telegram-bot-api": "^1.1.2"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"node_modules/@mongodb-js/saslprep": {
|
| 18 |
+
"version": "1.4.12",
|
| 19 |
+
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz",
|
| 20 |
+
"integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==",
|
| 21 |
+
"license": "MIT",
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"sparse-bitfield": "^3.0.3"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"node_modules/@types/webidl-conversions": {
|
| 27 |
+
"version": "7.0.3",
|
| 28 |
+
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
| 29 |
+
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
| 30 |
+
"license": "MIT"
|
| 31 |
+
},
|
| 32 |
+
"node_modules/@types/whatwg-url": {
|
| 33 |
+
"version": "11.0.5",
|
| 34 |
+
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
| 35 |
+
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
| 36 |
+
"license": "MIT",
|
| 37 |
+
"dependencies": {
|
| 38 |
+
"@types/webidl-conversions": "*"
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"node_modules/bson": {
|
| 42 |
+
"version": "6.10.4",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
|
| 44 |
+
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
|
| 45 |
+
"license": "Apache-2.0",
|
| 46 |
+
"engines": {
|
| 47 |
+
"node": ">=16.20.1"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"node_modules/dotenv": {
|
| 51 |
+
"version": "17.4.2",
|
| 52 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
| 53 |
+
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
| 54 |
+
"license": "BSD-2-Clause",
|
| 55 |
+
"engines": {
|
| 56 |
+
"node": ">=12"
|
| 57 |
+
},
|
| 58 |
+
"funding": {
|
| 59 |
+
"url": "https://dotenvx.com"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"node_modules/memory-pager": {
|
| 63 |
+
"version": "1.5.0",
|
| 64 |
+
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
| 65 |
+
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
| 66 |
+
"license": "MIT"
|
| 67 |
+
},
|
| 68 |
+
"node_modules/mongodb": {
|
| 69 |
+
"version": "6.21.0",
|
| 70 |
+
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz",
|
| 71 |
+
"integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==",
|
| 72 |
+
"license": "Apache-2.0",
|
| 73 |
+
"dependencies": {
|
| 74 |
+
"@mongodb-js/saslprep": "^1.3.0",
|
| 75 |
+
"bson": "^6.10.4",
|
| 76 |
+
"mongodb-connection-string-url": "^3.0.2"
|
| 77 |
+
},
|
| 78 |
+
"engines": {
|
| 79 |
+
"node": ">=16.20.1"
|
| 80 |
+
},
|
| 81 |
+
"peerDependencies": {
|
| 82 |
+
"@aws-sdk/credential-providers": "^3.188.0",
|
| 83 |
+
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
| 84 |
+
"gcp-metadata": "^5.2.0",
|
| 85 |
+
"kerberos": "^2.0.1",
|
| 86 |
+
"mongodb-client-encryption": ">=6.0.0 <7",
|
| 87 |
+
"snappy": "^7.3.2",
|
| 88 |
+
"socks": "^2.7.1"
|
| 89 |
+
},
|
| 90 |
+
"peerDependenciesMeta": {
|
| 91 |
+
"@aws-sdk/credential-providers": {
|
| 92 |
+
"optional": true
|
| 93 |
+
},
|
| 94 |
+
"@mongodb-js/zstd": {
|
| 95 |
+
"optional": true
|
| 96 |
+
},
|
| 97 |
+
"gcp-metadata": {
|
| 98 |
+
"optional": true
|
| 99 |
+
},
|
| 100 |
+
"kerberos": {
|
| 101 |
+
"optional": true
|
| 102 |
+
},
|
| 103 |
+
"mongodb-client-encryption": {
|
| 104 |
+
"optional": true
|
| 105 |
+
},
|
| 106 |
+
"snappy": {
|
| 107 |
+
"optional": true
|
| 108 |
+
},
|
| 109 |
+
"socks": {
|
| 110 |
+
"optional": true
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"node_modules/mongodb-connection-string-url": {
|
| 115 |
+
"version": "3.0.2",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
| 117 |
+
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
| 118 |
+
"license": "Apache-2.0",
|
| 119 |
+
"dependencies": {
|
| 120 |
+
"@types/whatwg-url": "^11.0.2",
|
| 121 |
+
"whatwg-url": "^14.1.0 || ^13.0.0"
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
"node_modules/node-telegram-bot-api": {
|
| 125 |
+
"version": "1.1.2",
|
| 126 |
+
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-1.1.2.tgz",
|
| 127 |
+
"integrity": "sha512-PtLczp0WZCQz7LHsObl1WIyfMSTco58cfYXITqB4Fbeuybd/A/sM729h892fbcjz+szJ9lpu+/pfewUs9NObrg==",
|
| 128 |
+
"license": "MIT",
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=18"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"node_modules/punycode": {
|
| 134 |
+
"version": "2.3.1",
|
| 135 |
+
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
| 136 |
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
| 137 |
+
"license": "MIT",
|
| 138 |
+
"engines": {
|
| 139 |
+
"node": ">=6"
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"node_modules/sparse-bitfield": {
|
| 143 |
+
"version": "3.0.3",
|
| 144 |
+
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
| 145 |
+
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
| 146 |
+
"license": "MIT",
|
| 147 |
+
"dependencies": {
|
| 148 |
+
"memory-pager": "^1.0.2"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"node_modules/tr46": {
|
| 152 |
+
"version": "5.1.1",
|
| 153 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
| 154 |
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
| 155 |
+
"license": "MIT",
|
| 156 |
+
"dependencies": {
|
| 157 |
+
"punycode": "^2.3.1"
|
| 158 |
+
},
|
| 159 |
+
"engines": {
|
| 160 |
+
"node": ">=18"
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
"node_modules/webidl-conversions": {
|
| 164 |
+
"version": "7.0.0",
|
| 165 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
| 166 |
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
| 167 |
+
"license": "BSD-2-Clause",
|
| 168 |
+
"engines": {
|
| 169 |
+
"node": ">=12"
|
| 170 |
+
}
|
| 171 |
+
},
|
| 172 |
+
"node_modules/whatwg-url": {
|
| 173 |
+
"version": "14.2.0",
|
| 174 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
| 175 |
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"dependencies": {
|
| 178 |
+
"tr46": "^5.1.0",
|
| 179 |
+
"webidl-conversions": "^7.0.0"
|
| 180 |
+
},
|
| 181 |
+
"engines": {
|
| 182 |
+
"node": ">=18"
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "telegram-bid-bot",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 8 |
+
},
|
| 9 |
+
"keywords": [],
|
| 10 |
+
"author": "",
|
| 11 |
+
"license": "ISC",
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"dotenv": "^17.4.2",
|
| 14 |
+
"mongodb": "^6.8.0",
|
| 15 |
+
"node-telegram-bot-api": "^1.1.2"
|
| 16 |
+
}
|
| 17 |
+
}
|
response.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
|
| 6 |
+
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
|
| 7 |
+
<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>
|
| 8 |
+
<script async src=\"https://simgbb.com/5286/ibb.js\"></script>
|
| 9 |
+
<title>Add DF Logo to Image</title>
|
| 10 |
+
</head>
|
| 11 |
+
|
| 12 |
+
<body>
|
| 13 |
+
<div class=\"container mt-3\">
|
| 14 |
+
<div class=\"mt-4 p-5 bg-primary text-white rounded\">
|
| 15 |
+
<h1>Add DF Logo</h1>
|
| 16 |
+
<p>Easily add DF Logo to your edits. Upload an image and get the logo added ! Just like that...</p>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
<div class=\"container mt-3\">
|
| 20 |
+
<h2>Result</h2> <code class=\"user-select-all\" data-bs-toggle=\"tooltip\" title=\"Click to select
|
| 21 |
+
all\">[url=https://ibb.co/5dCbjN7][img]https://i.ibb.co/DNm0Mxq/6f2af84fcbfde16dfc3e5dcb46411e14.jpg[/img][/url]</code>
|
| 22 |
+
<textarea id=\"uploaded-embed-code-0\"
|
| 23 |
+
style=\"display:none\">[url=https://ibb.co/5dCbjN7][img]https://i.ibb.co/DNm0Mxq/6f2af84fcbfde16dfc3e5dcb46411e14.jpg[/img][/url]</textarea>
|
| 24 |
+
<br /> <br /> Want to try one more? <a href=\"?\">Click here</a>
|
| 25 |
+
</div>
|
| 26 |
+
</body>
|
| 27 |
+
<script defer=\"\" src=\"https://simgbb.com/5286/jquery2.js\" onload=\"jQueryLoaded(this, event)\"></script>
|
| 28 |
+
<script>document.documentElement.className +=\" js\";var devices=[\"phone\",\"phablet\",\"tablet\",\"laptop\",\"desktop\",\"largescreen\"],window_to_device=function(){for(var e=[480,768,992,1200,1880,2180],t=[],n=\"\",d=document.documentElement.clientWidth||document.getElementsByTagName(\"body\")[0].clientWidth||window.innerWidth,c=0;c<devices.length;++c)d>=e[c]&&t.push(devices[c]);for(0==t.length&&t.push(devices[0]),n=t[t.length-1],c=0;c<devices.length;++c)document.documentElement.className=document.documentElement.className.replace(devices[c],\"\"),c==devices.length-1&&(document.documentElement.className+=\" \"+n),document.documentElement.className=document.documentElement.className.replace(/\\s+/g,\" \");if(\"laptop\"==n||\"desktop\"==n){var o=document.getElementById(\"pop-box-mask\");null!==o&&o.parentNode.removeChild(o)}};window_to_device(),window.onresize=window_to_device;function jQueryLoaded(){!function(n,d){n.each(readyQ,function(d,e){n(e)}),n.each(bindReadyQ,function(e,i){n(d).bind(\"ready\",i)})}(jQuery,document)}!function(n,d,e){function i(d,e){\"ready\"==d?n.bindReadyQ.push(e):n.readyQ.push(d)}n.readyQ=[],n.bindReadyQ=[];var u={ready:i,bind:i};n.$=n.jQuery=function(n){return n===d||void 0===n?u:void i(n)}}(window,document);const copyToClipboard = async () => { try { const element = document.querySelector(\".user-select-all\"); await navigator.clipboard.writeText(element.textContent); console.log(\"Text copied to clipboard!\"); /*Optional: Display a success message to the user*/ } catch (error) { console.error(\"Failed to copy to clipboard:\", error); /*Optional: Display an error message to the user*/ }};//window.close();document.getElementById(\"ibbjs\").addEventListener(\"load\", function () {PF.obj.devices = window.devices;PF.fn.window_to_device = window.window_to_device;PF.obj.config.base_url=\"https://imgbb.com\";PF.obj.config.json_api=\"https://imgbb.com/json\";PF.obj.config.listing.items_per_page=\"32\";PF.obj.config.auth_token=\"b66a116304254b9d7eea1462a703b76b700f0857\";PF.obj.l10n=[];CHV.obj.embed_tpl={\"links\":{\"label\":\"Links\",\"options\":{\"viewer-links\":{\"label\":\"Viewer links\",\"template\":\"%URL_VIEWER%\",\"size\":\"viewer\"},\"direct-links\":{\"label\":\"Direct links\",\"template\":\"%URL%\",\"size\":\"full\"}}},\"html-codes\":{\"label\":\"HTML Codes\",\"options\":{\"html-embed\":{\"label\":\"HTML image\",\"template\":\"<img src=\\\"%URL%\\\" alt=\\\"%NAME%\\\" border=\\\"0\\\">\",\"size\":\"full\"},\"html-embed-full\":{\"label\":\"HTML full linked\",\"template\":\"<a href=\\\"%URL_VIEWER%\\\"><img src=\\\"%URL%\\\" alt=\\\"%NAME%\\\" border=\\\"0\\\"><\\/a>\",\"size\":\"full\"},\"html-embed-medium\":{\"label\":\"HTML medium linked\",\"template\":\"<a href=\\\"%URL_VIEWER_PREVIEW%\\\"><img src=\\\"%MEDIUM_URL%\\\" alt=\\\"%MEDIUM_NAME%\\\" border=\\\"0\\\"><\\/a>\",\"size\":\"medium\"},\"html-embed-thumbnail\":{\"label\":\"HTML thumbnail linked\",\"template\":\"<a href=\\\"%URL_VIEWER_THUMB%\\\"><img src=\\\"%THUMB_URL%\\\" alt=\\\"%THUMB_NAME%\\\" border=\\\"0\\\"><\\/a>\",\"size\":\"thumb\"}}},\"bbcodes\":{\"label\":\"BBCodes\",\"options\":{\"bbcode-embed\":{\"label\":\"BBCode full\",\"template\":\"[img]%URL%[\\/img]\",\"size\":\"full\"},\"bbcode-embed-full\":{\"label\":\"BBCode full linked\",\"template\":\"[url=%URL_VIEWER%][img]%URL%[\\/img][\\/url]\",\"size\":\"full\"},\"bbcode-embed-medium\":{\"label\":\"BBCode medium linked\",\"template\":\"[url=%URL_VIEWER_PREVIEW%][img]%MEDIUM_URL%[\\/img][\\/url]\",\"size\":\"medium\"},\"bbcode-embed-thumbnail\":{\"label\":\"BBCode thumbnail linked\",\"template\":\"[url=%URL_VIEWER_THUMB%][img]%THUMB_URL%[\\/img][\\/url]\",\"size\":\"thumb\"}}}};CHV.obj.config={\"image\":{\"max_filesize\":32000000,\"load_max_filesize\":8000000},\"upload\":{\"image_types\":[\"jpg\",\"png\",\"bmp\",\"gif\",\"tif\",\"webp\",\"heic\",\"avif\",\"pdf\",\"jpeg\",\"tiff\",\"heif\"]},\"listing\":{\"viewer\":true},\"user\":{\"avatar_max_filesize\":1000000,\"background_max_filesize\":2000000}};CHV.obj.page_info={\"doctitle\":\"Upload your images\",\"pre_doctitle\":\"\"};CHV.obj.logged_user = {\"name\":\"SD Learner2512\",\"username\":\"sd-learner2512\",\"id\":\"XyYcY8\",\"url\":\"https:\\/\\/sd-learner2512.imgbb.com\\/\",\"url_albums\":\"https:\\/\\/sd-learner2512.imgbb.com\\/albums\"};ibb();});</script>
|
| 29 |
+
|
| 30 |
+
</html>
|