Muttered3 commited on
Commit
a7f0c46
·
verified ·
1 Parent(s): a9e914f

Update bot.py

Browse files
Files changed (1) hide show
  1. bot.py +271 -263
bot.py CHANGED
@@ -3,32 +3,43 @@ import re
3
  import time
4
  import io
5
  import asyncio
6
- import aiohttp
7
- from telethon import events, Button
 
8
  import db
9
  import scraper
10
  from curl_cffi.requests import AsyncSession
11
 
12
- # --- AUTHENTICATION ---
13
- ADMIN_IDS = {int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x}
 
 
 
14
 
15
- def is_admin(event):
16
- return event.sender_id in ADMIN_IDS
17
 
18
- # --- MENUS & DASHBOARD ---
 
 
 
19
  def get_main_menu():
20
- return [
21
- [Button.inline("▶️ Start Scan", b"start_scan"), Button.inline("⏸ Pause", b"pause_scan"), Button.inline("⏹ Stop", b"stop_scan")],
22
- [Button.inline("📊 Live Status", b"show_status"), Button.inline("📥 Export Data", b"export_files")],
23
- [Button.inline("⚙️ System Settings", b"menu_settings")]
24
- ]
 
 
 
25
 
26
  def get_settings_menu():
27
- return [
28
- [Button.inline("💾 Load Words", b"load_words"), Button.inline("📏 Set Length", b"set_min_length")],
29
- [Button.inline(" Set Speed", b"set_speed"), Button.inline("🔴 Wipe DB", b"reset_confirm")],
30
- [Button.inline("« Back to Main Menu", b"back_main")]
31
- ]
 
 
32
 
33
  async def generate_status_msg():
34
  state = await db.get_state()
@@ -84,323 +95,320 @@ async def test_proxy(proxy_url):
84
  pass
85
  return proxy_url, None
86
 
87
- # --- HANDLER SETUP ---
88
- def setup_handlers(client):
89
-
90
- @client.on(events.NewMessage(pattern='/start'))
91
- async def start_cmd(event):
92
- if is_admin(event):
93
- await event.respond("**Fragment Control System**", buttons=get_main_menu())
94
 
95
- # --- NEW: PROXY UPLOADER & CHECKER ---
96
- @client.on(events.NewMessage(pattern='/proxies'))
97
- async def proxy_cmd(event):
98
- if not is_admin(event): return
99
- if not event.is_reply:
100
- return await event.respond("⚠️ Reply to a `proxies.txt` file with `/proxies`.")
101
-
102
- reply = await event.get_reply_message()
103
- if not reply.document:
104
- return await event.respond("⚠️ Attached message is not a document.")
105
-
106
- msg = await event.respond("⏳ Downloading proxies...")
107
- file_path = await reply.download_media(file="temp_proxies.txt")
108
-
109
- with open(file_path, "r", encoding="utf-8") as f:
110
- raw_proxies = [line.strip() for line in f if line.strip()]
111
-
112
- os.remove(file_path)
113
-
114
- if not raw_proxies:
115
- return await msg.edit("⚠️ No proxies found in file.")
116
-
117
- await msg.edit(f"🔍 Testing **{len(raw_proxies)}** proxies concurrently...")
118
-
119
- # Test all proxies concurrently
120
- tasks = [test_proxy(p) for p in raw_proxies]
121
- results = await asyncio.gather(*tasks)
122
-
123
- working_proxies = []
124
- latency_board = []
125
 
126
- for p_url, latency in results:
127
- if latency is not None:
128
- working_proxies.append(p_url)
129
- latency_board.append((p_url, latency))
130
-
131
- # Sort by fastest latency
132
- latency_board.sort(key=lambda x: x[1])
133
-
134
- # Save working proxies to Redis
135
- await db.save_working_proxies(working_proxies)
136
 
137
- # Format Top 10 Report
138
- top_str = "\n".join([f"⏱ `{lat}ms` | {url.split('@')[-1] if '@' in url else url}" for url, lat in latency_board[:10]])
139
 
140
- report = (
141
- f"✅ **Proxy Test Complete**\n"
142
- f"━━━━━━━━━━━━━━━━\n"
143
- f"**Total Tested:** `{len(raw_proxies)}`\n"
144
- f"**Working & Saved:** `{len(working_proxies)}`\n\n"
145
- f"🏆 **Top Fastest Proxies:**\n{top_str if top_str else 'None'}"
146
- )
147
- await msg.edit(report, buttons=get_main_menu())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
 
 
 
 
 
 
 
 
 
 
149
 
