Muttered3 commited on
Commit
8b5491d
·
verified ·
1 Parent(s): fbbf512

Update bot.py

Browse files
Files changed (1) hide show
  1. bot.py +179 -139
bot.py CHANGED
@@ -4,7 +4,9 @@ import time
4
  import io
5
  import asyncio
6
  from telethon import events, Button
 
7
  import db
 
8
 
9
  def get_admins():
10
  return {int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x}
@@ -12,14 +14,75 @@ def get_admins():
12
  def is_admin(event):
13
  return event.sender_id in get_admins()
14
 
15
- def get_main_buttons():
 
16
  return [
17
- [Button.inline("▶️ Start Scan", b"start_scan"), Button.inline("Pause", b"pause_scan")],
18
- [Button.inline(" Stop", b"stop_scan"), Button.inline("📊 Status", b"show_status")],
19
- [Button.inline(" Set Speed", b"set_speed"), Button.inline("📥 Export", b"export_files")],
20
- [Button.inline("🔄 Load Words", b"load_words"), Button.inline("💣 Reset", b"reset_confirm")]
21
  ]
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  async def generate_status_msg():
24
  state = await db.get_state()
25
  counts = await db.get_counts()
@@ -29,243 +92,220 @@ async def generate_status_msg():
29
  total = int(state.get("total", 0))
30
  processed = int(state.get("processed", 0))
31
  pct = (processed / total * 100) if total > 0 else 0
32
- run_str = "YES" if state.get("running") == "1" else "NO"
33
- pause_str = "YES" if state.get("paused") == "1" else "NO"
34
 
35
- bar_length = 15
 
 
 
36
  filled = int(bar_length * (processed / total)) if total > 0 else 0
37
- bar = "" * filled + "" * (bar_length - filled)
38
 
39
  msg = (
40
- f"**📊 SCANNER STATUS**\n"
41
- f"━━━━━━━━━━━━━━━━━━━\n"
42
- f"🟢 Running : `{run_str:<10}`\n"
43
- f"⏸ Paused : `{pause_str:<10}`\n"
44
- f"📦 Total : `{total:<10,}`\n"
45
- f"✅ Processed : `{processed:<10,}`\n"
46
- f"📈 Progress : `{pct:.2f}%`\n"
47
  f"`[{bar}]`\n"
48
- f"━━━━━━━━━━━━━━━━━━━\n"
49
- f"🔴 Taken : `{counts['taken']:<10,}`\n"
50
- f"🚫 Unavail : `{counts['unavailable']:<10,}`\n"
51
- f"💰 For Sale : `{counts['forsale']:<10,}`\n"
52
- f"🔨 Auction : `{counts['auction']:<10,}`\n"
53
- f"🛒 Sold : `{counts['sold']:<10,}`\n"
54
- f"━━━━━━━━━━━━━━━━━━━\n"
55
- f"⚡ Speed : `{concurrency:<10}`\n"
56
- f"📋 Queue : `{qlen:<10,}`\n"
57
  )
58
  return msg
59
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def setup_handlers(client):
 
61
  @client.on(events.NewMessage(pattern='/start'))
62
  async def start_cmd(event):
63
- if not is_admin(event):
64
- return
65
- await event.respond("👋 **Fragment Scanner Bot**\nSelect an action below:", buttons=get_main_buttons())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  @client.on(events.NewMessage(pattern='/upload'))
68
  async def upload_cmd(event):
69
- if not is_admin(event):
70
- return
71
  if not event.is_reply:
72
- await event.respond("⚠️ Please reply to a `.txt` file with `/upload`.")
73
  return
74
 
75
  reply_msg = await event.get_reply_message()
76
  if not reply_msg.document or reply_msg.file.ext != '.txt':
77
- await event.respond("⚠️ The message you replied to must be a `.txt` document.")
78
  return
79
 
80
- msg = await event.respond("📥 Downloading massive file to server disk...")
81
  await reply_msg.download_media(file="words.txt")
82
- await msg.edit(" **File uploaded to server successfully!**\n\nClick **🔄 Load Words** to bite off the first 200,000 words into the database.", buttons=get_main_buttons())
83
-
84
- @client.on(events.NewMessage(pattern='/load'))
85
- async def load_cmd(event):
86
- if not is_admin(event):
87
- return
88
- await _load_words(event, is_callback=False)
89
 
90
  async def _load_words(event, is_callback=True):
91
- async def update_msg(text):
92
  if is_callback:
93
- await event.edit(text, buttons=get_main_buttons())
94
  else:
95
- await event.respond(text, buttons=get_main_buttons())
96
 
97
  try:
98
- with open("words.txt", "r", encoding="utf-8") as f:
99
- all_lines = [line.strip().lower() for line in f if line.strip()]
100
  except Exception:
101
- await update_msg("`words.txt` not found. Please upload your master list using `/upload`.")
102
  return
103
 
104
  if not all_lines:
105
- await update_msg("⚠️ The `words.txt` file on the server is empty. You've processed everything! Upload a new file.")
106
  return
107
 
108
- # Load chunks safely
109
  CHUNK_SIZE = 200000
