amanfor18 commited on
Commit
21011e9
·
verified ·
1 Parent(s): 64562e1

Upload 15 files

Browse files
Files changed (15) hide show
  1. Dockerfile +8 -0
  2. README.md +102 -8
  3. characters.csv +2 -0
  4. characters.js +7 -0
  5. db.js +97 -0
  6. db.json +544 -0
  7. docker-compose.yml +6 -0
  8. documentation.md +189 -0
  9. import_characters.js +213 -0
  10. index.js +2583 -0
  11. logger.js +64 -0
  12. migrate_to_mongo.js +54 -0
  13. package-lock.json +186 -0
  14. package.json +17 -0
  15. 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
- title: TeleAuctionBot
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>