150
- # --- LIVE HTML SCREENSHOT COMMAND ---
151
- @client.on(events.NewMessage(pattern=r'/check\s+([a-zA-Z0-9_]+)'))
152
- async def check_cmd(event):
153
- if not is_admin(event): return
154
- target = event.pattern_match.group(1).lower()
155
- msg = await event.respond(f"📸 Taking live screenshot of `@{target}` on Fragment...")
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- url = f"https://fragment.com/username/{target}"
158
- img_filename = None
 
 
159
 
160
- try:
161
- def take_screenshot():
162
- from html2image import Html2Image
163
- hti = Html2Image(
164
- browser_executable='/usr/bin/chromium',
165
- custom_flags=['--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--disable-dev-shm-usage']
166
- )
167
- hti.size = (1000, 750)
168
- name = f"{target}_fragment.png"
169
- hti.screenshot(url=url, save_as=name)
170
- return name
171
 
172
- img_filename = await asyncio.to_thread(take_screenshot)
173
-
174
- await client.send_file(event.chat_id, file=img_filename, caption=f"**Live Web Snapshot:** `@{target}`")
175
- await msg.delete()
176
-
177
- except Exception as e:
178
- try: await msg.edit(f"❌ **Screenshot Error:**\n`{str(e)}`")
179
- except: pass
180
- finally:
181
- if img_filename and os.path.exists(img_filename):
182
- os.remove(img_filename)
183
 
184
 
185
- # --- HTML DEBUG COMMAND (With Redirect Tracking) ---
186
- @client.on(events.NewMessage(pattern=r'/html\s+([a-zA-Z0-9_]+)'))
187
- async def html_cmd(event):
188
- if not is_admin(event): return
189
- target = event.pattern_match.group(1).lower()
190
- msg = await event.respond(f"🔍 Fetching raw HTML for `@{target}` via Chrome Impersonation...")
191
-
192
- url = f"https://fragment.com/username/{target}"
193
-
194
- try:
195
- async with AsyncSession(impersonate="chrome120") as session:
196
- resp = await session.get(url, timeout=15, allow_redirects=True)
197
- html_text = resp.text
198
- status_code = resp.status_code
199
- final_url = str(resp.url)
200
-
201
- f = io.BytesIO(html_text.encode('utf-8'))
202
- f.name = f"{target}_fragment.html"
203
- f.seek(0)
204
-
205
- caption = (
206
- f"📄 **Raw Response for** `@{target}`\n"
207
- f"**Requested URL:** `{url}`\n"
208
- f"**Final URL:** `{final_url}`\n"
209
- f"**Status Code:** `{status_code}`\n"
210
- )
211
-
212
- await client.send_file(event.chat_id, file=f, caption=caption)
213
- await msg.delete()
214
-
215
- except Exception as e:
216
- try: await msg.edit(f"❌ **Error fetching HTML:** `{str(e)}`")
217
- except: pass
218
 
219
- # --- WORDS UPLOAD COMMAND ---
220
- @client.on(events.NewMessage(pattern='/upload'))
221
- async def upload_cmd(event):
222
- if not is_admin(event): return
223
- if not event.is_reply:
224
- return await event.respond("Reply to a `.txt` file.")
225
- reply = await event.get_reply_message()
226
- if not reply.document:
227
- return await event.respond("Not a document.")
228
-
229
- wait = await event.respond("Downloading...")
230
- await reply.download_media(file="words.txt")
231
- await wait.edit("✅ Uploaded. Go to Settings -> Load Database.", buttons=get_main_menu())
232
 
233
- # --- SESSION GENERATOR ---
234
- @client.on(events.NewMessage(pattern='/getstring'))
235
- async def pyrogram_string_cmd(event):
236
- if not is_admin(event): return
237
-
238
- async with client.conversation(event.chat_id, timeout=300) as conv:
239
- try:
240
- await conv.send_message("🛠 **Pyrogram String Session Generator**\n\nEnter your `API_ID`:")
241
- api_id = int((await conv.get_response()).text.strip())
242
 