110
  current_chunk = all_lines[:CHUNK_SIZE]
111
  remaining_lines = all_lines[CHUNK_SIZE:]
112
 
113
- # OPTIMIZATION 1: Fast regex filtering
114
- valid_words = {w for w in current_chunk if re.match(r'^[a-z0-9_]{4,32}$', w)}
115
-
116
  r = await db.get_redis()
117
  done = await r.smembers("frag:done")
118
-
119
- # OPTIMIZATION 2: Mathematical Set Subtraction (Instant calculation instead of freezing CPU)
120
  to_queue = list(valid_words - done)
121
 
122
  if to_queue:
123
  pipe = r.pipeline()
124
  for i in range(0, len(to_queue), 1000):
125
  pipe.lpush("frag:queue", *to_queue[i:i+1000])
126
- # OPTIMIZATION 3: Yield to event loop so bot buttons stay smooth
127
  await asyncio.sleep(0)
128
-
129
  try:
130
  await pipe.execute()
131
  except Exception as e:
132
- err_str = str(e).lower()
133
- if "oom" in err_str or "maxmemory" in err_str:
134
- await update_msg("❌ **Error:** Redis ran out of memory! Click **💣 Reset**, then try loading again.")
135
- else:
136
- await update_msg(f"❌ **Database Error:** {str(e)}")
137
  return
138
 
139
- with open("words.txt", "w", encoding="utf-8") as f:
140
- f.write("\n".join(remaining_lines))
141
-
142
  await db.set_state(total=len(valid_words))
143
  skipped = len(valid_words) - len(to_queue)
144
 
145
- msg = f"📥 Loaded **{len(to_queue):,}** words into the queue. Skipped {skipped:,}.\n\n"
146
- if remaining_lines:
147
- msg += f"📦 **{len(remaining_lines):,} words remaining on server disk.**\n*(When this batch finishes: Export, Reset, and click Load Words again!)*"
148
- else:
149
- msg += "✅ **No words remaining on server disk.**"
150
-
151
- await update_msg(msg)
152
 
153
  @client.on(events.CallbackQuery())
154
  async def button_handler(event):
155
- if not is_admin(event):
156
- try:
157
- await event.answer("⛔ Unauthorized.", alert=True)
158
- except Exception:
159
- pass
160
- return
161
-
162
- # Instantly tell Telegram we received the click to stop the loading clock
163
- try:
164
- await event.answer()
165
- except Exception:
166
- pass
167
 
168
  data = event.data.decode('utf-8')
169
 
 
170
  if data == "back_main":
171
- await event.edit("👋 **Fragment Scanner Bot**\nSelect an action below:", buttons=get_main_buttons())
 
 
172
 
 
173
  elif data == "load_words":
174
- # Immediate UI feedback
175
- await event.edit("⏳ **Reading and loading words... Please wait.**")
176
  await _load_words(event, is_callback=True)
177
 
178
  elif data == "start_scan":
179
  await db.set_state(running="1", paused="0", start_time=str(time.time()))
180
- qlen = await db.queue_size()
181
- await event.edit(f"🚀 **Scan started!**\n`{qlen:,}` words currently in the processing queue.", buttons=get_main_buttons())
182
 
183
  elif data == "pause_scan":
184
  state = await db.get_state()
185
  if state.get("paused") == "1":
186
  await db.set_state(paused="0")
187
- await event.edit("▶️ **Scan resumed.**", buttons=get_main_buttons())
188
  else:
189
  await db.set_state(paused="1")
190
- await event.edit(" **Scan paused.**", buttons=get_main_buttons())
191
 
192
  elif data == "stop_scan":
193
  await db.set_state(running="0", paused="0")
194
- await event.edit(" **Scan stopped.** Progress has been saved.", buttons=get_main_buttons())
195
 
196
  elif data in ("show_status", "refresh_status"):
197
  msg = await generate_status_msg()
198
- buttons = [
199
- [Button.inline("🔄 Refresh Live", b"refresh_status")],
200
- [Button.inline("🔙 Back to Menu", b"back_main")]
201
- ]
202
- try:
203
- await event.edit(msg, buttons=buttons)
204
- except Exception:
205
- pass
206
 
207
  elif data == "set_speed":
208
  buttons = [
209
- [Button.inline("🐢 10", b"spd_10"), Button.inline("🚶 20", b"spd_20"), Button.inline("🚗 30", b"spd_30")],
210
- [Button.inline("🚀 50", b"spd_50"), Button.inline(" 75", b"spd_75"), Button.inline("🔥 100", b"spd_100")],
211
- [Button.inline("🔙 Cancel", b"back_main")]
212
  ]
213
- await event.edit(" **Select Scanner Speed (Concurrent Workers):**", buttons=buttons)
214
 
215
  elif data.startswith("spd_"):
216
  try:
217
  speed_limit = int(data.split("_")[1])
218
  await db.set_concurrency(speed_limit)
219
- await event.edit(f" **Speed updated successfully!**\nNow running with `{speed_limit}` concurrent workers.", buttons=get_main_buttons())
220
  except Exception:
221
- await event.edit("❌ Error setting speed.", buttons=get_main_buttons())
222
 
223
  elif data == "export_files":
224
- await event.edit(" **Exporting data...**\nPlease wait while files are generated.")
225
 
226
  taken = sorted(await db.get_all_taken())
227
  unavail = sorted(await db.get_all_unavailable())
228
  sold = sorted(await db.get_all_sold())
229
  forsale = sorted(await db.get_all_forsale())
230
 
231
- await asyncio.sleep(0) # Keep UI smooth
232
 
233
- exported_count = 0
 
 
 
234
 
235
- if taken:
236
- f_taken = io.BytesIO("\n".join(taken).encode('utf-8'))
237
- f_taken.name = "taken.txt"
238
- await client.send_file(event.chat_id, file=f_taken)
239
- exported_count += 1
240
-
241
- if unavail:
242
- f_unavail = io.BytesIO("\n".join(unavail).encode('utf-8'))
243
- f_unavail.name = "unavailable.txt"
244
- await client.send_file(event.chat_id, file=f_unavail)
245
- exported_count += 1
246
-
247
- if sold:
248
- f_sold = io.BytesIO("\n".join(sold).encode('utf-8'))
249
- f_sold.name = "sold.txt"
250
- await client.send_file(event.chat_id, file=f_sold)
251
- exported_count += 1
252
-
253
- if forsale:
254
- f_forsale = io.BytesIO("\n".join(forsale).encode('utf-8'))
255
- f_forsale.name = "forsale.txt"
256
- await client.send_file(event.chat_id, file=f_forsale)
257
- exported_count += 1
258
-
259
- await event.edit(f"✅ **Export Complete!**\nGenerated `{exported_count}` text files.", buttons=get_main_buttons())
260
 
261
  elif data == "reset_confirm":
262
- buttons = [
263
- [Button.inline(" Yes, WIPE ALL DATA", b"reset_do")],
264
- [Button.inline("❌ Cancel", b"back_main")]
265
- ]
266
- await event.edit("⚠️ **DANGER ZONE** ⚠️\n\nAre you sure you want to completely wipe the Redis database? All progress will be lost.", buttons=buttons)
267
 
268
  elif data == "reset_do":
269
- await event.edit(" **Wiping database...**")
270
  await db.flush_all()
271
- await event.edit("💣 **Database wiped.**\n\nRedis is completely clear. Click **🔄 Load Words** to grab the next batch from the server disk!", buttons=get_main_buttons())
 
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
  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()
 
92
  total = int(state.get("total", 0))
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"
109
+ f"**Progress:** `{pct:.2f}%`\n"
 
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"
117
+ f"Auction : `{counts.get('auction', 0):,}`\n"
118
+ f"Sold : `{counts.get('sold', 0):,}`\n"
 
119
  )
120
  return msg
121
 
122
+ # --- BACKGROUND HELPERS ---
123
+ def read_file_sync():
124
+ with open("words.txt", "r", encoding="utf-8") as f:
125
+ return [line.strip().lower() for line in f if line.strip()]
126
+
127
+ def write_file_sync(lines):
128
+ with open("words.txt", "w", encoding="utf-8") as f:
129
+ f.write("\n".join(lines))
130
+
131
+ def filter_words_sync(chunk):
132
+ return {w for w in chunk if re.match(r'^[a-z0-9_]{4,32}$', w)}
133
+
134
  def setup_handlers(client):
135
+
136
  @client.on(events.NewMessage(pattern='/start'))
137
  async def start_cmd(event):
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):
186
  if is_callback:
187
+ await event.edit(text, buttons=buttons or get_settings_menu())
188
  else:
189
+ await event.respond(text, buttons=buttons or get_settings_menu())
190
 
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
202
  current_chunk = all_lines[:CHUNK_SIZE]
203
  remaining_lines = all_lines[CHUNK_SIZE:]
204
 
205
+ valid_words = await asyncio.to_thread(filter_words_sync, current_chunk)
 
 
206
  r = await db.get_redis()
207
  done = await r.smembers("frag:done")
 
 
208
  to_queue = list(valid_words - done)
209
 
210
  if to_queue:
211
  pipe = r.pipeline()
212
  for i in range(0, len(to_queue), 1000):
213
  pipe.lpush("frag:queue", *to_queue[i:i+1000])
 
214
  await asyncio.sleep(0)
 
215
  try:
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())
230
  async def button_handler(event):
231
+ if not is_admin(event): return
232
+ try: await event.answer()
233
+ except Exception: pass
 
 
 
 
 
 
 
 
 
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
 
279
  elif data.startswith("spd_"):
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())
292
  sold = sorted(await db.get_all_sold())
293
  forsale = sorted(await db.get_all_forsale())
294
 
295
+ await asyncio.sleep(0)
296
 
297
+ if taken: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(taken).encode('utf-8')), caption="taken.txt")
298
+ if unavail: await client.send_file(event.chat_id, file=io.BytesIO("\n".join(unavail).encode('utf-8')), caption="unavailable.txt")
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())