Muttered3 commited on
Commit
476be0e
Β·
verified Β·
1 Parent(s): 1ed198d

Update bot.py

Browse files
Files changed (1) hide show
  1. bot.py +107 -84
bot.py CHANGED
@@ -4,9 +4,9 @@ import time
4
  import io
5
  import asyncio
6
  from telethon import events, Button
7
- from PIL import Image, ImageDraw, ImageFont
8
  import db
9
- import scraper # Required for the /check command
10
 
11
  def get_admins():
12
  return {int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x}
@@ -14,75 +14,99 @@ def get_admins():
14
  def is_admin(event):
15
  return event.sender_id in get_admins()
16
 
17
- # --- PROFESSIONAL UI MENUS ---
18
  def get_main_menu():
19
  return [
20
- [Button.inline("Start Scan", b"start_scan"), Button.inline("Pause", b"pause_scan"), Button.inline("Stop", b"stop_scan")],
21
- [Button.inline("Live Status", b"show_status"), Button.inline("Export Data", b"export_files")],
22
- [Button.inline("System Settings", b"menu_settings")]
23
  ]
24
 
25
  def get_settings_menu():
26
  return [
27
- [Button.inline("Load Database", b"load_words")],
28
- [Button.inline("Set Speed Limit", b"set_speed")],
29
- [Button.inline("Wipe Database", b"reset_confirm")],
30
- [Button.inline("« Back to Main Menu", b"back_main")]
31
  ]
32
 
33
- # --- IMAGE GENERATOR (LIQUID GLASS STYLE) ---
34
- def generate_status_image(username, status):
35
- # Base dark theme colors
36
- bg_color = (15, 23, 42) # Slate 900
37
- card_color = (30, 41, 59) # Slate 800 (Glass card)
38
- outline_color = (51, 65, 85) # Slate 700
39
- text_primary = (248, 250, 252) # Slate 50
40
 
41
- # Determine status color
42
- if status == "TAKEN":
43
- status_color = (239, 68, 68) # Red
44
- elif status == "UNAVAILABLE":
45
- status_color = (156, 163, 175) # Gray
46
- elif status == "FOR_SALE" or status == "ON_AUCTION":
47
- status_color = (59, 130, 246) # Blue
48
- else:
49
- status_color = (34, 197, 94) # Green (Available)
50
-
51
- # Create Image
52
- img = Image.new('RGB', (800, 400), color=bg_color)
53
- d = ImageDraw.Draw(img)
54
 
55
- # Draw "Glass" Card (Rounded Rectangle)
56
- d.rounded_rectangle([(50, 50), (750, 350)], radius=20, fill=card_color, outline=outline_color, width=3)
 
 
57
 
58
- # Status Indicator Dot
59
- d.ellipse([(90, 90), (110, 110)], fill=status_color)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # Text Setup (Using default font scaled up via ImageFont if custom isn't available)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  try:
63
- # Tries to load a standard Linux font, falls back to default
64
- font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 60)
65
- font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 30)
66
  except IOError:
67
- font_large = ImageFont.load_default()
68
- font_small = ImageFont.load_default()
69
 
70
  # Draw Text
71
- d.text((130, 75), "FRAGMENT STATUS REPORT", fill=outline_color, font=font_small)
72
- d.text((90, 160), f"@{username}", fill=text_primary, font=font_large)
 
 
 
 
 
 
 
73
 
74
- # Draw Status Pill
75
- d.rounded_rectangle([(90, 260), (350, 310)], radius=25, fill=bg_color, outline=status_color, width=2)
76
- d.text((120, 265), status, fill=status_color, font=font_small)
77
 
78
- # Save to buffer
79
  buf = io.BytesIO()
80
- img.save(buf, format='PNG')
81
  buf.seek(0)
82
  buf.name = f"{username}_status.png"
83
  return buf
84
 
85
- # --- MODERN STATUS DASHBOARD ---
86
  async def generate_status_msg():
87
  state = await db.get_state()
88
  counts = await db.get_counts()
@@ -93,16 +117,17 @@ async def generate_status_msg():
93
  processed = int(state.get("processed", 0))
94
  pct = (processed / total * 100) if total > 0 else 0
95
 
96
- run_str = "RUNNING" if state.get("running") == "1" else "STOPPED"
97
- if state.get("paused") == "1": run_str = "PAUSED"
98
 
 
99
  bar_length = 20