243
- await conv.send_message("Enter your `API_HASH`:")
244
- api_hash = (await conv.get_response()).text.strip()
 
 
 
 
245
 
246
- await conv.send_message("Enter the **Phone Number** (e.g., +1234567890):")
247
- phone = (await conv.get_response()).text.replace(" ", "").strip()
248
 
249
- await conv.send_message(" Requesting OTP from Telegram...")
250
-
251
- from pyrogram import Client
252
- from pyrogram.errors import SessionPasswordNeeded
253
 
254
- app = Client("temp_session", api_id=api_id, api_hash=api_hash, in_memory=True)
255
- await app.connect()
256
-
257
- sent_code = await app.send_code(phone)
258
-
259
- await conv.send_message("📥 **Code Sent!**\n\n⚠️ **CRITICAL:** You MUST enter the code with spaces between numbers.\nExample: `1 2 3 4 5`")
260
- otp_code = (await conv.get_response()).text.replace(" ", "").strip()
261
-
262
- try:
263
- await app.sign_in(phone, sent_code.phone_code_hash, otp_code)
264
- except SessionPasswordNeeded:
265
- await conv.send_message("🔐 **2FA Password Required.**\nEnter your Cloud Password:")
266
- pwd = (await conv.get_response()).text.strip()
267
- await app.check_password(pwd)
268
-
269
- string_session = await app.export_session_string()
270
- await conv.send_message(f"✅ **String Session Generated!**\n\n`{string_session}`\n\n⚠️ Keep this secret.")
271
- except asyncio.TimeoutError:
272
- await conv.send_message("❌ **Timeout.** Run `/getstring` again.")
273
- except Exception as e:
274
- await conv.send_message(f"❌ **Error:** `{str(e)}`")
275
- finally:
276
- if 'app' in locals():
277
- try: await app.disconnect()
278
- except: pass
279
 
280
- # --- DASHBOARD CALLBACKS ---
281
- async def _load_words(event):
282
- try:
283
- with open("words.txt", "r", encoding="utf-8") as f:
284
- all_lines = [line.strip().lower() for line in f if line.strip()]
285
- except Exception:
286
- return await event.edit("❌ `words.txt` not found. Upload it first.")
287
-
288
- if not all_lines:
289
- return await event.edit("Empty file.")
290
-
291
- state = await db.get_state()
292
- min_length = int(state.get("min_length", 4))
293
-
294
- CHUNK = 200000
295
- current = all_lines[:CHUNK]
296
- remaining = all_lines[CHUNK:]
297
 
298
- pattern = re.compile(rf'^[a-z0-9_]{{{min_length},32}}$')
299
- valid = {w for w in current if pattern.match(w)}
300
 
301
- r = await db.get_redis()
302
- done = await r.smembers("frag:done")
303
- to_q = list(valid - done)
304
 
305
- if to_q:
306
- pipe = r.pipeline()
307
- for i in range(0, len(to_q), 1000):
308
- pipe.lpush("frag:queue", *to_q[i:i+1000])
309
- await pipe.execute()
310
-
311
- with open("words.txt", "w", encoding="utf-8") as f:
312
- f.write("\n".join(remaining))
313
-
314
- await db.set_state(total=len(valid))
315
- await event.edit(f" Loaded {len(to_q):,} words (Min length: {min_length}).", buttons=get_main_menu())
316
-
317
- @client.on(events.CallbackQuery())
318
- async def button_handler(event):
319
- if not is_admin(event): return
320
- try: await event.answer()
321
- except: pass
322
- data = event.data.decode('utf-8')
323
 
 
 
 
 
 
 
324
  if data == "back_main":
325
- await event.edit("**Fragment Control System**", buttons=get_main_menu())
326
 
327
  elif data == "menu_settings":
328
- await event.edit("**System Settings**", buttons=get_settings_menu())
329
 
330
  elif data == "set_min_length":
331
- await event.edit("📏 **Select Minimum Word Length:**", buttons=[
332
- [Button.inline("4 Letters", b"min_len_4"), Button.inline("5 Letters", b"min_len_5")],
333
- [Button.inline("6 Letters", b"min_len_6"), Button.inline("7 Letters", b"min_len_7")],
334
- [Button.inline("« Back", b"menu_settings")]
335
  ])
 
336
 
337
  elif data.startswith("min_len_"):
338
  new_len = int(data.split("_")[2])
