Oth1 commited on
Commit
ccd4e1a
·
verified ·
1 Parent(s): 6f9ef33

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +804 -1
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