100
  filled = int(bar_length * (processed / total)) if total > 0 else 0
101
- bar = "Ò–ˆ" * filled + "Γ’β€“β€˜" * (bar_length - filled)
102
 
103
  msg = (
104
  f"**SYSTEM DASHBOARD**\n"
105
- f"Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€\n"
106
  f"**State:** `{run_str}`\n"
107
  f"**Queue:** `{qlen:,}` waiting\n"
108
  f"**Speed:** `{concurrency}` workers\n"
@@ -110,7 +135,7 @@ async def generate_status_msg():
110
  f"`[{bar}]`\n"
111
  f"`{processed:,} / {total:,}`\n\n"
112
  f"**DATABASE METRICS**\n"
113
- f"Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€\n"
114
  f"Taken : `{counts.get('taken', 0):,}`\n"
115
  f"Unavailable: `{counts.get('unavailable', 0):,}`\n"
116
  f"For Sale : `{counts.get('forsale', 0):,}`\n"
@@ -138,48 +163,48 @@ def setup_handlers(client):
138
  if not is_admin(event): return
139
  await event.respond("**Fragment Control System**\nSelect an operation:", buttons=get_main_menu())
140
 
141
- # --- NEW MANUAL CHECK FEATURE ---
142
  @client.on(events.NewMessage(pattern=r'/check\s+([a-zA-Z0-9_]+)'))
143
  async def check_cmd(event):
144
  if not is_admin(event): return
145
  username = event.pattern_match.group(1).lower()
146
 
147
- msg = await event.respond(f"Scanning `{username}`...")
148
 
149
- # Scrape data
150
  try:
 
151
  status = await scraper.check_fragment(username)
152
  if not status:
153
  status = "UNKNOWN"
154
  except Exception:
155
  status = "ERROR"
156
 
157
- # Generate sleek image
158
- img_buffer = await asyncio.to_thread(generate_status_image, username, status)
159
 
160
  # Send result
161
  await msg.delete()
162
  await client.send_file(
163
  event.chat_id,
164
  file=img_buffer,
165
- caption=f"**Target:** `@{username}`\n**Status:** `{status}`"
166
  )
167
 
168
  @client.on(events.NewMessage(pattern='/upload'))
169
  async def upload_cmd(event):
170
  if not is_admin(event): return
171
  if not event.is_reply:
172
- await event.respond("Reply to a `.txt` file with `/upload`.")
173
  return
174
 
175
  reply_msg = await event.get_reply_message()
176
  if not reply_msg.document or reply_msg.file.ext != '.txt':
177
- await event.respond("Target must be a `.txt` document.")
178
  return
179
 
180
- msg = await event.respond("Transferring to secure server storage...")
181
  await reply_msg.download_media(file="words.txt")
182
- await msg.edit("Transfer complete. Open **System Settings** -> **Load Database** to initialize.", buttons=get_main_menu())
183
 
184
  async def _load_words(event, is_callback=True):
185
  async def update_msg(text, buttons=None):
@@ -191,11 +216,11 @@ def setup_handlers(client):
191
  try:
192
  all_lines = await asyncio.to_thread(read_file_sync)
193
  except Exception:
194
- await update_msg("`words.txt` missing. Please use `/upload`.")
195
  return
196
 
197
  if not all_lines:
198
- await update_msg("Database is empty. Upload a new master list.")
199
  return
200
 
201
  CHUNK_SIZE = 200000
@@ -216,14 +241,14 @@ def setup_handlers(client):
216
  await pipe.execute()
217
  except Exception as e:
218
  if "oom" in str(e).lower() or "maxmemory" in str(e).lower():
219
- await update_msg("Memory limit exceeded. Please Wipe Database first.")
220
  return
221
 
222
  await asyncio.to_thread(write_file_sync, remaining_lines)
223
  await db.set_state(total=len(valid_words))
224
  skipped = len(valid_words) - len(to_queue)
225
 
226
- msg = f"**Database Loaded**\nÒ€’ Added: `{len(to_queue):,}`\nÒ€’ Skipped: `{skipped:,}`\nÒ€’ Disk Remaining: `{len(remaining_lines):,}`"
227
  await update_msg(msg, buttons=get_main_menu())
228
 
229
  @client.on(events.CallbackQuery())
@@ -234,45 +259,43 @@ def setup_handlers(client):
234
 
235
  data = event.data.decode('utf-8')
236
 
237
- # Menu Navigation
238
  if data == "back_main":
239
  await event.edit("**Fragment Control System**\nSelect an operation:", buttons=get_main_menu())
240
  elif data == "menu_settings":
241
  await event.edit("**System Settings**\nConfigure scanner variables:", buttons=get_settings_menu())
242
 
243
- # Logic
244
  elif data == "load_words":
245
- await event.edit("Processing chunk logic in background...")
246
  await _load_words(event, is_callback=True)
247
 
248
  elif data == "start_scan":
249
  await db.set_state(running="1", paused="0", start_time=str(time.time()))
250
- await event.edit("Scanner initialized and running.", buttons=get_main_menu())
251
 
252
  elif data == "pause_scan":
253
  state = await db.get_state()
254
  if state.get("paused") == "1":
255
  await db.set_state(paused="0")
256
- await event.edit("Scanner resumed.", buttons=get_main_menu())
257
  else:
258
  await db.set_state(paused="1")
259
- await event.edit("Scanner paused.", buttons=get_main_menu())
260
 
261
  elif data == "stop_scan":
262
  await db.set_state(running="0", paused="0")
263
- await event.edit("Scanner terminated.", buttons=get_main_menu())
264
 
265
  elif data in ("show_status", "refresh_status"):
266
  msg = await generate_status_msg()
267
- buttons = [[Button.inline("Refresh Data", b"refresh_status")], [Button.inline("« Back", b"back_main")]]
268
  try: await event.edit(msg, buttons=buttons)
269
  except Exception: pass
270
 
271
  elif data == "set_speed":
272
  buttons = [
273
- [Button.inline("Slow (10)", b"spd_10"), Button.inline("Normal (20)", b"spd_20")],
274
- [Button.inline("Fast (30)", b"spd_30"), Button.inline("Max (50)", b"spd_50")],
275
- [Button.inline("« Back", b"menu_settings")]
276
  ]
277
  await event.edit("**Thread Allocation**\nSelect maximum concurrent workers:", buttons=buttons)
278
 
@@ -280,12 +303,12 @@ def setup_handlers(client):
280
  try:
281
  speed_limit = int(data.split("_")[1])
282
  await db.set_concurrency(speed_limit)
283
- await event.edit(f"Thread limit updated to `{speed_limit}`.", buttons=get_settings_menu())
284
  except Exception:
285
  pass
286
 
287
  elif data == "export_files":
288
- await event.edit("Generating export packages...")
289
 
290
  taken = sorted(await db.get_all_taken())
291
  unavail = sorted(await db.get_all_unavailable())
@@ -299,13 +322,13 @@ def setup_handlers(client):
299
  if sold: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(sold).encode('utf-8')), caption="sold.txt")