339
  await db.set_state(min_length=new_len)
340
- await event.edit(f"✅ Filter updated: Sniping words with at least `{new_len}` letters.", buttons=get_settings_menu())
341
 
342
  elif data == "load_words":
343
- await event.edit("⏳ Processing and loading chunk...")
344
- await _load_words(event)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  elif data == "start_scan":
347
  await db.set_state(running="1", paused="0")
348
- await event.edit("▶️ Scanner Running", buttons=get_main_menu())
349
 
350
  elif data == "pause_scan":
351
  s = await db.get_state()
352
  p = "0" if s.get("paused") == "1" else "1"
353
  await db.set_state(paused=p)
354
- await event.edit("⏸ Scanner Paused" if p=="1" else "▶️ Resumed", buttons=get_main_menu())
355
 
356
  elif data == "stop_scan":
357
  await db.set_state(running="0", paused="0")
358
- await event.edit("⏹ Scanner Stopped", buttons=get_main_menu())
359
 
360
  elif data in ("show_status", "refresh_status"):
361
- try:
362
- msg = await generate_status_msg()
363
- await event.edit(msg, buttons=[[Button.inline("🔄 Refresh", b"refresh_status")], [Button.inline("« Back", b"back_main")]])
364
- except Exception as e:
365
- if "not modified" in str(e).lower() or "MessageNotModifiedError" in str(type(e)): pass
366
- else: print(f"Status Error: {e}")
367
 
368
  elif data == "set_speed":
369
- await event.edit("⚡ **Select Concurrent Workers:**", buttons=[
370
- [Button.inline("10", b"spd_10"), Button.inline("20", b"spd_20")],
371
- [Button.inline("30", b"spd_30"), Button.inline("50", b"spd_50")],
372
- [Button.inline("« Back", b"menu_settings")]
373
  ])
 
374
 
375
  elif data.startswith("spd_"):
376
  await db.set_concurrency(int(data.split("_")[1]))
377
- await event.edit("✅ Speed Updated", buttons=get_settings_menu())
378
 
379
  elif data == "export_files":
380
- await event.edit("⏳ Generating text files...")
381
  exported_count = 0
382
 
383
- # Export all tracking sets including the new 'available' one
384
  for s in ["taken", "unavailable", "sold", "forsale", "auction", "available"]:
385
  lst = sorted(list(await (getattr(db, f"get_all_{s}")())))
386
  if lst:
387
  f = io.BytesIO("\n".join(lst).encode('utf-8'))
388
  f.name = f"{s}.txt"
389
- await client.send_file(event.chat_id, file=f)
390
  exported_count += 1
391
 
392
  if os.path.exists("other.txt"):
393
- await client.send_file(event.chat_id, "other.txt", caption="other.txt (Unknown HTML Responses)")
394
  exported_count += 1
395
 
396
  if exported_count > 0:
397
- await event.edit(f"✅ Export complete! Sent {exported_count} files.", buttons=get_main_menu())
398
  else:
399
- await event.edit("⚠️ No data to export yet. Start a scan first!", buttons=get_main_menu())
400
 
401
  elif data == "reset_confirm":
402
- await event.edit("⚠️ WIPE ALL REDIS DATA?", buttons=[[Button.inline("CONFIRM WIPE", b"reset_do")], [Button.inline("Cancel", b"menu_settings")]])
 
403
 
404
  elif data == "reset_do":
405
  await db.flush_all()
406
- await event.edit("💣 Memory purged successfully.", buttons=get_settings_menu())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import time
4
  import io
5
  import asyncio
6
+ from pyrogram import Client, filters
7
+ from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
8
+ from pyrogram.errors import MessageNotModified
9
  import db
10
  import scraper
11
  from curl_cffi.requests import AsyncSession
12
 
13
+ # --- CONFIG & AUTH ---
14
+ API_ID = int(os.environ.get("API_ID", 0))
15
+ API_HASH = os.environ.get("API_HASH", "")
16
+ BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
17
+ ADMIN_IDS = [int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x]
18
 
19
+ app = Client("bot_session", api_id=API_ID, api_hash=API_HASH, bot_token=BOT_TOKEN)
 
20
 
21
+ # Custom admin filter for Pyrogram
22
+ is_admin = filters.user(ADMIN_IDS)
23
+
24
+ # --- NAVIGATION MENUS ---
25
  def get_main_menu():
