Upload main.py
Browse files
main.py
CHANGED
|
@@ -1294,4 +1294,807 @@ class Bot:
|
|
| 1294 |
|
| 1295 |
async def check_target(self, event: Any, actor: Info, target: Target, strict: bool) -> bool:
|
| 1296 |
if target.id == self.me.id:
|
| 1297 |
-
await self.say(event, "لا يمكنني تنفيذ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1294 |
|
| 1295 |
async def check_target(self, event: Any, actor: Info, target: Target, strict: bool) -> bool:
|
| 1296 |
if target.id == self.me.id:
|
| 1297 |
+
await self.say(event, "لا يمكنني تنفيذ ذلك على نفسي.")
|
| 1298 |
+
return False
|
| 1299 |
+
if target.id == actor.user_id:
|
| 1300 |
+
await self.say(event, "لا يمكنك تنفيذ ذلك على نفسك.")
|
| 1301 |
+
return False
|
| 1302 |
+
if not strict:
|
| 1303 |
+
return True
|
| 1304 |
+
tinfo = await self.perms.info(event.chat_id, target.peer, target.id)
|
| 1305 |
+
if tinfo.is_creator:
|
| 1306 |
+
await self.say(event, "لا يمكن تنفيذ الأمر على مالك المجموعة.")
|
| 1307 |
+
return False
|
| 1308 |
+
if tinfo.is_admin and tinfo.rank >= actor.rank:
|
| 1309 |
+
await self.say(event, "لا يمكن تنفيذ الأمر على مشرف بصلاحيات مساوية أو أعلى من صلاحياتك.")
|
| 1310 |
+
return False
|
| 1311 |
+
return True
|
| 1312 |
+
|
| 1313 |
+
# ---- registration
|
| 1314 |
+
def register(self) -> None:
|
| 1315 |
+
self.client.add_event_handler(self.on_message, events.NewMessage(incoming=True))
|
| 1316 |
+
self.client.add_event_handler(self.on_callback, events.CallbackQuery())
|
| 1317 |
+
|
| 1318 |
+
# ---- message router
|
| 1319 |
+
async def on_message(self, event: Any) -> None:
|
| 1320 |
+
try:
|
| 1321 |
+
if event.is_private:
|
| 1322 |
+
await self.on_private(event)
|
| 1323 |
+
return
|
| 1324 |
+
if not event.is_group or event.chat_id is None:
|
| 1325 |
+
return
|
| 1326 |
+
msg = event.message
|
| 1327 |
+
text = msg.raw_text or ""
|
| 1328 |
+
anon = self.is_anon(event)
|
| 1329 |
+
group = await self.db.group(event.chat_id)
|
| 1330 |
+
sender = None
|
| 1331 |
+
if not anon:
|
| 1332 |
+
sender = msg.sender
|
| 1333 |
+
if sender is not None:
|
| 1334 |
+
self.bg(self.db.remember_user(sender))
|
| 1335 |
+
if await self.customs.capture(event):
|
| 1336 |
+
return
|
| 1337 |
+
if await self.protection.check(event, group, sender):
|
| 1338 |
+
return
|
| 1339 |
+
first = text.split("\n", 1)[0][:200]
|
| 1340 |
+
if first and not first.startswith("/"):
|
| 1341 |
+
cmd, arg = parse_command(first)
|
| 1342 |
+
if cmd:
|
| 1343 |
+
await self.dispatch(event, cmd, arg)
|
| 1344 |
+
return
|
| 1345 |
+
if not anon and len(first) <= 100 and await self.try_custom(event, norm(first), group):
|
| 1346 |
+
return
|
| 1347 |
+
except Exception:
|
| 1348 |
+
log.exception("on_message failed")
|
| 1349 |
+
|
| 1350 |
+
async def on_private(self, event: Any) -> None:
|
| 1351 |
+
text = (event.message.raw_text or "").strip()
|
| 1352 |
+
if not text.startswith("/start"):
|
| 1353 |
+
return
|
| 1354 |
+
url = ("https://t.me/%s?startgroup=true&admin=delete_messages+restrict_members+pin_messages+invite_users"
|
| 1355 |
+
% (self.me.username or ""))
|
| 1356 |
+
await self.say(
|
| 1357 |
+
event,
|
| 1358 |
+
"مرحباً بك 👋\nأنا بوت مخصص لإدارة المجموعات.\n"
|
| 1359 |
+
"أضفني إلى مجموعتك وارفعني مشرفاً، ثم اكتب: الاوامر",
|
| 1360 |
+
buttons=[[Button.url("➕ إضافة البوت إلى مجموعة", url)]],
|
| 1361 |
+
)
|
| 1362 |
+
|
| 1363 |
+
async def try_custom(self, event: Any, name: str, group: Dict[str, Any]) -> bool:
|
| 1364 |
+
items = await self.db.customs_of(event.chat_id)
|
| 1365 |
+
if not items:
|
| 1366 |
+
return False
|
| 1367 |
+
doc = items.get(("command", name)) or items.get(("reply", name))
|
| 1368 |
+
if not doc:
|
| 1369 |
+
return False
|
| 1370 |
+
if ("c:" + name) in group["locks"]:
|
| 1371 |
+
actor = await self.actor_info(event)
|
| 1372 |
+
if not actor.is_admin:
|
| 1373 |
+
return True
|
| 1374 |
+
await self.customs.send(event, doc)
|
| 1375 |
+
return True
|
| 1376 |
+
|
| 1377 |
+
# ---- dispatcher
|
| 1378 |
+
async def dispatch(self, event: Any, cmd: str, arg: str) -> None:
|
| 1379 |
+
try:
|
| 1380 |
+
if cmd in MOD_CMDS:
|
| 1381 |
+
await self.c_moderate(event, cmd, arg)
|
| 1382 |
+
elif cmd == "warn":
|
| 1383 |
+
await self.c_warn(event, arg)
|
| 1384 |
+
elif cmd == "unwarn":
|
| 1385 |
+
await self.c_unwarn(event, arg)
|
| 1386 |
+
elif cmd == "warns":
|
| 1387 |
+
await self.c_warns(event, arg)
|
| 1388 |
+
elif cmd == "clearwarns":
|
| 1389 |
+
await self.c_clearwarns(event, arg)
|
| 1390 |
+
elif cmd == "id":
|
| 1391 |
+
await self.c_id(event, arg, False)
|
| 1392 |
+
elif cmd == "kash":
|
| 1393 |
+
await self.c_id(event, arg, True)
|
| 1394 |
+
elif cmd == "calc":
|
| 1395 |
+
await self.c_calc(event, arg)
|
| 1396 |
+
elif cmd == "date":
|
| 1397 |
+
await self.c_date(event, arg)
|
| 1398 |
+
elif cmd in ("grp", "owners", "managers", "admins"):
|
| 1399 |
+
await self.c_staff(event, cmd, arg)
|
| 1400 |
+
elif cmd == "mytitle":
|
| 1401 |
+
await self.c_mytitle(event, arg)
|
| 1402 |
+
elif cmd in ("lock", "unlock"):
|
| 1403 |
+
await self.c_lock(event, arg, cmd == "lock")
|
| 1404 |
+
elif cmd in ("enable", "disable"):
|
| 1405 |
+
await self.c_enable(event, arg, cmd == "enable")
|
| 1406 |
+
elif cmd == "menu":
|
| 1407 |
+
text, rows = self.menu.main()
|
| 1408 |
+
await self.say(event, text, buttons=rows)
|
| 1409 |
+
elif cmd == "settings":
|
| 1410 |
+
actor = await self.gate(event, None, arg)
|
| 1411 |
+
if actor:
|
| 1412 |
+
idx = [s[0] for s in SECTIONS].index("settings")
|
| 1413 |
+
text, rows = await self.menu.section(idx, event.chat_id)
|
| 1414 |
+
await self.say(event, text, buttons=rows)
|
| 1415 |
+
elif cmd == "addword":
|
| 1416 |
+
await self.c_addword(event, arg)
|
| 1417 |
+
elif cmd == "delword":
|
| 1418 |
+
await self.c_delword(event, arg)
|
| 1419 |
+
elif cmd == "words":
|
| 1420 |
+
await self.c_words(event, arg)
|
| 1421 |
+
elif cmd == "delete":
|
| 1422 |
+
await self.c_delete(event, arg)
|
| 1423 |
+
elif cmd == "pin":
|
| 1424 |
+
await self.c_pin(event, arg, True)
|
| 1425 |
+
elif cmd == "unpin":
|
| 1426 |
+
await self.c_pin(event, arg, False)
|
| 1427 |
+
elif cmd in ("addreply", "editreply", "addcmd", "editcmd", "delreply", "delcmd"):
|
| 1428 |
+
actor = await self.gate(event, None, arg)
|
| 1429 |
+
if not actor:
|
| 1430 |
+
return
|
| 1431 |
+
kind = "reply" if cmd.endswith("reply") else "command"
|
| 1432 |
+
if cmd.startswith("del"):
|
| 1433 |
+
await self.customs.delete(event, kind, arg)
|
| 1434 |
+
else:
|
| 1435 |
+
await self.customs.begin(event, kind, arg, cmd.startswith("edit"))
|
| 1436 |
+
elif cmd in ("replies", "cmds"):
|
| 1437 |
+
actor = await self.gate(event, None, arg)
|
| 1438 |
+
if actor:
|
| 1439 |
+
await self.say(event, await self.customs.list_text(
|
| 1440 |
+
event.chat_id, "reply" if cmd == "replies" else "command"))
|
| 1441 |
+
elif cmd == "rule":
|
| 1442 |
+
await self.c_rule(event, arg)
|
| 1443 |
+
elif cmd == "rules":
|
| 1444 |
+
actor = await self.gate(event, None, arg)
|
| 1445 |
+
if actor:
|
| 1446 |
+
await self.say(event, self.warnings.rules_text(await self.db.group(event.chat_id)))
|
| 1447 |
+
elif cmd == "setspam":
|
| 1448 |
+
await self.c_setspam(event, arg)
|
| 1449 |
+
elif cmd == "setmute":
|
| 1450 |
+
await self.c_setmute(event, arg)
|
| 1451 |
+
except FloodWaitError as e:
|
| 1452 |
+
await self.say(event, err_text(e))
|
| 1453 |
+
except RPCError as e:
|
| 1454 |
+
await self.say(event, err_text(e))
|
| 1455 |
+
except ValueError:
|
| 1456 |
+
log.warning("command %s failed", cmd, exc_info=True)
|
| 1457 |
+
await self.say(event, "تعذر تنفيذ الأمر. تأكد أن المجموعة سوبر جروب.")
|
| 1458 |
+
except Exception:
|
| 1459 |
+
log.exception("command %s crashed", cmd)
|
| 1460 |
+
|
| 1461 |
+
# ---- moderation commands
|
| 1462 |
+
async def c_moderate(self, event: Any, cmd: str, arg: str) -> None:
|
| 1463 |
+
actor = await self.gate(event, "ban_users", arg)
|
| 1464 |
+
if not actor:
|
| 1465 |
+
return
|
| 1466 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1467 |
+
if err or target is None:
|
| 1468 |
+
await self.say(event, err or "تعذر تحديد العضو.")
|
| 1469 |
+
return
|
| 1470 |
+
if not await self.check_target(event, actor, target, cmd in ("kick", "ban", "mute", "restrict")):
|
| 1471 |
+
return
|
| 1472 |
+
g = await self.db.group(event.chat_id)
|
| 1473 |
+
await self.moderation.do(event.chat_id, target.peer, cmd, 0)
|
| 1474 |
+
await self.say(event, DONE_TEXT[cmd])
|
| 1475 |
+
|
| 1476 |
+
async def c_warn(self, event: Any, arg: str) -> None:
|
| 1477 |
+
actor = await self.gate(event, "ban_users", arg)
|
| 1478 |
+
if not actor:
|
| 1479 |
+
return
|
| 1480 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1481 |
+
if err or target is None:
|
| 1482 |
+
await self.say(event, err or "تعذر تحديد العضو.")
|
| 1483 |
+
return
|
| 1484 |
+
if not await self.check_target(event, actor, target, True):
|
| 1485 |
+
return
|
| 1486 |
+
count = await self.db.add_warn(event.chat_id, target.id)
|
| 1487 |
+
await self.say(event, await self.warnings.enforce(event.chat_id, target, count))
|
| 1488 |
+
|
| 1489 |
+
async def c_unwarn(self, event: Any, arg: str) -> None:
|
| 1490 |
+
actor = await self.gate(event, "ban_users", arg)
|
| 1491 |
+
if not actor:
|
| 1492 |
+
return
|
| 1493 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1494 |
+
if err or target is None:
|
| 1495 |
+
await self.say(event, err or "تعذر تحديد العضو.")
|
| 1496 |
+
return
|
| 1497 |
+
left = await self.db.remove_warn(event.chat_id, target.id)
|
| 1498 |
+
if left is None:
|
| 1499 |
+
await self.say(event, "لا توجد تحذيرات على هذا العضو.")
|
| 1500 |
+
else:
|
| 1501 |
+
await self.say(event, "تم رفع تحذير عن العضو. المتبقي: %d" % left)
|
| 1502 |
+
|
| 1503 |
+
async def c_clearwarns(self, event: Any, arg: str) -> None:
|
| 1504 |
+
actor = await self.gate(event, "ban_users", arg)
|
| 1505 |
+
if not actor:
|
| 1506 |
+
return
|
| 1507 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1508 |
+
if err or target is None:
|
| 1509 |
+
await self.say(event, err or "تعذر تحديد العضو.")
|
| 1510 |
+
return
|
| 1511 |
+
await self.db.clear_warns(event.chat_id, target.id)
|
| 1512 |
+
await self.say(event, "تم مسح تحذيرات العضو.")
|
| 1513 |
+
|
| 1514 |
+
async def c_warns(self, event: Any, arg: str) -> None:
|
| 1515 |
+
if not self.plausible(event, arg) or self.is_anon(event):
|
| 1516 |
+
return
|
| 1517 |
+
if event.message.is_reply or arg:
|
| 1518 |
+
actor = await self.gate(event, None, arg)
|
| 1519 |
+
if not actor:
|
| 1520 |
+
return
|
| 1521 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1522 |
+
if err or target is None:
|
| 1523 |
+
await self.say(event, err or "تعذر تحديد العضو.")
|
| 1524 |
+
return
|
| 1525 |
+
uid, who = target.id, "العضو"
|
| 1526 |
+
else:
|
| 1527 |
+
uid, who = event.sender_id, "لديك"
|
| 1528 |
+
n = await self.db.warn_count(event.chat_id, uid)
|
| 1529 |
+
if who == "لديك":
|
| 1530 |
+
await self.say(event, "عدد تحذيراتك: %d" % n)
|
| 1531 |
+
else:
|
| 1532 |
+
await self.say(event, "عدد تحذيرات العضو: %d" % n)
|
| 1533 |
+
|
| 1534 |
+
# ---- id / kash / date / calc
|
| 1535 |
+
async def is_locked_for(self, event: Any, group: Dict[str, Any], key: str) -> bool:
|
| 1536 |
+
if key not in group["locks"] or self.is_anon(event):
|
| 1537 |
+
return False
|
| 1538 |
+
return not (await self.actor_info(event)).is_admin
|
| 1539 |
+
|
| 1540 |
+
@staticmethod
|
| 1541 |
+
def weird_arg(event: Any, arg: str) -> bool:
|
| 1542 |
+
return bool(arg) and not event.message.is_reply and not (arg[0] == "@" or arg[0].isdigit())
|
| 1543 |
+
|
| 1544 |
+
async def pick_user(self, event: Any, arg: str) -> Optional[types.User]:
|
| 1545 |
+
"""Reply / @username / ID, or the sender itself. Answers with an error on failure."""
|
| 1546 |
+
msg = event.message
|
| 1547 |
+
if msg.is_reply or arg:
|
| 1548 |
+
target, err = await self.resolver.resolve(event, arg)
|
| 1549 |
+
if err or target is None:
|
| 1550 |
+
await self.say(event, err or "تعذر تحديد الحساب.")
|
| 1551 |
+
return None
|
| 1552 |
+
return target.user
|
| 1553 |
+
user = msg.sender
|
| 1554 |
+
if user is None and not self.is_anon(event):
|
| 1555 |
+
try:
|
| 1556 |
+
user = await event.get_sender()
|
| 1557 |
+
except Exception:
|
| 1558 |
+
user = None
|
| 1559 |
+
if not isinstance(user, types.User):
|
| 1560 |
+
await self.say(event, "لا يمكن تحديد الحساب.")
|
| 1561 |
+
return None
|
| 1562 |
+
return user
|
| 1563 |
+
|
| 1564 |
+
async def c_id(self, event: Any, arg: str, detailed: bool) -> None:
|
| 1565 |
+
if self.weird_arg(event, arg):
|
| 1566 |
+
return
|
| 1567 |
+
group = await self.db.group(event.chat_id)
|
| 1568 |
+
key = "kash" if detailed else "id"
|
| 1569 |
+
if await self.is_locked_for(event, group, key):
|
| 1570 |
+
if not detailed:
|
| 1571 |
+
await self.say(event, "أمر ايدي مقفل للأعضاء.")
|
| 1572 |
+
return
|
| 1573 |
+
user = await self.pick_user(event, arg)
|
| 1574 |
+
if user is None:
|
| 1575 |
+
return
|
| 1576 |
+
chat = event.chat_id
|
| 1577 |
+
uname = ("@" + user.username) if user.username else "لا يوجد"
|
| 1578 |
+
photo_task = self.start_photo(user) # download in parallel with building the card
|
| 1579 |
+
if detailed:
|
| 1580 |
+
tinfo = await self.perms.info(chat, user, user.id, True)
|
| 1581 |
+
role = tinfo.role if tinfo.known else "عضو"
|
| 1582 |
+
state = STATE_AR.get(tinfo.state, "—") if tinfo.known else "غير معروف (ارفعني مشرفاً)"
|
| 1583 |
+
text = kash_card_text(full_name(user), user.id, uname, role, state)
|
| 1584 |
+
else:
|
| 1585 |
+
tinfo, about = await asyncio.gather(
|
| 1586 |
+
self.perms.info(chat, user, user.id), self.fetch_about(user))
|
| 1587 |
+
est = describe_estimate(user.id)
|
| 1588 |
+
if est:
|
| 1589 |
+
cr = est[0] + (" (دقة منخفضة)" if est[2] == "low" else "")
|
| 1590 |
+
else:
|
| 1591 |
+
cr = "لا يمكن تقديره بشكل موثوق"
|
| 1592 |
+
text = id_card_text(
|
| 1593 |
+
full_name(user), uname, tinfo.role if tinfo.known else "عضو", user.id, cr,
|
| 1594 |
+
about[:100] if about else "لا توجد نبذة")
|
| 1595 |
+
await self.send_card(event, user, text, photo_task)
|
| 1596 |
+
|
| 1597 |
+
async def c_date(self, event: Any, arg: str) -> None:
|
| 1598 |
+
if self.weird_arg(event, arg):
|
| 1599 |
+
return
|
| 1600 |
+
group = await self.db.group(event.chat_id)
|
| 1601 |
+
if await self.is_locked_for(event, group, "date"):
|
| 1602 |
+
return
|
| 1603 |
+
user = await self.pick_user(event, arg)
|
| 1604 |
+
if user is None:
|
| 1605 |
+
return
|
| 1606 |
+
rows = ["👤 <b>الاسم</b> » %s" % esc(display_name(user)),
|
| 1607 |
+
"🆔 <b>الايدي</b> » <code>%d</code>" % user.id]
|
| 1608 |
+
est = describe_estimate(user.id)
|
| 1609 |
+
if est:
|
| 1610 |
+
date, age, level = est
|
| 1611 |
+
rows.append("📅 <b>تاريخ الإنشاء التقريبي</b> » %s" % date)
|
| 1612 |
+
rows.append("⏳ <b>عمر الحساب</b> » %s" % age)
|
| 1613 |
+
rows.append("🎯 <b>الدقة</b> » %s" % (
|
| 1614 |
+
"متوسطة (± عدة أشهر)" if level == "mid" else "منخفضة (± سنة أو أكثر)"))
|
| 1615 |
+
else:
|
| 1616 |
+
rows.append("📅 <i>لا يمكن تقدير تاريخ الإنشاء بشكل موثوق.</i>")
|
| 1617 |
+
rows.append("ℹ️ <i>تقدير مبني على الايدي فقط، وتي��يجرام لا يوفّر التاريخ الحقيقي.</i>")
|
| 1618 |
+
text = "✦ ━━ 🕰 <b>عمر الحساب</b> ━━ ✦\n\n%s" % "\n".join(rows)
|
| 1619 |
+
await self.say(event, text, reply_to=event.message.id)
|
| 1620 |
+
|
| 1621 |
+
async def c_calc(self, event: Any, arg: str) -> None:
|
| 1622 |
+
if not arg:
|
| 1623 |
+
await self.say(event, "اكتب العملية بعد الأمر. مثال: <code>احسب 55+20</code>")
|
| 1624 |
+
return
|
| 1625 |
+
if not _CALC_OK.match(arg):
|
| 1626 |
+
return # normal chat, not a calculation
|
| 1627 |
+
group = await self.db.group(event.chat_id)
|
| 1628 |
+
if await self.is_locked_for(event, group, "calc"):
|
| 1629 |
+
return
|
| 1630 |
+
res = calc_eval(arg)
|
| 1631 |
+
if res is None:
|
| 1632 |
+
await self.say(event, "عملية غير صحيحة. مثال: <code>احسب 55+20</code>")
|
| 1633 |
+
return
|
| 1634 |
+
await self.say(event, "🧮 <b>الحاسبة</b>\n<code>%s</code> = <b>%s</b>" % (esc(arg), res),
|
| 1635 |
+
reply_to=event.message.id)
|
| 1636 |
+
|
| 1637 |
+
# ---- cards / staff
|
| 1638 |
+
async def fetch_about(self, user: types.User) -> str:
|
| 1639 |
+
try:
|
| 1640 |
+
full = await asyncio.wait_for(self.client(GetFullUserRequest(user)), timeout=4)
|
| 1641 |
+
return (full.full_user.about or "").strip()
|
| 1642 |
+
except Exception:
|
| 1643 |
+
return ""
|
| 1644 |
+
|
| 1645 |
+
async def get_staff(self, chat_id: int) -> Dict[str, List[Tuple[Any, str]]]:
|
| 1646 |
+
hit = self._staff_cache.get(chat_id)
|
| 1647 |
+
if hit and time.monotonic() - hit[0] < 60:
|
| 1648 |
+
return hit[1]
|
| 1649 |
+
out: Dict[str, List[Tuple[Any, str]]] = {"owner": [], "manager": [], "admin": []}
|
| 1650 |
+
async for u in self.client.iter_participants(chat_id, filter=types.ChannelParticipantsAdmins()):
|
| 1651 |
+
part = getattr(u, "participant", None)
|
| 1652 |
+
if isinstance(part, types.ChannelParticipantCreator):
|
| 1653 |
+
kind = "owner"
|
| 1654 |
+
elif isinstance(part, types.ChannelParticipantAdmin):
|
| 1655 |
+
kind = "manager" if getattr(part.admin_rights, "add_admins", False) else "admin"
|
| 1656 |
+
else:
|
| 1657 |
+
continue
|
| 1658 |
+
if getattr(u, "bot", False) or getattr(u, "deleted", False):
|
| 1659 |
+
continue
|
| 1660 |
+
out[kind].append((u, getattr(part, "rank", None) or ""))
|
| 1661 |
+
if len(self._staff_cache) > 200:
|
| 1662 |
+
self._staff_cache.clear()
|
| 1663 |
+
self._staff_cache[chat_id] = (time.monotonic(), out)
|
| 1664 |
+
return out
|
| 1665 |
+
|
| 1666 |
+
async def c_staff(self, event: Any, cmd: str, arg: str) -> None:
|
| 1667 |
+
if arg:
|
| 1668 |
+
return # normal chat, not a command
|
| 1669 |
+
chat = event.chat_id
|
| 1670 |
+
staff = await self.get_staff(chat)
|
| 1671 |
+
sec_owner = staff_section("👑 <b>المالك</b>", staff["owner"])
|
| 1672 |
+
sec_mgr = staff_section("🛡 <b>المدراء</b>", staff["manager"])
|
| 1673 |
+
sec_adm = staff_section("👮 <b>الأدمنية</b>", staff["admin"])
|
| 1674 |
+
if cmd == "owners":
|
| 1675 |
+
text = sec_owner
|
| 1676 |
+
elif cmd == "managers":
|
| 1677 |
+
text = sec_mgr
|
| 1678 |
+
elif cmd == "admins":
|
| 1679 |
+
text = sec_adm
|
| 1680 |
+
else:
|
| 1681 |
+
title, count = "", None
|
| 1682 |
+
try:
|
| 1683 |
+
ch = await event.get_chat()
|
| 1684 |
+
title = getattr(ch, "title", "") or ""
|
| 1685 |
+
full = await self.client(GetFullChannelRequest(ch))
|
| 1686 |
+
count = full.full_chat.participants_count
|
| 1687 |
+
except Exception:
|
| 1688 |
+
pass
|
| 1689 |
+
head = ["🏛 <b>كشف المجموعة</b>"]
|
| 1690 |
+
if title:
|
| 1691 |
+
head.append("\u21DC الاسم \u21A2 %s" % esc(title))
|
| 1692 |
+
if count is not None:
|
| 1693 |
+
head.append("\u21DC الأعضاء \u21A2 %s" % "{:,}".format(count))
|
| 1694 |
+
text = "\n".join(head) + "\n\n" + sec_owner + "\n\n" + sec_mgr + "\n\n" + sec_adm
|
| 1695 |
+
await self.say(event, join_limited(text.split("\n"), 3900), reply_to=event.message.id)
|
| 1696 |
+
|
| 1697 |
+
async def c_mytitle(self, event: Any, arg: str) -> None:
|
| 1698 |
+
if arg or self.is_anon(event):
|
| 1699 |
+
return
|
| 1700 |
+
info = await self.perms.info(
|
| 1701 |
+
event.chat_id, event.message.input_sender or event.sender_id, event.sender_id, True)
|
| 1702 |
+
if not info.known:
|
| 1703 |
+
await self.say(event, "ارفعني مشرفاً لأتمكن من قراءة الألقاب.")
|
| 1704 |
+
return
|
| 1705 |
+
await self.say(
|
| 1706 |
+
event,
|
| 1707 |
+
"\u21DC رتبتك \u21A2 %s\n\u21DC لقبك \u21A2 %s" % (
|
| 1708 |
+
info.role, esc(info.title) if info.title else "لا يوجد لقب"),
|
| 1709 |
+
reply_to=event.message.id)
|
| 1710 |
+
|
| 1711 |
+
def start_photo(self, user: types.User) -> Optional["asyncio.Task[Any]"]:
|
| 1712 |
+
c = self._photo_cache.get(user.id)
|
| 1713 |
+
if c and time.monotonic() - c[0] < 3600:
|
| 1714 |
+
return None
|
| 1715 |
+
if not isinstance(user.photo, types.UserProfilePhoto):
|
| 1716 |
+
return None
|
| 1717 |
+
return asyncio.create_task(self._download_photo(user))
|
| 1718 |
+
|
| 1719 |
+
async def _download_photo(self, user: types.User) -> Optional[io.BytesIO]:
|
| 1720 |
+
try:
|
| 1721 |
+
data = await self.client.download_profile_photo(user, file=bytes)
|
| 1722 |
+
if data:
|
| 1723 |
+
bio = io.BytesIO(data)
|
| 1724 |
+
bio.name = "photo.jpg"
|
| 1725 |
+
return bio
|
| 1726 |
+
except Exception:
|
| 1727 |
+
log.debug("photo download failed", exc_info=True)
|
| 1728 |
+
return None
|
| 1729 |
+
|
| 1730 |
+
async def send_card(self, event: Any, user: types.User, text: str, photo_task: Any) -> None:
|
| 1731 |
+
chat, reply_to = event.chat_id, event.message.id
|
| 1732 |
+
cached = self._photo_cache.get(user.id)
|
| 1733 |
+
if cached and time.monotonic() - cached[0] < 3600:
|
| 1734 |
+
try: # re-use the already uploaded photo: no download, no upload
|
| 1735 |
+
await self.client.send_file(chat, cached[1], caption=text, reply_to=reply_to)
|
| 1736 |
+
return
|
| 1737 |
+
except RPCError:
|
| 1738 |
+
self._photo_cache.pop(user.id, None)
|
| 1739 |
+
photo = await photo_task if photo_task is not None else None
|
| 1740 |
+
if photo is not None:
|
| 1741 |
+
try:
|
| 1742 |
+
sent = await self.client.send_file(chat, photo, caption=text, reply_to=reply_to)
|
| 1743 |
+
media = getattr(sent, "media", None)
|
| 1744 |
+
if media is not None:
|
| 1745 |
+
if len(self._photo_cache) > 500:
|
| 1746 |
+
self._photo_cache.clear()
|
| 1747 |
+
self._photo_cache[user.id] = (time.monotonic(), media)
|
| 1748 |
+
return
|
| 1749 |
+
except RPCError as e:
|
| 1750 |
+
log.debug("send photo failed: %s", getattr(e, "message", e))
|
| 1751 |
+
try:
|
| 1752 |
+
await event.reply(text, link_preview=False)
|
| 1753 |
+
except RPCError as e:
|
| 1754 |
+
log.warning("reply failed: %s", getattr(e, "message", e))
|
| 1755 |
+
|
| 1756 |
+
# ---- locks
|
| 1757 |
+
async def c_lock(self, event: Any, arg: str, lock: bool) -> None:
|
| 1758 |
+
if not arg:
|
| 1759 |
+
if self.plausible(event, arg):
|
| 1760 |
+
actor = await self.gate(event, None, arg)
|
| 1761 |
+
if actor:
|
| 1762 |
+
await self.say(event, "اكتب اسم القفل. مثال: قفل الروابط")
|
| 1763 |
+
return
|
| 1764 |
+
actor = None
|
| 1765 |
+
chat = event.chat_id
|
| 1766 |
+
key = LOCK_ALIAS.get(arg)
|
| 1767 |
+
if key is None:
|
| 1768 |
+
customs = await self.db.customs_of(chat)
|
| 1769 |
+
if any((k, arg) in customs for k in ("reply", "command")):
|
| 1770 |
+
key = "c:" + arg
|
| 1771 |
+
else:
|
| 1772 |
+
return
|
| 1773 |
+
actor = await self.gate(event, None, "")
|
| 1774 |
+
if not actor:
|
| 1775 |
+
return
|
| 1776 |
+
if key == "warn":
|
| 1777 |
+
await self.db.set_group(chat, autowarn=not lock)
|
| 1778 |
+
await self.say(event, "تم تعطيل نظام التحذير التلقائي." if lock
|
| 1779 |
+
else "تم تفعيل نظام التحذير التلقائي.")
|
| 1780 |
+
return
|
| 1781 |
+
g = await self.db.group(chat)
|
| 1782 |
+
locks = list(g["locks"])
|
| 1783 |
+
if lock and key not in locks:
|
| 1784 |
+
locks.append(key)
|
| 1785 |
+
elif not lock and key in locks:
|
| 1786 |
+
locks.remove(key)
|
| 1787 |
+
await self.db.set_group(chat, locks=locks)
|
| 1788 |
+
name = LOCK_NAME_AR.get(key, arg)
|
| 1789 |
+
await self.say(event, ("تم قفل %s." if lock else "تم فتح %s.") % esc(name))
|
| 1790 |
+
|
| 1791 |
+
async def c_enable(self, event: Any, arg: str, enable: bool) -> None:
|
| 1792 |
+
if LOCK_ALIAS.get(arg) != "warn":
|
| 1793 |
+
return
|
| 1794 |
+
actor = await self.gate(event, None, "")
|
| 1795 |
+
if not actor:
|
| 1796 |
+
return
|
| 1797 |
+
await self.db.set_group(event.chat_id, autowarn=enable)
|
| 1798 |
+
await self.say(event, "تم تفعيل نظام التحذير التلقائي." if enable
|
| 1799 |
+
else "تم تعطيل نظام التحذير التلقائي.")
|
| 1800 |
+
|
| 1801 |
+
# ---- banned words
|
| 1802 |
+
async def c_addword(self, event: Any, arg: str) -> None:
|
| 1803 |
+
actor = await self.gate(event, None, arg)
|
| 1804 |
+
if not actor:
|
| 1805 |
+
return
|
| 1806 |
+
if not arg:
|
| 1807 |
+
await self.say(event, "اكتب: اضف كلمة + الكلمة.")
|
| 1808 |
+
return
|
| 1809 |
+
if len(arg) > 60:
|
| 1810 |
+
await self.say(event, "الكلمة طويلة جداً.")
|
| 1811 |
+
return
|
| 1812 |
+
g = await self.db.group(event.chat_id)
|
| 1813 |
+
words = list(g["banned_words"])
|
| 1814 |
+
if arg in words:
|
| 1815 |
+
await self.say(event, "الكلمة موجودة مسبقاً.")
|
| 1816 |
+
return
|
| 1817 |
+
if len(words) >= 500:
|
| 1818 |
+
await self.say(event, "وصلت للحد الأقصى من الكلمات.")
|
| 1819 |
+
return
|
| 1820 |
+
words.append(arg)
|
| 1821 |
+
await self.db.set_group(event.chat_id, banned_words=words)
|
| 1822 |
+
await self.say(event, "تمت إضافة الكلمة.")
|
| 1823 |
+
|
| 1824 |
+
async def c_delword(self, event: Any, arg: str) -> None:
|
| 1825 |
+
actor = await self.gate(event, None, arg)
|
| 1826 |
+
if not actor:
|
| 1827 |
+
return
|
| 1828 |
+
g = await self.db.group(event.chat_id)
|
| 1829 |
+
words = list(g["banned_words"])
|
| 1830 |
+
if arg not in words:
|
| 1831 |
+
await self.say(event, "الكلمة غير موجودة.")
|
| 1832 |
+
return
|
| 1833 |
+
words.remove(arg)
|
| 1834 |
+
await self.db.set_group(event.chat_id, banned_words=words)
|
| 1835 |
+
await self.say(event, "تم حذف الكلمة.")
|
| 1836 |
+
|
| 1837 |
+
async def c_words(self, event: Any, arg: str) -> None:
|
| 1838 |
+
actor = await self.gate(event, None, arg)
|
| 1839 |
+
if not actor:
|
| 1840 |
+
return
|
| 1841 |
+
g = await self.db.group(event.chat_id)
|
| 1842 |
+
if not g["banned_words"]:
|
| 1843 |
+
await self.say(event, "لا توجد كلمات ممنوعة.")
|
| 1844 |
+
return
|
| 1845 |
+
lines = ["<b>الكلمات الممنوعة:</b>"] + ["• " + esc(w) for w in g["banned_words"]]
|
| 1846 |
+
await self.say(event, join_limited(lines))
|
| 1847 |
+
|
| 1848 |
+
# ---- messages
|
| 1849 |
+
async def c_delete(self, event: Any, arg: str) -> None:
|
| 1850 |
+
actor = await self.gate(event, "delete_messages", arg)
|
| 1851 |
+
if not actor:
|
| 1852 |
+
return
|
| 1853 |
+
msg = event.message
|
| 1854 |
+
if msg.is_reply:
|
| 1855 |
+
ids = [msg.reply_to_msg_id, msg.id]
|
| 1856 |
+
elif arg.isdigit() and int(arg) > 0:
|
| 1857 |
+
n = min(int(arg), 100)
|
| 1858 |
+
ids = list(range(max(1, msg.id - n), msg.id + 1))
|
| 1859 |
+
else:
|
| 1860 |
+
await self.say(event, "رد على رسالة أو اكتب: مسح 10")
|
| 1861 |
+
return
|
| 1862 |
+
await self.client.delete_messages(event.chat_id, ids)
|
| 1863 |
+
|
| 1864 |
+
async def c_pin(self, event: Any, arg: str, pin: bool) -> None:
|
| 1865 |
+
actor = await self.gate(event, "pin_messages", arg)
|
| 1866 |
+
if not actor:
|
| 1867 |
+
return
|
| 1868 |
+
msg = event.message
|
| 1869 |
+
if pin:
|
| 1870 |
+
if not msg.is_reply:
|
| 1871 |
+
await self.say(event, "رد على الرسالة المراد تثبيتها.")
|
| 1872 |
+
return
|
| 1873 |
+
await self.client.pin_message(event.chat_id, msg.reply_to_msg_id)
|
| 1874 |
+
await self.say(event, "تم تثبيت الرسالة.")
|
| 1875 |
+
else:
|
| 1876 |
+
if msg.is_reply:
|
| 1877 |
+
await self.client.unpin_message(event.chat_id, msg.reply_to_msg_id)
|
| 1878 |
+
await self.say(event, "تم إلغاء تثبيت الرسالة.")
|
| 1879 |
+
else:
|
| 1880 |
+
await self.client.unpin_message(event.chat_id)
|
| 1881 |
+
await self.say(event, "تم إلغاء تثبيت جميع الرسائل.")
|
| 1882 |
+
|
| 1883 |
+
# ---- settings commands
|
| 1884 |
+
async def c_rule(self, event: Any, arg: str) -> None:
|
| 1885 |
+
actor = await self.gate(event, None, arg)
|
| 1886 |
+
if not actor:
|
| 1887 |
+
return
|
| 1888 |
+
m = re.match(r"^(\d{1,2})\s+(\S+)$", arg)
|
| 1889 |
+
act = ACTION_FROM_AR.get(m.group(2)) if m else None
|
| 1890 |
+
if not m or act is None or not (1 <= int(m.group(1)) <= 10):
|
| 1891 |
+
await self.say(event, "مثال: عقوبة 3 كتم\nالإجراءات: كتم، تقييد، طرد، حظر، بدون")
|
| 1892 |
+
return
|
| 1893 |
+
g = await self.db.group(event.chat_id)
|
| 1894 |
+
rules = dict(g["warn_rules"])
|
| 1895 |
+
if act == "none":
|
| 1896 |
+
rules.pop(str(int(m.group(1))), None)
|
| 1897 |
+
else:
|
| 1898 |
+
rules[str(int(m.group(1)))] = act
|
| 1899 |
+
await self.db.set_group(event.chat_id, warn_rules=rules)
|
| 1900 |
+
await self.say(event, "تم تحديث العقوبة.")
|
| 1901 |
+
|
| 1902 |
+
async def c_setspam(self, event: Any, arg: str) -> None:
|
| 1903 |
+
actor = await self.gate(event, None, arg)
|
| 1904 |
+
if not actor:
|
| 1905 |
+
return
|
| 1906 |
+
m = re.match(r"^(\d+)\s+(\d+)$", arg)
|
| 1907 |
+
if not m:
|
| 1908 |
+
await self.say(event, "مثال: ضبط التكرار 6 5\n(عدد الرسائل ثم الثواني)")
|
| 1909 |
+
return
|
| 1910 |
+
c, s = int(m.group(1)), int(m.group(2))
|
| 1911 |
+
c = min(max(c, BOUNDS["spam_count"][0]), BOUNDS["spam_count"][1])
|
| 1912 |
+
s = min(max(s, BOUNDS["spam_seconds"][0]), BOUNDS["spam_seconds"][1])
|
| 1913 |
+
await self.db.set_group(event.chat_id, spam_count=c, spam_seconds=s)
|
| 1914 |
+
await self.say(event, "تم الضبط: %d رسائل خلال %d ثواني." % (c, s))
|
| 1915 |
+
|
| 1916 |
+
async def c_setmute(self, event: Any, arg: str) -> None:
|
| 1917 |
+
actor = await self.gate(event, None, arg)
|
| 1918 |
+
if not actor:
|
| 1919 |
+
return
|
| 1920 |
+
if not arg.isdigit():
|
| 1921 |
+
await self.say(event, "مثال: ضبط الكتم 60 (بالدقائق)")
|
| 1922 |
+
return
|
| 1923 |
+
lo, hi = BOUNDS["mute_minutes"]
|
| 1924 |
+
v = min(max(int(arg), lo), hi)
|
| 1925 |
+
await self.db.set_group(event.chat_id, mute_minutes=v)
|
| 1926 |
+
await self.say(event, "تم الضبط: مدة الكتم %d دقيقة." % v)
|
| 1927 |
+
|
| 1928 |
+
# ---- inline callbacks
|
| 1929 |
+
async def cb_is_admin(self, event: Any) -> bool:
|
| 1930 |
+
try:
|
| 1931 |
+
peer = await event.get_input_sender()
|
| 1932 |
+
except Exception:
|
| 1933 |
+
peer = event.sender_id
|
| 1934 |
+
inf = await self.perms.info(event.chat_id, peer, event.sender_id)
|
| 1935 |
+
return inf.is_admin
|
| 1936 |
+
|
| 1937 |
+
@staticmethod
|
| 1938 |
+
async def _edit(event: Any, text: str, rows: List[List[Any]]) -> None:
|
| 1939 |
+
try:
|
| 1940 |
+
await event.edit(text, buttons=rows, link_preview=False)
|
| 1941 |
+
except RPCError as e:
|
| 1942 |
+
if "MESSAGE_NOT_MODIFIED" not in (getattr(e, "message", "") or "").upper():
|
| 1943 |
+
log.debug("edit failed: %s", getattr(e, "message", e))
|
| 1944 |
+
|
| 1945 |
+
async def on_callback(self, event: Any) -> None:
|
| 1946 |
+
try:
|
| 1947 |
+
data = (event.data or b"").decode("utf-8", "ignore")
|
| 1948 |
+
if event.chat_id is None or event.is_private or data == "noop":
|
| 1949 |
+
await event.answer()
|
| 1950 |
+
return
|
| 1951 |
+
parts = data.split(":")
|
| 1952 |
+
if data == "m:main":
|
| 1953 |
+
text, rows = self.menu.main()
|
| 1954 |
+
await self._edit(event, text, rows)
|
| 1955 |
+
elif len(parts) == 3 and parts[0] == "m" and parts[1] == "s":
|
| 1956 |
+
idx = int(parts[2])
|
| 1957 |
+
if not 0 <= idx < len(SECTIONS):
|
| 1958 |
+
await event.answer()
|
| 1959 |
+
return
|
| 1960 |
+
if SECTIONS[idx][2] and not await self.cb_is_admin(event):
|
| 1961 |
+
await event.answer("هذا القسم للمشرفين فقط.", alert=True)
|
| 1962 |
+
return
|
| 1963 |
+
text, rows = await self.menu.section(idx, event.chat_id)
|
| 1964 |
+
await self._edit(event, text, rows)
|
| 1965 |
+
elif parts[0] == "st" and len(parts) >= 3:
|
| 1966 |
+
if not await self.cb_is_admin(event):
|
| 1967 |
+
await event.answer("للمشرفين فقط.", alert=True)
|
| 1968 |
+
return
|
| 1969 |
+
await self.menu.apply_setting(parts, event.chat_id)
|
| 1970 |
+
idx = [s[0] for s in SECTIONS].index("settings")
|
| 1971 |
+
text, rows = await self.menu.section(idx, event.chat_id)
|
| 1972 |
+
await self._edit(event, text, rows)
|
| 1973 |
+
await event.answer()
|
| 1974 |
+
except FloodWaitError:
|
| 1975 |
+
pass
|
| 1976 |
+
except Exception:
|
| 1977 |
+
log.exception("on_callback failed")
|
| 1978 |
+
try:
|
| 1979 |
+
await event.answer()
|
| 1980 |
+
except Exception:
|
| 1981 |
+
pass
|
| 1982 |
+
|
| 1983 |
+
|
| 1984 |
+
# ============================================================================
|
| 1985 |
+
# Health server (Hugging Face Spaces needs an open port) & main
|
| 1986 |
+
# ============================================================================
|
| 1987 |
+
|
| 1988 |
+
async def health_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
| 1989 |
+
try:
|
| 1990 |
+
await asyncio.wait_for(reader.read(1024), timeout=5)
|
| 1991 |
+
body = b"Bot is running"
|
| 1992 |
+
writer.write(
|
| 1993 |
+
b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"
|
| 1994 |
+
b"Content-Length: %d\r\nConnection: close\r\n\r\n" % len(body) + body
|
| 1995 |
+
)
|
| 1996 |
+
await writer.drain()
|
| 1997 |
+
except Exception:
|
| 1998 |
+
pass
|
| 1999 |
+
finally:
|
| 2000 |
+
try:
|
| 2001 |
+
writer.close()
|
| 2002 |
+
except Exception:
|
| 2003 |
+
pass
|
| 2004 |
+
|
| 2005 |
+
|
| 2006 |
+
async def janitor(bot: Bot) -> None:
|
| 2007 |
+
while True:
|
| 2008 |
+
await asyncio.sleep(300)
|
| 2009 |
+
try:
|
| 2010 |
+
bot.protection.flood.purge()
|
| 2011 |
+
now = time.time()
|
| 2012 |
+
for k in [k for k, p in bot.customs.pending.items() if p["exp"] < now]:
|
| 2013 |
+
bot.customs.pending.pop(k, None)
|
| 2014 |
+
except Exception:
|
| 2015 |
+
log.debug("janitor error", exc_info=True)
|
| 2016 |
+
|
| 2017 |
+
|
| 2018 |
+
def require_env() -> Tuple[int, str, str, str]:
|
| 2019 |
+
names = ("API_ID", "API_HASH", "BOT_TOKEN", "MONGO_URI")
|
| 2020 |
+
missing = [n for n in names if not os.environ.get(n, "").strip()]
|
| 2021 |
+
if missing:
|
| 2022 |
+
print("Missing environment variables: " + ", ".join(missing), file=sys.stderr)
|
| 2023 |
+
sys.exit(1)
|
| 2024 |
+
try:
|
| 2025 |
+
api_id = int(os.environ["API_ID"].strip())
|
| 2026 |
+
except ValueError:
|
| 2027 |
+
print("API_ID must be an integer.", file=sys.stderr)
|
| 2028 |
+
sys.exit(1)
|
| 2029 |
+
return (api_id, os.environ["API_HASH"].strip(),
|
| 2030 |
+
os.environ["BOT_TOKEN"].strip(), os.environ["MONGO_URI"].strip())
|
| 2031 |
+
|
| 2032 |
+
|
| 2033 |
+
async def main() -> None:
|
| 2034 |
+
print("Starting bot...", flush=True)
|
| 2035 |
+
api_id, api_hash, bot_token, mongo_uri = require_env()
|
| 2036 |
+
|
| 2037 |
+
port = int(os.environ.get("PORT", "7860"))
|
| 2038 |
+
server = await asyncio.start_server(health_handler, "0.0.0.0", port)
|
| 2039 |
+
log.info("Health server listening on port %d", port)
|
| 2040 |
+
|
| 2041 |
+
db = Database(mongo_uri)
|
| 2042 |
+
while True:
|
| 2043 |
+
try:
|
| 2044 |
+
await db.init()
|
| 2045 |
+
break
|
| 2046 |
+
except Exception as e:
|
| 2047 |
+
log.error("MongoDB connection failed (%s). Retrying in 10s...", type(e).__name__)
|
| 2048 |
+
await asyncio.sleep(10)
|
| 2049 |
+
log.info("MongoDB connected")
|
| 2050 |
+
|
| 2051 |
+
client = TelegramClient(
|
| 2052 |
+
StringSession(), api_id, api_hash,
|
| 2053 |
+
flood_sleep_threshold=60, connection_retries=-1, retry_delay=2,
|
| 2054 |
+
)
|
| 2055 |
+
client.parse_mode = "html"
|
| 2056 |
+
bot = Bot(client, db)
|
| 2057 |
+
bot.register()
|
| 2058 |
+
|
| 2059 |
+
while True:
|
| 2060 |
+
try:
|
| 2061 |
+
await client.start(bot_token=bot_token)
|
| 2062 |
+
break
|
| 2063 |
+
except FloodWaitError as e:
|
| 2064 |
+
log.warning("FloodWait on login: sleeping %ds", e.seconds)
|
| 2065 |
+
await asyncio.sleep(e.seconds + 1)
|
| 2066 |
+
except Exception as e:
|
| 2067 |
+
log.error("Login failed (%s). Retrying in 10s...", type(e).__name__)
|
| 2068 |
+
await asyncio.sleep(10)
|
| 2069 |
+
|
| 2070 |
+
bot.me = await client.get_me()
|
| 2071 |
+
bot.perms.me_id = bot.me.id
|
| 2072 |
+
log.info("Bot started as @%s", bot.me.username)
|
| 2073 |
+
|
| 2074 |
+
task = asyncio.create_task(janitor(bot))
|
| 2075 |
+
try:
|
| 2076 |
+
while True: # never exit: keep the container alive and reconnect if needed
|
| 2077 |
+
try:
|
| 2078 |
+
await client.run_until_disconnected()
|
| 2079 |
+
log.warning("Client disconnected; reconnecting in 5s")
|
| 2080 |
+
except Exception:
|
| 2081 |
+
log.exception("Connection loop error; reconnecting in 5s")
|
| 2082 |
+
await asyncio.sleep(5)
|
| 2083 |
+
try:
|
| 2084 |
+
if not client.is_connected():
|
| 2085 |
+
await client.connect()
|
| 2086 |
+
except Exception:
|
| 2087 |
+
log.exception("Reconnect failed")
|
| 2088 |
+
finally:
|
| 2089 |
+
task.cancel()
|
| 2090 |
+
server.close()
|
| 2091 |
+
|
| 2092 |
+
|
| 2093 |
+
if __name__ == "__main__":
|
| 2094 |
+
try:
|
| 2095 |
+
if uvloop is not None and hasattr(uvloop, "run"):
|
| 2096 |
+
uvloop.run(main())
|
| 2097 |
+
else:
|
| 2098 |
+
asyncio.run(main())
|
| 2099 |
+
except KeyboardInterrupt:
|
| 2100 |
+
pass
|