Spaces:
Paused
Paused
Create bot.py
Browse files
bot.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import time
|
| 4 |
+
import io
|
| 5 |
+
from telethon import events, Button
|
| 6 |
+
import db
|
| 7 |
+
|
| 8 |
+
def get_admins():
|
| 9 |
+
return {int(x) for x in os.environ.get("ADMIN_IDS", "").split(",") if x}
|
| 10 |
+
|
| 11 |
+
def is_admin(event):
|
| 12 |
+
return event.sender_id in get_admins()
|
| 13 |
+
|
| 14 |
+
def setup_handlers(client):
|
| 15 |
+
@client.on(events.NewMessage(pattern='/start'))
|
| 16 |
+
async def start_cmd(event):
|
| 17 |
+
if not is_admin(event):
|
| 18 |
+
await event.respond("β Unauthorized.")
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
buttons = [
|
| 22 |
+
[Button.inline("βΆοΈ Start Scan", b"start_scan"), Button.inline("βΈ Pause", b"pause_scan")],
|
| 23 |
+
[Button.inline("βΉ Stop", b"stop_scan"), Button.inline("π Status", b"show_status")],
|
| 24 |
+
[Button.inline("β‘ Set Speed", b"set_speed"), Button.inline("π₯ Export", b"export_files")],
|
| 25 |
+
[Button.inline("π Load Words", b"load_words"), Button.inline("π£ Reset", b"reset_confirm")]
|
| 26 |
+
]
|
| 27 |
+
await event.respond("π Fragment Scanner Bot", buttons=buttons)
|
| 28 |
+
|
| 29 |
+
@client.on(events.NewMessage(pattern='/load'))
|
| 30 |
+
async def load_cmd(event):
|
| 31 |
+
if not is_admin(event):
|
| 32 |
+
await event.respond("β Unauthorized.")
|
| 33 |
+
return
|
| 34 |
+
await _load_words(event.respond)
|
| 35 |
+
|
| 36 |
+
async def _load_words(reply_func):
|
| 37 |
+
try:
|
| 38 |
+
with open("words.txt", "r", encoding="utf-8") as f:
|
| 39 |
+
words = [line.strip().lower() for line in f]
|
| 40 |
+
except Exception:
|
| 41 |
+
await reply_func("β words.txt not found.")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
valid_words = [w for w in set(words) if re.match(r'^[a-z0-9_]{4,32}$', w)]
|
| 45 |
+
|
| 46 |
+
r = await db.get_redis()
|
| 47 |
+
done = await r.smembers("frag:done")
|
| 48 |
+
|
| 49 |
+
to_queue = [w for w in valid_words if w not in done]
|
| 50 |
+
|
| 51 |
+
if to_queue:
|
| 52 |
+
pipe = r.pipeline()
|
| 53 |
+
for i in range(0, len(to_queue), 1000):
|
| 54 |
+
pipe.lpush("frag:queue", *to_queue[i:i+1000])
|
| 55 |
+
await pipe.execute()
|
| 56 |
+
|
| 57 |
+
await db.set_state(total=len(valid_words))
|
| 58 |
+
skipped = len(valid_words) - len(to_queue)
|
| 59 |
+
await reply_func(f"π₯ Loaded {len(to_queue)} words. Skipped {skipped} (already done).")
|
| 60 |
+
|
| 61 |
+
@client.on(events.CallbackQuery())
|
| 62 |
+
async def button_handler(event):
|
| 63 |
+
if not is_admin(event):
|
| 64 |
+
await event.answer("β Unauthorized.", alert=True)
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
data = event.data.decode('utf-8')
|
| 68 |
+
|
| 69 |
+
if data == "load_words":
|
| 70 |
+
await event.answer()
|
| 71 |
+
await _load_words(event.reply)
|
| 72 |
+
|
| 73 |
+
elif data == "start_scan":
|
| 74 |
+
await db.set_state(running="1", paused="0", start_time=str(time.time()))
|
| 75 |
+
qlen = await db.queue_size()
|
| 76 |
+
await event.answer()
|
| 77 |
+
await event.reply(f"π Scan started! {qlen} words in queue.")
|
| 78 |
+
|
| 79 |
+
elif data == "pause_scan":
|
| 80 |
+
state = await db.get_state()
|
| 81 |
+
if state.get("paused") == "1":
|
| 82 |
+
await db.set_state(paused="0")
|
| 83 |
+
await event.answer()
|
| 84 |
+
await event.reply("βΆοΈ Scan resumed.")
|
| 85 |
+
else:
|
| 86 |
+
await db.set_state(paused="1")
|
| 87 |
+
await event.answer()
|
| 88 |
+
await event.reply("βΈ Scan paused.")
|
| 89 |
+
|
| 90 |
+
elif data == "stop_scan":
|
| 91 |
+
await db.set_state(running="0", paused="0")
|
| 92 |
+
await event.answer()
|
| 93 |
+
await event.reply("βΉ Scan stopped. Progress saved.")
|
| 94 |
+
|
| 95 |
+
elif data == "show_status":
|
| 96 |
+
state = await db.get_state()
|
| 97 |
+
counts = await db.get_counts()
|
| 98 |
+
concurrency = await db.get_concurrency()
|
| 99 |
+
qlen = await db.queue_size()
|
| 100 |
+
|
| 101 |
+
total = int(state.get("total", 0))
|
| 102 |
+
processed = int(state.get("processed", 0))
|
| 103 |
+
pct = (processed / total * 100) if total > 0 else 0
|
| 104 |
+
run_str = "YES" if state.get("running") == "1" else "NO"
|
| 105 |
+
pause_str = "YES" if state.get("paused") == "1" else "NO"
|
| 106 |
+
|
| 107 |
+
msg = (
|
| 108 |
+
f"**π SCANNER STATUS**\n"
|
| 109 |
+
f"βββββββββββββββββββ\n"
|
| 110 |
+
f"π’ Running : `{run_str:<10}`\n"
|
| 111 |
+
f"βΈ Paused : `{pause_str:<10}`\n"
|
| 112 |
+
f"π¦ Total : `{total:<10,}`\n"
|
| 113 |
+
f"β
Processed : `{processed:<10,}`\n"
|
| 114 |
+
f"π Progress : `{pct:.2f}%`\n"
|
| 115 |
+
f"βββββββββββββββββββ\n"
|
| 116 |
+
f"π΄ Taken : `{counts['taken']:<10,}`\n"
|
| 117 |
+
f"π« Unavail : `{counts['unavailable']:<10,}`\n"
|
| 118 |
+
f"π° For Sale : `{counts['forsale']:<10,}`\n"
|
| 119 |
+
f"π¨ Auction : `{counts['auction']:<10,}`\n"
|
| 120 |
+
f"βββββββββββββββββββ\n"
|
| 121 |
+
f"β‘ Speed : `{concurrency:<10}`\n"
|
| 122 |
+
f"π Queue : `{qlen:<10,}`\n"
|
| 123 |
+
)
|
| 124 |
+
await event.answer()
|
| 125 |
+
await event.reply(msg)
|
| 126 |
+
|
| 127 |
+
elif data == "set_speed":
|
| 128 |
+
buttons = [
|
| 129 |
+
[Button.inline("π’ 10", b"spd_10"), Button.inline("πΆ 20", b"spd_20"), Button.inline("π 30", b"spd_30")],
|
| 130 |
+
[Button.inline("π 50", b"spd_50"), Button.inline("β‘ 75", b"spd_75"), Button.inline("π₯ 100", b"spd_100")]
|
| 131 |
+
]
|
| 132 |
+
await event.answer()
|
| 133 |
+
await event.reply("Select Speed:", buttons=buttons)
|
| 134 |
+
|
| 135 |
+
elif data.startswith("spd_"):
|
| 136 |
+
val = int(data.split("_")[1])
|
| 137 |
+
await db.set_concurrency(val)
|
| 138 |
+
await event.answer()
|
| 139 |
+
await event.reply(f"β‘ Speed set to {val} concurrent workers.")
|
| 140 |
+
|
| 141 |
+
elif data == "export_files":
|
| 142 |
+
await event.answer()
|
| 143 |
+
taken = sorted(await db.get_all_taken())
|
| 144 |
+
unavail = sorted(await db.get_all_unavailable())
|
| 145 |
+
|
| 146 |
+
f_taken = io.BytesIO("\n".join(taken).encode('utf-8'))
|
| 147 |
+
f_taken.name = "taken.txt"
|
| 148 |
+
await client.send_file(event.chat_id, file=f_taken)
|
| 149 |
+
|
| 150 |
+
f_unavail = io.BytesIO("\n".join(unavail).encode('utf-8'))
|
| 151 |
+
f_unavail.name = "unavailable.txt"
|
| 152 |
+
await client.send_file(event.chat_id, file=f_unavail)
|
| 153 |
+
|
| 154 |
+
await event.reply(f"β
Exported {len(taken)} taken, {len(unavail)} unavailable.")
|
| 155 |
+
|
| 156 |
+
elif data == "reset_confirm":
|
| 157 |
+
buttons = [
|
| 158 |
+
[Button.inline("β
Yes, WIPE ALL DATA", b"reset_do"), Button.inline("β Cancel", b"reset_cancel")]
|
| 159 |
+
]
|
| 160 |
+
await event.answer()
|
| 161 |
+
await event.reply("β οΈ Are you sure you want to completely wipe the database?", buttons=buttons)
|
| 162 |
+
|
| 163 |
+
elif data == "reset_do":
|
| 164 |
+
await db.flush_all()
|
| 165 |
+
await event.answer()
|
| 166 |
+
await event.reply("π£ All data wiped. Use /load to reload words.")
|
| 167 |
+
|
| 168 |
+
elif data == "reset_cancel":
|
| 169 |
+
await event.answer()
|
| 170 |
+
await event.reply("Cancelled.")
|