26
+ return InlineKeyboardMarkup([
27
+ [InlineKeyboardButton("▶️ Start", callback_data="start_scan"),
28
+ InlineKeyboardButton(" Pause", callback_data="pause_scan"),
29
+ InlineKeyboardButton(" Stop", callback_data="stop_scan")],
30
+ [InlineKeyboardButton("📊 Live Status", callback_data="show_status"),
31
+ InlineKeyboardButton("📥 Export Data", callback_data="export_files")],
32
+ [InlineKeyboardButton("⚙️ System Settings", callback_data="menu_settings")]
33
+ ])
34
 
35
  def get_settings_menu():
36
+ return InlineKeyboardMarkup([
37
+ [InlineKeyboardButton("💾 Load Words", callback_data="load_words"),
38
+ InlineKeyboardButton("📏 Set Length", callback_data="set_min_length")],
39
+ [InlineKeyboardButton(" Set Speed", callback_data="set_speed"),
40
+ InlineKeyboardButton("🔴 Wipe DB", callback_data="reset_confirm")],
41
+ [InlineKeyboardButton("« Back to Main", callback_data="back_main")]
42
+ ])
43
 
44
  async def generate_status_msg():
45
  state = await db.get_state()
 
95
  pass
96
  return proxy_url, None
97
 
98
+ # --- MESSAGE HANDLERS ---
99
+ @app.on_message(filters.command("start") & is_admin)
100
+ async def start_cmd(client, message):
101
+ await message.reply_text("**Fragment Control System**", reply_markup=get_main_menu())
 
 
 
102
 