300
  if forsale: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(forsale).encode('utf-8')), caption="forsale.txt")
301
 
302
- await event.edit("Export sequence complete.", buttons=get_main_menu())
303
 
304
  elif data == "reset_confirm":
305
- buttons = [[Button.inline("CONFIRM WIPE", b"reset_do")], [Button.inline("Cancel", b"menu_settings")]]
306
  await event.edit("**AUTHORIZATION REQUIRED**\n\nThis will purge the active Redis database. Confirm?", buttons=buttons)
307
 
308
  elif data == "reset_do":
309
- await event.edit("Purging memory...")
310
  await db.flush_all()
311
- await event.edit("Database wiped. Awaiting new chunk allocation.", buttons=get_settings_menu())
 
4
  import io
5
  import asyncio
6
  from telethon import events, Button
7
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
8
  import db
9
+ import scraper
10
 
11
  def get_admins():
12
  return {int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x}
 
14
  def is_admin(event):
15
  return event.sender_id in get_admins()
16
 
17
+ # --- PROFESSIONAL UI MENUS (Color-coded with emojis) ---
18
  def get_main_menu():
19
  return [
20
+ [Button.inline("▢️ Start Scan", b"start_scan"), Button.inline("⏸ Pause", b"pause_scan"), Button.inline("⏹ Stop", b"stop_scan")],
21
+ [Button.inline("πŸ“Š Live Status", b"show_status"), Button.inline("πŸ“₯ Export Data", b"export_files")],
22
+ [Button.inline("βš™οΈ System Settings", b"menu_settings")]
23
  ]