103
+ @app.on_message(filters.command("proxies") & is_admin)
104
+ async def proxy_cmd(client, message):
105
+ if not message.reply_to_message or not message.reply_to_message.document:
106
+ return await message.reply_text("⚠️ Reply to a `proxies.txt` file with `/proxies`.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ msg = await message.reply_text("⏳ Downloading proxies...")
109
+ file_path = await message.reply_to_message.download(file_name="temp_proxies.txt")
110
+
111
+ with open(file_path, "r", encoding="utf-8") as f:
112
+ raw_proxies = [line.strip() for line in f if line.strip()]
 
 
 
 
 
113
 
114
+ os.remove(file_path)
115
+ if not raw_proxies: return await msg.edit_text("⚠️ No proxies found in file.")
116
 
117
+ await msg.edit_text(f"🔍 Testing **{len(raw_proxies)}** proxies concurrently...")
118
+
119
+ tasks = [test_proxy(p) for p in raw_proxies]
120
+ results = await asyncio.gather(*tasks)
121
+
122
+ working_proxies = []
123
+ latency_board = []
124
+
125
+ for p_url, latency in results:
126
+ if latency is not None:
127
+ working_proxies.append(p_url)
128
+ latency_board.append((p_url, latency))
129
+
130
+ latency_board.sort(key=lambda x: x[1])
131
+ await db.save_working_proxies(working_proxies)
132
+
133
+ top_str = "\n".join([f"⏱ `{lat}ms` | {url.split('@')[-1] if '@' in url else url}" for url, lat in latency_board[:10]])
134
+ report = (
135
+ f"✅ **Proxy Test Complete**\n"
136
+ f"━━━━━━━━━━━━━━━━\n"
137
+ f"**Total Tested:** `{len(raw_proxies)}`\n"
138
+ f"**Working & Saved:** `{len(working_proxies)}`\n\n"
139
+ f"🏆 **Top Fastest Proxies:**\n{top_str if top_str else 'None'}"
140
+ )
141
+ await msg.edit_text(report, reply_markup=get_main_menu())
142
+
143
+ @app.on_message(filters.command("check") & is_admin)
144
+ async def check_cmd(client, message):
145
+ if len(message.command) < 2: return await message.reply_text("Usage: `/check <word>`")
146
+ target = message.command[1].lower()
147
+ msg = await message.reply_text(f"📸 Taking live screenshot of `@{target}` on Fragment...")
148
+
149
+ url = f"https://fragment.com/username/{target}"
150
+ img_filename = None
151
+
152
+ try:
153
+ def take_screenshot():
154
+ from html2image import Html2Image
155
+ hti = Html2Image(
156
+ browser_executable='/usr/bin/chromium',
157
+ custom_flags=['--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--disable-dev-shm-usage']
158
+ )
159
+ hti.size = (1000, 750)
160
+ name = f"{target}_fragment.png"
161
+ hti.screenshot(url=url, save_as=name)
162
+ return name
163
 
164
+ img_filename = await asyncio.to_thread(take_screenshot)
165
+ await message.reply_document(document=img_filename, caption=f"**Live Web Snapshot:** `@{target}`")
166
+ await msg.delete()
167
+
168
+ except Exception as e:
169
+ try: await msg.edit_text(f"❌ **Screenshot Error:**\n`{str(e)}`")
170
+ except: pass
171
+ finally:
172
+ if img_filename and os.path.exists(img_filename):
173
+ os.remove(img_filename)
174
 
175
+ @app.on_message(filters.command("html") & is_admin)
176
+ async def html_cmd(client, message):
177
+ if len(message.command) < 2: return await message.reply_text("Usage: `/html <word>`")
178
+ target = message.command[1].lower()
179
+ msg = await message.reply_text(f"🔍 Fetching raw HTML for `@{target}` via Chrome Impersonation...")
180
+
181
+ url = f"https://fragment.com/username/{target}"
182
+
183
+ try:
184
+ async with AsyncSession(impersonate="chrome120") as session:
185
+ resp = await session.get(url, timeout=15, allow_redirects=True)
186
+ html_text = resp.text
187
+ status_code = resp.status_code
188
+ final_url = str(resp.url)
189
+
190
+ f = io.BytesIO(html_text.encode('utf-8'))
191
+ f.name = f"{target}_fragment.html"
192
+ f.seek(0)
193
 
194
+ caption = (f"📄 **Raw Response for** `@{target}`\n"
195
+ f"**Requested URL:** `{url}`\n"
196
+ f"**Final URL:** `{final_url}`\n"
197
+ f"**Status Code:** `{status_code}`")
198
 
199
+ await message.reply_document(document=f, caption=caption)
200
+ await msg.delete()
201
+ except Exception as e:
202
+ try: await msg.edit_text(f"❌ **Error fetching HTML:** `{str(e)}`")
203
+ except: pass
 
 
 
 
 
 
204
 
205
+ @app.on_message(filters.command("upload") & is_admin)
206
+ async def upload_cmd(client, message):
207
+ if not message.reply_to_message or not message.reply_to_message.document:
208
+ return await message.reply_text("Reply to a `.txt` file.")
209
+
210
+ msg = await message.reply_text("Downloading...")
211
+ await message.reply_to_message.download(file_name="words.txt")
212
+ await msg.edit_text("✅ Uploaded. Go to Settings -> Load Database.", reply_markup=get_main_menu())
 
 
 
213
 
214
 
215
+ # --- STRING SESSION STATE MACHINE ---
216
+ # Pyrogram doesn't have native "conversation" handlers, so we use Futures
217
+ input_waiters = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ @app.on_message(filters.text & is_admin, group=-1)
220
+ async def catch_input(client, message):
221
+ chat_id = message.chat.id
222
+ if chat_id in input_waiters and not input_waiters[chat_id].done():
223
+ input_waiters[chat_id].set_result(message.text)
224
+ message.stop_propagation()
 
 
 
 
 
 
 
225
 
226
+ async def get_response(chat_id, timeout=300):
227
+ loop = asyncio.get_running_loop()
228
+ future = loop.create_future()
229
+ input_waiters[chat_id] = future
230
+ try: return await asyncio.wait_for(future, timeout)
231
+ finally: del input_waiters[chat_id]
 
 
 
232
 
233
+ @app.on_message(filters.command("getstring") & is_admin)
234
+ async def pyrogram_string_cmd(client, message):
235
+ chat_id = message.chat.id
236
+ try:
237
+ await message.reply_text("🛠 **Pyrogram Session Generator**\nEnter your `API_ID`:")
238
+ api_id = int((await get_response(chat_id)).strip())
239
 
240
+ await message.reply_text("Enter your `API_HASH`:")
241
+ api_hash = (await get_response(chat_id)).strip()
242
 
243
+ await message.reply_text("Enter the **Phone Number** (e.g., +1234567890):")
244
+ phone = (await get_response(chat_id)).replace(" ", "").strip()
 
 
245
 
246
+ await message.reply_text(" Requesting OTP from Telegram...")
247
+
248
+ from pyrogram import Client as TmpClient
249
+ from pyrogram.errors import SessionPasswordNeeded
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ tmp_app = TmpClient("temp_session", api_id=api_id, api_hash=api_hash, in_memory=True)
252
+ await tmp_app.connect()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ sent_code = await tmp_app.send_code(phone)
 
255
 
256
+ await message.reply_text("📥 **Code Sent!**\n⚠️ Enter code with spaces (e.g., `1 2 3 4 5`):")
257
+ otp_code = (await get_response(chat_id)).replace(" ", "").strip()
 
258
 
259
+ try:
260
+ await tmp_app.sign_in(phone, sent_code.phone_code_hash, otp_code)
261
+ except SessionPasswordNeeded:
262
+ await message.reply_text("🔐 **2FA Password Required:**")
263
+ pwd = (await get_response(chat_id)).strip()
264
+ await tmp_app.check_password(pwd)
265
+
266
+ string_session = await tmp_app.export_session_string()
267
+ await message.reply_text(f"✅ **Session Generated!**\n\n`{string_session}`\n\n⚠️ Keep this secret.")
268
+ except asyncio.TimeoutError:
269
+ await message.reply_text(" **Timeout.** Run `/getstring` again.")
270
+ except Exception as e:
271
+ await message.reply_text(f"❌ **Error:** `{str(e)}`")
272
+ finally:
273
+ if 'tmp_app' in locals():
274
+ try: await tmp_app.disconnect()
275
+ except: pass
 
276
 
277
+ # --- CALLBACK ROUTER ---
278
+ @app.on_callback_query(is_admin)
279
+ async def button_handler(client, query):
280
+ data = query.data
281
+
282
+ try:
283
  if data == "back_main":
284
+ await query.edit_message_text("**Fragment Control System**", reply_markup=get_main_menu())
285
 
286
  elif data == "menu_settings":
287
+ await query.edit_message_text("**System Settings**", reply_markup=get_settings_menu())
288
 
289
  elif data == "set_min_length":
290
+ kb = InlineKeyboardMarkup([
291
+ [InlineKeyboardButton("4 Letters", callback_data="min_len_4"), InlineKeyboardButton("5 Letters", callback_data="min_len_5")],
292
+ [InlineKeyboardButton("6 Letters", callback_data="min_len_6"), InlineKeyboardButton("7 Letters", callback_data="min_len_7")],
293
+ [InlineKeyboardButton("« Back", callback_data="menu_settings")]
294
  ])
295
+ await query.edit_message_text("📏 **Select Minimum Word Length:**", reply_markup=kb)
296
 
297
  elif data.startswith("min_len_"):
298
  new_len = int(data.split("_")[2])
299
  await db.set_state(min_length=new_len)
300
+ await query.edit_message_text(f"✅ Filter updated to `{new_len}` letters.", reply_markup=get_settings_menu())
301
 
302
  elif data == "load_words":
303
+ await query.edit_message_text("⏳ Processing and loading chunk...")
304
+ try:
305
+ with open("words.txt", "r", encoding="utf-8") as f:
306
+ all_lines = [line.strip().lower() for line in f if line.strip()]
307
+ except Exception:
308
+ return await query.edit_message_text("❌ `words.txt` not found. Upload it first.")
309
+
310
+ if not all_lines: return await query.edit_message_text("Empty file.")
311
+
312
+ state = await db.get_state()
313
+ min_length = int(state.get("min_length", 4))
314
+
315
+ CHUNK = 200000
316
+ current = all_lines[:CHUNK]
317
+ remaining = all_lines[CHUNK:]
318
+
319
+ pattern = re.compile(rf'^[a-z0-9_]{{{min_length},32}}$')
320
+ valid = {w for w in current if pattern.match(w)}
321
+
322
+ r = await db.get_redis()
323
+ done = await r.smembers("frag:done")
324
+ to_q = list(valid - done)
325
+
326
+ if to_q:
327
+ pipe = r.pipeline()
328
+ for i in range(0, len(to_q), 1000): pipe.lpush("frag:queue", *to_q[i:i+1000])
329
+ await pipe.execute()
330
+
331
+ with open("words.txt", "w", encoding="utf-8") as f:
332
+ f.write("\n".join(remaining))
333
+
334
+ await db.set_state(total=len(valid))
335
+ await query.edit_message_text(f"✅ Loaded {len(to_q):,} words.", reply_markup=get_main_menu())
336
 
337
  elif data == "start_scan":
338
  await db.set_state(running="1", paused="0")
339
+ await query.edit_message_text("▶️ Scanner Running", reply_markup=get_main_menu())
340
 
341
  elif data == "pause_scan":
342
  s = await db.get_state()
343
  p = "0" if s.get("paused") == "1" else "1"
344
  await db.set_state(paused=p)
345
+ await query.edit_message_text("⏸ Scanner Paused" if p=="1" else "▶️ Resumed", reply_markup=get_main_menu())
346
 
347
  elif data == "stop_scan":
348
  await db.set_state(running="0", paused="0")
349
+ await query.edit_message_text("⏹ Scanner Stopped", reply_markup=get_main_menu())
350
 
351
  elif data in ("show_status", "refresh_status"):
352
+ msg = await generate_status_msg()
353
+ kb = InlineKeyboardMarkup([[InlineKeyboardButton("🔄 Refresh", callback_data="refresh_status")], [InlineKeyboardButton("« Back", callback_data="back_main")]])
354
+ try: await query.edit_message_text(msg, reply_markup=kb)
355
+ except MessageNotModified: pass
 
 
356
 
357
  elif data == "set_speed":
358
+ kb = InlineKeyboardMarkup([
359
+ [InlineKeyboardButton("10", callback_data="spd_10"), InlineKeyboardButton("20", callback_data="spd_20")],
360
+ [InlineKeyboardButton("30", callback_data="spd_30"), InlineKeyboardButton("50", callback_data="spd_50")],
361
+ [InlineKeyboardButton("« Back", callback_data="menu_settings")]
362
  ])
363
+ await query.edit_message_text("⚡ **Select Concurrent Workers:**", reply_markup=kb)
364
 
365
  elif data.startswith("spd_"):
366
  await db.set_concurrency(int(data.split("_")[1]))
367
+ await query.edit_message_text("✅ Speed Updated", reply_markup=get_settings_menu())
368
 
369
  elif data == "export_files":
370
+ await query.edit_message_text("⏳ Generating text files...")
371
  exported_count = 0
372
 
 
373
  for s in ["taken", "unavailable", "sold", "forsale", "auction", "available"]:
374
  lst = sorted(list(await (getattr(db, f"get_all_{s}")())))
375
  if lst:
376
  f = io.BytesIO("\n".join(lst).encode('utf-8'))
377
  f.name = f"{s}.txt"
378
+ await client.send_document(query.message.chat.id, document=f)
379
  exported_count += 1
380
 
381
  if os.path.exists("other.txt"):
382
+ await client.send_document(query.message.chat.id, document="other.txt", caption="Unknown HTML Responses")
383
  exported_count += 1
384
 
385
  if exported_count > 0:
386
+ await query.edit_message_text(f"✅ Export complete! Sent {exported_count} files.", reply_markup=get_main_menu())
387
  else:
388
+ await query.edit_message_text("⚠️ No data to export yet.", reply_markup=get_main_menu())
389
 
390
  elif data == "reset_confirm":
391
+ kb = InlineKeyboardMarkup([[InlineKeyboardButton("CONFIRM WIPE", callback_data="reset_do")], [InlineKeyboardButton("Cancel", callback_data="menu_settings")]])
392
+ await query.edit_message_text("⚠️ WIPE ALL REDIS DATA?", reply_markup=kb)
393
 
394
  elif data == "reset_do":
395
  await db.flush_all()
396
+ await query.edit_message_text("💣 Memory purged successfully.", reply_markup=get_settings_menu())
397
+
398
+ except Exception as e:
399
+ if not isinstance(e, MessageNotModified):
400
+ print(f"Callback Error: {e}")
401
+
402
+ # If run directly as a script
403
+ if __name__ == "__main__":
404
+ from worker import run_worker
405
+
406
+ async def main_loop():
407
+ await app.start()
408
+ print("🤖 Pyrogram Bot Started!")
409
+ asyncio.create_task(run_worker())
410
+ from pyrogram import idle
411
+ await idle()
412
+ await app.stop()
413
+
414
+ app.run(main_loop())