24
 
25
  def get_settings_menu():
26
  return [
27
+ [Button.inline("πŸ’Ύ Load Database", b"load_words")],
28
+ [Button.inline("⚑ Set Speed Limit", b"set_speed")],
29
+ [Button.inline("πŸ”΄ Wipe Database", b"reset_confirm")],
30
+ [Button.inline("Β« Back to Main Menu", b"back_main")]
31
  ]
32
 
33
+ # --- PRO LIQUID GLASS IMAGE GENERATOR ---
34
+ def generate_glass_image(username, status):
35
+ width, height = 800, 420
 
 
 
 
36
 
37
+ # 1. Base vibrant background (Deep Dark Theme)
38
+ base = Image.new('RGBA', (width, height), (15, 23, 42, 255))
39
+ draw_base = ImageDraw.Draw(base)
 
 
 
 
 
 
 
 
 
 
40
 
41
+ # Draw abstract colorful light orbs to simulate background glow
42
+ draw_base.ellipse([-100, -100, 350, 350], fill=(59, 130, 246, 255)) # Ocean Blue
43
+ draw_base.ellipse([450, 150, 950, 650], fill=(139, 92, 246, 255)) # Neon Purple
44
+ draw_base.ellipse([200, -150, 700, 250], fill=(14, 165, 233, 255)) # Cyan
45
 
46
+ # Blur the orbs to create a smooth, glowing ambient background
47
+ base = base.filter(ImageFilter.GaussianBlur(50))
48
+
49
+ # 2. Create the Frosted Glass Card
50
+ card_x1, card_y1, card_x2, card_y2 = 60, 60, 740, 360
51
+ glass = Image.new('RGBA', (width, height), (0, 0, 0, 0))
52
+ draw_glass = ImageDraw.Draw(glass)
53
+
54
+ # Semi-transparent white card (Alpha 25) with a slightly brighter border
55
+ draw_glass.rounded_rectangle(
56
+ [card_x1, card_y1, card_x2, card_y2],
57
+ radius=30,
58
+ fill=(255, 255, 255, 25),
59
+ outline=(255, 255, 255, 90),
60
+ width=2
61
+ )
62
 
63
+ # Composite the glass card directly over the glowing background
64
+ img = Image.alpha_composite(base, glass)
65
+ draw = ImageDraw.Draw(img)
66
+
67
+ # 3. Determine Status Colors for the Badge
68
+ if status == "TAKEN":
69
+ status_color = (239, 68, 68, 255) # Red
70
+ elif status == "UNAVAILABLE":
71
+ status_color = (156, 163, 175, 255) # Gray
72
+ elif status in ["FOR_SALE", "ON_AUCTION"]:
73
+ status_color = (59, 130, 246, 255) # Blue
74
+ elif status == "SOLD":
75
+ status_color = (245, 158, 11, 255) # Orange
76
+ else:
77
+ status_color = (34, 197, 94, 255) # Green
78
+
79
+ # 4. Typography Loading
80
  try:
81
+ font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64)
82
+ font_med = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32)
83
+ font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
84
  except IOError:
85
+ font_large = font_med = font_small = ImageFont.load_default()
 
86
 
87
  # Draw Text
88
+ draw.text((110, 100), "FRAGMENT INTELLIGENCE REPORT", fill=(226, 232, 240, 200), font=font_small)
89
+ draw.text((105, 140), f"@{username}", fill=(255, 255, 255, 255), font=font_large)
90
+
91
+ # Draw Status Pill / Badge
92
+ # Measure text to dynamically size the pill
93
+ text_bbox = draw.textbbox((0, 0), status, font=font_med)
94
+ text_width = text_bbox[2] - text_bbox[0]
95
+ badge_w = text_width + 60
96
+ badge_h = 56
97
 
98
+ # Render pill background and text
99
+ draw.rounded_rectangle([110, 250, 110 + badge_w, 250 + badge_h], radius=28, fill=status_color)
100
+ draw.text((140, 258), status, fill=(255, 255, 255, 255), font=font_med)
101
 
102
+ # Save to memory buffer
103
  buf = io.BytesIO()
104
+ img.convert('RGB').save(buf, format='PNG')
105
  buf.seek(0)
106
  buf.name = f"{username}_status.png"
107
  return buf
108
 
109
+ # --- ENCODING-SAFE MODERN DASHBOARD ---
110
  async def generate_status_msg():
111
  state = await db.get_state()
112
  counts = await db.get_counts()
 
117
  processed = int(state.get("processed", 0))
118
  pct = (processed / total * 100) if total > 0 else 0
119
 
120
+ run_str = "🟒 RUNNING" if state.get("running") == "1" else "⏹ STOPPED"
121
+ if state.get("paused") == "1": run_str = "⏸ PAUSED"
122
 
123
+ # Clean ASCII Progress bar (No weird encoding issues)
124
  bar_length = 20
125
  filled = int(bar_length * (processed / total)) if total > 0 else 0
126
+ bar = "=" * filled + "-" * (bar_length - filled)
127
 
128
  msg = (
129
  f"**SYSTEM DASHBOARD**\n"
130
+ f"━━━━━━━━━━━━━━━━━━━\n"
131
  f"**State:** `{run_str}`\n"
132
  f"**Queue:** `{qlen:,}` waiting\n"
133
  f"**Speed:** `{concurrency}` workers\n"
 
135
  f"`[{bar}]`\n"
136
  f"`{processed:,} / {total:,}`\n\n"
137
  f"**DATABASE METRICS**\n"
138
+ f"━━━━━━━━━━━━━━━━━━━\n"
139
  f"Taken : `{counts.get('taken', 0):,}`\n"
140
  f"Unavailable: `{counts.get('unavailable', 0):,}`\n"
141
  f"For Sale : `{counts.get('forsale', 0):,}`\n"
 
163
  if not is_admin(event): return
164
  await event.respond("**Fragment Control System**\nSelect an operation:", buttons=get_main_menu())
165
 
166
+ # --- ADVANCED MANUAL CHECK FEATURE ---
167
  @client.on(events.NewMessage(pattern=r'/check\s+([a-zA-Z0-9_]+)'))
168
  async def check_cmd(event):
169
  if not is_admin(event): return
170
  username = event.pattern_match.group(1).lower()
171
 
172
+ msg = await event.respond(f"πŸ” Executing deep scan on `@{username}`...")
173
 
 
174
  try:
175
+ # Uses the exact same robust scraper your queue workers use
176
  status = await scraper.check_fragment(username)
177
  if not status:
178
  status = "UNKNOWN"
179
  except Exception:
180
  status = "ERROR"
181
 
182
+ # Generate the Liquid Glass UI Card
183
+ img_buffer = await asyncio.to_thread(generate_glass_image, username, status)
184
 
185
  # Send result
186
  await msg.delete()
187
  await client.send_file(
188
  event.chat_id,
189
  file=img_buffer,
190
+ caption=f"**Target:** `@{username}`\n**Real-time Status:** `{status}`"
191
  )
192
 
193
  @client.on(events.NewMessage(pattern='/upload'))
194
  async def upload_cmd(event):
195
  if not is_admin(event): return
196
  if not event.is_reply:
197
+ await event.respond("⚠️ Reply to a `.txt` file with `/upload`.")
198
  return
199
 
200
  reply_msg = await event.get_reply_message()
201
  if not reply_msg.document or reply_msg.file.ext != '.txt':
202
+ await event.respond("⚠️ Target must be a `.txt` document.")
203
  return
204
 
205
+ msg = await event.respond("πŸ“₯ Transferring to secure server storage...")
206
  await reply_msg.download_media(file="words.txt")
207
+ await msg.edit("βœ… Transfer complete. Open **System Settings** -> **Load Database** to initialize.", buttons=get_main_menu())
208
 
209
  async def _load_words(event, is_callback=True):
210
  async def update_msg(text, buttons=None):
 
216
  try:
217
  all_lines = await asyncio.to_thread(read_file_sync)
218
  except Exception:
219
+ await update_msg("❌ `words.txt` missing. Please use `/upload`.")
220
  return
221
 
222
  if not all_lines:
223
+ await update_msg("⚠️ Database is empty. Upload a new master list.")
224
  return
225
 
226
  CHUNK_SIZE = 200000
 
241
  await pipe.execute()
242
  except Exception as e:
243
  if "oom" in str(e).lower() or "maxmemory" in str(e).lower():
244
+ await update_msg("❌ Memory limit exceeded. Please Wipe Database first.")
245
  return
246
 
247
  await asyncio.to_thread(write_file_sync, remaining_lines)
248
  await db.set_state(total=len(valid_words))
249
  skipped = len(valid_words) - len(to_queue)
250
 
251
+ msg = f"**Database Loaded**\nβ€’ Added: `{len(to_queue):,}`\nβ€’ Skipped: `{skipped:,}`\nβ€’ Disk Remaining: `{len(remaining_lines):,}`"
252
  await update_msg(msg, buttons=get_main_menu())
253
 
254
  @client.on(events.CallbackQuery())
 
259
 
260
  data = event.data.decode('utf-8')
261
 
 
262
  if data == "back_main":
263
  await event.edit("**Fragment Control System**\nSelect an operation:", buttons=get_main_menu())
264
  elif data == "menu_settings":
265
  await event.edit("**System Settings**\nConfigure scanner variables:", buttons=get_settings_menu())
266
 
 
267
  elif data == "load_words":
268
+ await event.edit("⏳ Processing chunk logic in background...")
269
  await _load_words(event, is_callback=True)
270
 
271
  elif data == "start_scan":
272
  await db.set_state(running="1", paused="0", start_time=str(time.time()))
273
+ await event.edit("▢️ Scanner initialized and running.", buttons=get_main_menu())
274
 
275
  elif data == "pause_scan":
276
  state = await db.get_state()
277
  if state.get("paused") == "1":
278
  await db.set_state(paused="0")
279
+ await event.edit("▢️ Scanner resumed.", buttons=get_main_menu())
280
  else:
281
  await db.set_state(paused="1")
282
+ await event.edit("⏸ Scanner paused.", buttons=get_main_menu())
283
 
284
  elif data == "stop_scan":
285
  await db.set_state(running="0", paused="0")
286
+ await event.edit("⏹ Scanner terminated.", buttons=get_main_menu())
287
 
288
  elif data in ("show_status", "refresh_status"):
289
  msg = await generate_status_msg()
290
+ buttons = [[Button.inline("πŸ”„ Refresh Data", b"refresh_status")], [Button.inline("Β« Back", b"back_main")]]
291
  try: await event.edit(msg, buttons=buttons)
292
  except Exception: pass
293
 
294
  elif data == "set_speed":
295
  buttons = [
296
+ [Button.inline("🐒 Slow (10)", b"spd_10"), Button.inline("🚢 Normal (20)", b"spd_20")],
297
+ [Button.inline("πŸš— Fast (30)", b"spd_30"), Button.inline("πŸš€ Max (50)", b"spd_50")],
298
+ [Button.inline("Β« Back", b"menu_settings")]
299
  ]
300
  await event.edit("**Thread Allocation**\nSelect maximum concurrent workers:", buttons=buttons)
301
 
 
303
  try:
304
  speed_limit = int(data.split("_")[1])
305
  await db.set_concurrency(speed_limit)
306
+ await event.edit(f"βœ… Thread limit updated to `{speed_limit}`.", buttons=get_settings_menu())
307
  except Exception:
308
  pass
309
 
310
  elif data == "export_files":
311
+ await event.edit("⏳ Generating export packages...")
312
 
313
  taken = sorted(await db.get_all_taken())
314
  unavail = sorted(await db.get_all_unavailable())
 
322
  if sold: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(sold).encode('utf-8')), caption="sold.txt")
323
  if forsale: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(forsale).encode('utf-8')), caption="forsale.txt")
324
 
325
+ await event.edit("βœ… Export sequence complete.", buttons=get_main_menu())
326
 
327
  elif data == "reset_confirm":
328
+ buttons = [[Button.inline("⚠️ CONFIRM WIPE", b"reset_do")], [Button.inline("Cancel", b"menu_settings")]]
329
  await event.edit("**AUTHORIZATION REQUIRED**\n\nThis will purge the active Redis database. Confirm?", buttons=buttons)
330
 
331
  elif data == "reset_do":
332
+ await event.edit("⏳ Purging memory...")
333
  await db.flush_all()
334
+ await event.edit("πŸ’£ Database wiped. Awaiting new chunk allocation.", buttons=get_settings_menu())