Oth1 commited on
Commit
5e5fbfa
ยท
verified ยท
1 Parent(s): d4f93d3

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +4 -12
  2. main.py +1452 -550
  3. requirements.txt +1 -6
Dockerfile CHANGED
@@ -1,10 +1,7 @@
1
  FROM python:3.11-slim
2
 
3
- # tesseract ู…ุทู„ูˆุจ ู„ู‚ุฑุงุกุฉ ุงู„ุฃุฑู‚ุงู… ู…ู† ุตูˆุฑ ุงู„ุญุณุงุจุงุช (ุงู„ู„ูŠูู„ / ุงู„ุฌูˆุงู‡ุฑ)
4
- RUN apt-get update && apt-get install -y --no-install-recommends \
5
- tesseract-ocr \
6
- tesseract-ocr-ara \
7
- && rm -rf /var/lib/apt/lists/*
8
 
9
  WORKDIR /app
10
 
@@ -13,12 +10,7 @@ RUN pip install --no-cache-dir -r requirements.txt
13
 
14
  COPY main.py .
15
 
16
- # ู…ุฌู„ุฏ ุงู„ุฌู„ุณุงุช (telethon ุณูŠุดู† ู„ู„ู€ user client ูˆุงู„ู€ bot client) ู„ุงุฒู… ูŠูƒูˆู†
17
- # ุนู„ู‰ volume ุฏุงุฆู… ุนุดุงู† ู…ุง ูŠุทู„ุจ ุชุณุฌูŠู„ ุฏุฎูˆู„ ู…ู† ุฌุฏูŠุฏ ูƒู„ ู…ุฑุฉ ูŠุนุงุฏ ุชุดุบูŠู„ ุงู„ุญุงูˆูŠุฉ
18
- VOLUME ["/app/session"]
19
- ENV TELEGRAM_SESSION=/app/session/bounty_bot
20
-
21
- # ู„ุงุฒู… ุชู…ุฑุฑ ูˆู‚ุช ุงู„ุชุดุบูŠู„: TELEGRAM_API_ID, TELEGRAM_API_HASH, BOT_TOKEN,
22
- # OWNER_ID (ุงูุชุฑุงุถูŠ 6069597294), MONGO_URI
23
 
24
  CMD ["python", "main.py"]
 
1
  FROM python:3.11-slim
2
 
3
+ ENV PYTHONUNBUFFERED=1 \
4
+ PYTHONDONTWRITEBYTECODE=1
 
 
 
5
 
6
  WORKDIR /app
7
 
 
10
 
11
  COPY main.py .
12
 
13
+ # Writable dir for the Telethon session file (HF Spaces containers run as non-root by default)
14
+ RUN mkdir -p /app/data && chmod -R 777 /app
 
 
 
 
 
15
 
16
  CMD ["python", "main.py"]
main.py CHANGED
@@ -1,661 +1,1563 @@
 
 
1
  """
2
- main.py
3
- -------
4
- ุจูˆุช ุชุณุนูŠุฑ ุญุณุงุจุงุช Bounty Rush - ู…ุฑูƒุฒูŠุŒ ูŠุณุชุฎุฏู…ู‡ ู…ุงู„ูƒ ูˆุงุญุฏ ุจุณ (OWNER_ID).
5
-
6
- ูƒู„ ุงู„ุงุชุตุงู„ ุจุชูŠู„ูŠุฌุฑุงู… ุนุจุฑ Telethon (MTProto ุฎุงู„ุตุŒ ุจุฏูˆู† aiogram ุฃูˆ ุฃูŠ
7
- ู…ูƒุชุจุฉ Bot API ุนุจุฑ HTTP).
8
-
9
- ููƒุฑุฉ ุงู„ุนู…ู„:
10
- - bot_client: ุจูˆุช ุชูŠู„ูŠุฌุฑุงู… ุญู‚ูŠู‚ูŠ (BOT_TOKEN ู…ู† BotFather) ุชูƒู„ู…ู‡ ุฃู†ุช ูˆุชุถุบุท
11
- ุฃุฒุฑุงุฑู‡. ู…ู‚ููˆู„ ุนู„ูŠูƒ ุจุณ (OWNER_ID).
12
- - ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ (Monitoring Accounts): ุญุณุงุจุงุช ุชูŠู„ูŠุฌุฑุงู… ุนุงุฏูŠุฉ (ุฃุฑู‚ุงู…
13
- ู‡ูˆุงุชู) ุชู†ุถุงู *ู…ู† ุฏุงุฎู„ ุงู„ุจูˆุช ู†ูุณู‡* (ุจุฏูˆู† ุฃูŠ ุณูƒุฑูŠุจุช ุฎุงุฑุฌูŠ ุฃูˆ ุทุฑููŠุฉ) -
14
- ุชูƒุชุจ ุฑู‚ู… ุงู„ู‡ุงุชู ุจุงู„ุดุงุชุŒ ูŠูˆุตู„ูƒ ูƒูˆุฏ ุชูƒุชุจู‡ ุจุงู„ุดุงุชุŒ ูˆุฎู„ุงุต. ุชู‚ุฏุฑ ุชุถูŠู ุฃูƒุซุฑ
15
- ู…ู† ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ูˆุญุฏุฉุŒ ูˆูƒู„ ู‚ู†ุงุฉ ุชู†ุถุงู ุชูˆุฒุน ุชู„ู‚ุงุฆูŠู‹ุง (round-robin) ุนู„ู‰
16
- ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู…ุนูŠู† ูŠุชูƒูู„ ุจู…ุฑุงู‚ุจุชู‡ุง ูˆุณุญุจ ุชุงุฑูŠุฎู‡ุง.
17
-
18
- ุฌู„ุณุงุช ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ุชู†ุฎุฒู† ูƒู€ ู†ุต (StringSession) ุฏุงุฎู„ MongoDB ู†ูุณู‡ุง -
19
- ู…ุง ุชุญุชุงุฌ ุฃูŠ ู…ู„ู ุฌู„ุณุฉ ูˆู„ุง volume ุฎุงุต ููŠู‡ุงุŒ ูƒู„ ุดูŠ ู…ุฑูƒุฒูŠ ุจู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช.
20
-
21
- ุฃุฒุฑุงุฑ /start:
22
- ๐Ÿ“– ุงู„ุดุฑุญ
23
- ๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ -> ูŠุจุฏุฃ ู…ุญุงุฏุซุฉ: ุฑู‚ู… ุงู„ู‡ุงุชู -> ูƒูˆุฏ ุงู„ุชุญู‚ู‚ ->
24
- (ูƒู„ู…ุฉ ุณุฑ ู„ูˆ ุนู†ุฏูƒ ุชุญู‚ู‚ ุจุฎุทูˆุชูŠู†) -> ูŠุถูŠู
25
- ุงู„ุญุณุงุจ ูƒุญุณุงุจ ู…ุฑุงู‚ุจุฉ ุฌุฏูŠุฏ.
26
- โž• ุฅุถุงูุฉ ู‚ู†ุงุฉ ู…ุฑุงู‚ุจุฉ -> ูŠุทู„ุจ ูŠูˆุฒุฑ ุงู„ู‚ู†ุงุฉุŒ ูŠูˆุฒุนู‡ุง ุนู„ู‰ ุฃุญุฏ ุญุณุงุจุงุช
27
- ุงู„ู…ุฑุงู‚ุจุฉ ุงู„ู…ุถุงูุฉ (ู„ุงุฒู… ูŠูƒูˆู† ุนู†ุฏูƒ ุญุณุงุจ
28
- ู…ุฑุงู‚ุจุฉ ูˆุงุญุฏ ุนู„ู‰ ุงู„ุฃู‚ู„).
29
- ๐Ÿ“… ุชุนูŠูŠู† ุชุงุฑูŠุฎ ุงู„ู…ุฑุงู‚ุจุฉ -> ูŠุทู„ุจ ุชุงุฑูŠุฎ ุจุตูŠุบุฉ YYYY|MM|DDุŒ ูˆูŠุจุฏุฃ ูŠุณุญุจ
30
- ูƒู„ ู…ู†ุดูˆุฑ ููŠู‡ ุตูˆุฑุฉ ูˆุณุนุฑ ู…ู† ูƒู„ ุงู„ู‚ู†ูˆุงุช
31
- ุงู„ู…ุถุงูุฉุŒ ู…ู† ู‡ุฐุง ุงู„ุชุงุฑูŠุฎ ู„ู„ูŠูˆู….
32
- ๐ŸŽฏ ุชู‚ูŠูŠู… ุญุณุงุจ -> ูŠุทู„ุจ ุตูˆุฑุฉ ุณูƒุฑูŠู† ุดูˆุชุŒ ูˆูŠุฑุฏ ุจุณุนุฑ ุชู‚ุฑูŠุจูŠ.
33
-
34
- ุงู„ู…ุชุบูŠุฑุงุช ุงู„ู…ุทู„ูˆุจุฉ (ุจูŠุฆุฉ ุงู„ุชุดุบูŠู„):
35
- TELEGRAM_API_ID - ู…ู† my.telegram.org
36
- TELEGRAM_API_HASH - ู…ู† my.telegram.org
37
- TELEGRAM_SESSION - ู…ุณุงุฑ ู…ู„ู ุฌู„ุณุฉ ุงู„ุจูˆุช ูู‚ุท (ุงูุชุฑุงุถูŠ: /app/session/bounty_bot)
38
- BOT_TOKEN - ุชูˆูƒู† ุจูˆุช ู…ู† @BotFather (ูŠุณุฌู„ ุฏุฎูˆู„ ุนุจุฑ MTProto ู…ุจุงุดุฑุฉ)
39
- OWNER_ID - ุขูŠุฏูŠ ุงู„ู…ุณุชุฎุฏู… ุงู„ู…ุณู…ูˆุญ ู„ู‡ ูˆุญุฏู‡ ูŠุณุชุฎุฏู… ุงู„ุจูˆุช (ุงูุชุฑุงุถูŠ: 6069597294)
40
- MONGO_URI - ุฑุงุจุท ุงุชุตุงู„ MongoDB
41
- MONGO_DB_NAME - ุงุณู… ู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช (ุงูุชุฑุงุถูŠ: bounty_pricer)
42
- SIMILARITY_TOP_K - ุนุฏุฏ ุฃู‚ุฑุจ ุงู„ู†ุชุงุฆุฌ ุจุญุณุงุจ ู…ุชูˆุณุท ุงู„ุณุนุฑ (ุงูุชุฑุงุถูŠ: 5)
43
-
44
- ู…ู„ุงุญุธุฉ: bot_client ูู‚ุท ูŠุญุชุงุฌ ู…ู„ู ุฌู„ุณุฉ ุนู„ู‰ volume (ุชุณุฌูŠู„ ุฏุฎูˆู„ู‡ ุจุชูˆูƒู†ุŒ
45
- ู…ุง ูŠุญุชุงุฌ ุชูุงุนู„ ุจุดุฑูŠ). ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ูƒู„ู‡ุง ุชู†ุถุงู ูˆุชู†ุฎุฒู† ุนุจุฑ ุงู„ู…ุญุงุฏุซุฉ
46
- ุฏุงุฎู„ MongoDBุŒ ูู…ุง ููŠู‡ ุฃูŠ ุญุงุฌุฉ ู„ุทุฑููŠุฉ ุชูุงุนู„ูŠุฉ ุฃูˆ ุณูƒุฑูŠุจุช ุฎุงุฑุฌูŠ.
47
  """
48
 
49
  import os
50
- import re
51
- import io
52
- import asyncio
53
  import logging
54
- from datetime import datetime, timezone
55
-
56
- import numpy as np
57
- from PIL import Image
58
- import pytesseract
59
- import torch
60
- import open_clip
61
- from pymongo import MongoClient, ReturnDocument
62
 
63
  from telethon import TelegramClient, events, Button
64
- from telethon.sessions import StringSession
65
- from telethon.tl.functions.channels import JoinChannelRequest
66
  from telethon.errors import (
67
- SessionPasswordNeededError,
68
- PhoneCodeInvalidError,
69
- PhoneCodeExpiredError,
70
- PasswordHashInvalidError,
71
- FloodWaitError,
 
 
 
 
 
 
 
 
 
 
 
 
72
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
75
- log = logging.getLogger("bounty_bot")
76
-
77
- # ---------------------------------------------------------------------------
78
- # ุงู„ุฅุนุฏุงุฏุงุช
79
- # ---------------------------------------------------------------------------
80
- API_ID = int(os.environ["TELEGRAM_API_ID"])
81
- API_HASH = os.environ["TELEGRAM_API_HASH"]
82
- BOT_SESSION_PATH = os.environ.get("TELEGRAM_SESSION", "bounty_bot")
83
-
84
- BOT_TOKEN = os.environ["BOT_TOKEN"]
85
- OWNER_ID = int(os.environ.get("OWNER_ID", "6069597294"))
86
-
87
- MONGO_URI = os.environ["MONGO_URI"]
88
- MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "bounty_pricer")
89
- TOP_K = int(os.environ.get("SIMILARITY_TOP_K", "5"))
90
-
91
- # ---------------------------------------------------------------------------
92
- # MongoDB
93
- # ---------------------------------------------------------------------------
94
- mongo_client = MongoClient(MONGO_URI)
95
- db = mongo_client[MONGO_DB_NAME]
96
- listings = db["listings"] # ูƒู„ ูˆุซูŠู‚ุฉ = ุฅุนู„ุงู†/ุญุณุงุจ (ุตูˆุฑุฉ ู…ุญู„ู„ุฉ + ุณุนุฑ + ุจุตู…ุฉ)
97
- channels_col = db["channels"] # ุงู„ู‚ู†ูˆุงุช ุงู„ู…ุถุงูุฉ + ุงู„ุญุณุงุจ ุงู„ู…ุณุคูˆู„ ุนู† ูƒู„ ูˆุญุฏุฉ
98
- accounts_col = db["accounts"] # ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ (StringSession ู…ุฎุฒู†ุฉ ุจู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช)
99
- config_col = db["config"] # ุฅุนุฏุงุฏุงุช ุนุงู…ุฉ (ุชุงุฑูŠุฎ ุจุฏุงูŠุฉ ุงู„ู…ุฑุงู‚ุจุฉุŒ ุนุฏุงุฏ ุงู„ุชูˆุฒูŠุน)
100
-
101
- # ---------------------------------------------------------------------------
102
- # ู†ู…ูˆุฐุฌ CLIP (ุจุตู…ุฉ ุจุตุฑูŠุฉ ู…ุฌุงู†ูŠุฉุŒ ุจุฏูˆู† API key) + OCR
103
- # ---------------------------------------------------------------------------
104
- _DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
105
- log.info("ุชุญู…ูŠู„ ู†ู…ูˆุฐุฌ CLIP ุนู„ู‰ %s ...", _DEVICE)
106
- _CLIP_MODEL, _, _CLIP_PREPROCESS = open_clip.create_model_and_transforms(
107
- "ViT-B-32", pretrained="openai"
108
  )
109
- _CLIP_MODEL.eval().to(_DEVICE)
110
- log.info("ุชู… ุชุญู…ูŠู„ ู†ู…ูˆุฐุฌ CLIP.")
111
 
 
112
 
113
- def get_image_embedding(image: Image.Image) -> np.ndarray:
114
- image = image.convert("RGB")
115
- tensor = _CLIP_PREPROCESS(image).unsqueeze(0).to(_DEVICE)
116
- with torch.no_grad():
117
- features = _CLIP_MODEL.encode_image(tensor)
118
- features = features / features.norm(dim=-1, keepdim=True)
119
- return features.cpu().numpy().flatten()
120
 
 
 
 
 
121
 
122
- def cosine_similarity(a, b) -> float:
123
- a = np.array(a, dtype=np.float32)
124
- b = np.array(b, dtype=np.float32)
125
- denom = np.linalg.norm(a) * np.linalg.norm(b)
126
- if denom == 0:
127
- return 0.0
128
- return float(np.dot(a, b) / denom)
129
 
 
 
130
 
131
- LEVEL_KEYWORDS = ["level", "lv", "lvl", "ู„ูŠูู„", "ู…ุณุชูˆู‰"]
132
- GEMS_KEYWORDS = ["gem", "gems", "ุฌูˆู‡ุฑ", "ุฌูˆุงู‡ุฑ", "ุงู„ู…ุงุณ"]
133
 
 
 
 
 
 
 
 
134
 
135
- def ocr_extract_text(image: Image.Image) -> str:
136
- try:
137
- return pytesseract.image_to_string(image, lang="eng+ara")
138
- except Exception:
139
- return pytesseract.image_to_string(image, lang="eng")
140
-
141
-
142
- def _find_number_near_keyword(text: str, keywords):
143
- for line in text.lower().splitlines():
144
- for kw in keywords:
145
- if kw in line:
146
- numbers = re.findall(r"\d[\d,]*", line)
147
- if numbers:
148
- try:
149
- return int(numbers[0].replace(",", ""))
150
- except ValueError:
151
- continue
152
- return None
153
 
 
 
 
 
 
 
 
 
154
 
155
- def extract_specs_from_image(image: Image.Image) -> dict:
156
- text = ocr_extract_text(image)
157
- return {
158
- "level": _find_number_near_keyword(text, LEVEL_KEYWORDS),
159
- "gems": _find_number_near_keyword(text, GEMS_KEYWORDS),
160
- "ocr_text": text.strip()[:500],
161
- }
162
 
 
 
 
163
 
164
- def extract_price_from_text(text: str):
165
- if not text:
166
- return None
167
- text_l = text.lower()
168
- patterns = [
169
- r"\$\s?(\d[\d,\.]*)",
170
- r"(\d[\d,\.]*)\s?\$",
171
- r"(\d[\d,\.]*)\s?(?:ุฏูˆู„ุงุฑ|usd)",
172
- r"(\d[\d,\.]*)\s?(?:ุฏูŠู†ุงุฑ|iqd)",
173
- ]
174
- for p in patterns:
175
- m = re.search(p, text_l)
176
- if m:
177
- try:
178
- return float(m.group(1).replace(",", ""))
179
- except ValueError:
180
- continue
181
- return None
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- def find_similar_and_price(embedding: np.ndarray, specs: dict) -> dict:
185
- all_docs = list(listings.find({"price": {"$ne": None}, "embedding": {"$exists": True}}))
186
- if not all_docs:
187
- return {"matches": [], "estimated_price": None}
188
-
189
- scored = []
190
- for doc in all_docs:
191
- visual_sim = cosine_similarity(embedding, doc["embedding"])
192
- numeric_bonus, numeric_count = 0.0, 0
193
- if specs.get("level") is not None and doc.get("level") is not None:
194
- diff = abs(specs["level"] - doc["level"])
195
- numeric_bonus += max(0.0, 1 - diff / max(specs["level"], doc["level"], 1))
196
- numeric_count += 1
197
- if specs.get("gems") is not None and doc.get("gems") is not None:
198
- diff = abs(specs["gems"] - doc["gems"])
199
- numeric_bonus += max(0.0, 1 - diff / max(specs["gems"], doc["gems"], 1))
200
- numeric_count += 1
201
- final_score = (
202
- 0.6 * visual_sim + 0.4 * (numeric_bonus / numeric_count)
203
- if numeric_count > 0
204
- else visual_sim
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  )
206
- scored.append((final_score, doc))
207
-
208
- scored.sort(key=lambda x: x[0], reverse=True)
209
- top = scored[:TOP_K]
210
- if not top:
211
- return {"matches": [], "estimated_price": None}
212
-
213
- weights = [max(s, 0.0001) for s, _ in top]
214
- prices = [d["price"] for _, d in top]
215
- estimated_price = sum(w * p for w, p in zip(weights, prices)) / sum(weights)
216
-
217
- matches = [
218
- {
219
- "similarity_percent": round(s * 100, 1),
220
- "price": d["price"],
221
- "level": d.get("level"),
222
- "gems": d.get("gems"),
223
- "source_channel": d.get("source_channel"),
224
- }
225
- for s, d in top
226
- ]
227
- return {"matches": matches, "estimated_price": round(estimated_price, 2)}
228
 
 
 
 
 
 
 
 
 
229
 
230
- # ---------------------------------------------------------------------------
231
- # ุนู…ูŠู„ ุงู„ุจูˆุช (bot_client) - MTProtoุŒ ุชุณุฌูŠู„ ุฏุฎูˆู„ ุจุชูˆูƒู†ุŒ ุจุฏูˆู† ุฃูŠ ุชูุงุนู„ ุจุดุฑูŠ
232
- # ---------------------------------------------------------------------------
233
- bot_client = TelegramClient(BOT_SESSION_PATH, API_ID, API_HASH)
234
 
235
- # ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ุงู„ูุนู‘ุงู„ุฉ ุญุงู„ูŠู‹ุง: phone -> TelegramClient (StringSession)
236
- active_accounts: dict[str, TelegramClient] = {}
237
- background_tasks: list[asyncio.Task] = []
 
 
238
 
239
 
240
- async def process_message_for_storage(message, channel_username: str) -> bool:
241
- """ูŠุญู„ู„ ุฑุณุงู„ุฉ (ุตูˆุฑ๏ฟฝ๏ฟฝ + ูƒุงุจุดู†) ูˆูŠุฎุฒู†ู‡ุง ุจู€ Mongo ู„ูˆ ููŠู‡ุง ุณุนุฑ ูˆุงุถุญ. ูŠุฑุฌุน True ู„ูˆ ุฎุฒู†."""
242
- if not message.photo:
243
- return False
244
- caption = message.message or ""
245
- price = extract_price_from_text(caption)
246
- if price is None:
247
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
- image_bytes = await message.download_media(file=bytes)
250
- image = Image.open(io.BytesIO(image_bytes))
251
-
252
- specs = extract_specs_from_image(image)
253
- embedding = get_image_embedding(image)
254
-
255
- doc = {
256
- "source_channel": channel_username,
257
- "message_id": message.id,
258
- "date": datetime.now(timezone.utc),
259
- "raw_text": caption,
260
- "price": price,
261
- "level": specs["level"],
262
- "gems": specs["gems"],
263
- "ocr_text": specs["ocr_text"],
264
- "embedding": embedding.tolist(),
265
- }
266
- listings.update_one(
267
- {"source_channel": channel_username, "message_id": message.id},
268
- {"$set": doc},
269
  upsert=True,
 
270
  )
271
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
 
274
- def _make_live_handler(channel_username: str):
275
- async def handler(event):
276
- try:
277
- await process_message_for_storage(event.message, channel_username)
278
- except Exception:
279
- log.exception("ุฎุทุฃ ุฃุซู†ุงุก ู…ุนุงู„ุฌุฉ ุฑุณุงู„ุฉ ู…ุจุงุดุฑุฉ ู…ู† %s", channel_username)
280
- return handler
281
 
282
 
283
- async def register_channel_monitor(client: TelegramClient, channel_username: str):
284
- """ูŠู†ุถู… ู„ู„ู‚ู†ุงุฉ (ู„ูˆ ู‚ุฏุฑ) ูˆูŠุณุฌู„ ู…ุฑุงู‚ุจุฉ ู„ุญุธูŠุฉ ุนู„ูŠู‡ุง ุนู† ุทุฑูŠู‚ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู…ุนูŠู†."""
 
 
 
285
  try:
286
- await client(JoinChannelRequest(channel_username))
287
- except Exception:
288
- log.warning("ู…ุง ูƒุฏุฑุช ุฃู†ุถู… ู„ู‚ู†ุงุฉ %s ุจู‡ุฐุง ุงู„ุญุณุงุจ (ุฎุงุตุฉ ุฃูˆ ู…ู†ุถู… ุฃุตู„ุงู‹)", channel_username)
289
-
290
- handler = _make_live_handler(channel_username)
291
- client.add_event_handler(handler, events.NewMessage(chats=channel_username))
292
- log.info("ุจุฏุฃุช ู…ุฑุงู‚ุจุฉ ุงู„ู‚ู†ุงุฉ: %s", channel_username)
 
 
 
 
 
293
 
294
 
295
- async def backfill_channel(client: TelegramClient, channel_username: str, start_date: datetime):
296
- """ูŠุณุญุจ ูƒู„ ุงู„ู…ู†ุดูˆุฑุงุช (ููŠู‡ุง ุตูˆุฑุฉ ูˆุณุนุฑ) ู…ู† ุชุงุฑูŠุฎ ู…ุนูŠู† ู„ู„ูŠูˆู…."""
297
- count = 0
298
  try:
299
- async for message in client.iter_messages(
300
- channel_username, offset_date=start_date, reverse=True
301
- ):
302
- try:
303
- stored = await process_message_for_storage(message, channel_username)
304
- if stored:
305
- count += 1
306
- except Exception:
307
- log.exception("ุฎุทุฃ ุจู…ุนุงู„ุฌุฉ ุฑุณุงู„ุฉ ู‚ุฏูŠู…ุฉ ู…ู† %s", channel_username)
308
- except Exception:
309
- log.exception("ูุดู„ ุณุญุจ ุชุงุฑูŠุฎ ุงู„ู‚ู†ุงุฉ %s", channel_username)
310
- await bot_client.send_message(OWNER_ID, f"โš ๏ธ ุตุงุฑ ุฎุทุฃ ุจุณุญุจ ู‚ู†ุงุฉ {channel_username}")
311
- return
312
 
313
- await bot_client.send_message(
314
- OWNER_ID, f"โœ… ุชู… ุณุญุจ {count} ุฅุนู„ุงู† (ููŠู‡ ุณุนุฑ ูˆุตูˆุฑุฉ) ู…ู† ู‚ู†ุงุฉ {channel_username}"
 
 
 
 
 
 
 
 
 
 
 
 
315
  )
 
316
 
317
 
318
- def pick_account_for_new_channel() -> str | None:
319
- """ูŠูˆุฒุน ุงู„ู‚ู†ูˆุงุช ุนู„ู‰ ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ุจุงู„ุชู†ุงูˆุจ (round-robin). ูŠุฑุฌุน ุฑู‚ู… ุงู„ู‡ุงุชู ุงู„ู…ุฎุชุงุฑ."""
320
- phones = list(active_accounts.keys())
321
- if not phones:
322
- return None
323
- counter_doc = config_col.find_one_and_update(
324
- {"_id": "rr_counter"},
325
- {"$inc": {"value": 1}},
326
- upsert=True,
327
- return_document=ReturnDocument.AFTER,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  )
329
- idx = counter_doc["value"] % len(phones)
330
- return phones[idx]
331
-
332
-
333
- async def connect_account_from_session(phone: str, session_string: str) -> TelegramClient:
334
- """ูŠุนูŠุฏ ุงุชุตุงู„ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู…ุญููˆุธ ู…ุณุจู‚ู‹ุง (ุฌู„ุณุชู‡ ู…ุฎุฒู†ุฉ ุจู€ Mongo)."""
335
- client = TelegramClient(StringSession(session_string), API_ID, API_HASH)
336
- await client.connect()
337
- active_accounts[phone] = client
338
- task = asyncio.create_task(client.run_until_disconnected())
339
- background_tasks.append(task)
340
- return client
341
-
342
-
343
- # ---------------------------------------------------------------------------
344
- # ุงู„ุจูˆุช ุงู„ุชูุงุนู„ูŠ (bot_client) - ู…ู‚ููˆู„ ุนู„ู‰ OWNER_ID ูู‚ุท
345
- # ---------------------------------------------------------------------------
346
- # ุญุงู„ุฉ ุงู„ู…ุญุงุฏุซุฉ ุงู„ุญุงู„ูŠุฉ ู„ูƒู„ ู…ุณุชุฎุฏู… (ู‡ู†ุง ุจุณ ุงู„ู…ุงู„ูƒ ุฃุตู„ุงู‹)
347
- pending_action: dict[int, str] = {}
348
- # ุจูŠุงู†ุงุช ุชุณุฌูŠู„ ุฏุฎูˆู„ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู‚ูŠุฏ ุงู„ุฅุถุงูุฉ ุญุงู„ูŠู‹ุง
349
- pending_login: dict[int, dict] = {}
350
-
351
- MAIN_MENU = [
352
- [Button.inline("๐Ÿ“– ุงู„ุดุฑุญ", b"help")],
353
- [Button.inline("๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ", b"add_account")],
354
- [Button.inline("โž• ุฅุถุงูุฉ ู‚ู†ุงุฉ ู…ุฑุงู‚ุจุฉ", b"add_channel")],
355
- [Button.inline("๐Ÿ“… ุชุนูŠูŠู† ุชุงุฑูŠุฎ ุงู„ู…ุฑุงู‚ุจุฉ", b"set_date")],
356
- [Button.inline("๐ŸŽฏ ุชู‚ูŠูŠู… ุญุณุงุจ", b"evaluate")],
357
- ]
358
 
359
- HELP_TEXT = (
360
- "ู‡ุฐุง ุจูˆุช ุชุณุนูŠุฑ ุญุณุงุจุงุช Bounty Rush.\n\n"
361
- "๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ: ุชุถูŠู ุญุณุงุจ ุชูŠู„ูŠุฌุฑุงู… (ุฑู‚ู… ู‡ุงุชู) ูŠุณุชุฎุฏู…ู‡ ุงู„ุจูˆุช "
362
- "ู„ู„ุงู†ุถู…ุงู… ู„ู„ู‚ู†ูˆุงุช ูˆู…ุฑุงู‚ุจุชู‡ุง. ุชู‚ุฏุฑ ุชุถูŠู ุฃูƒุซุฑ ู…ู† ุญุณุงุจุŒ ูˆูƒู„ ู‚ู†ุงุฉ ุชู†ุถุงู "
363
- "ุชูˆุฒุน ุชู„ู‚ุงุฆูŠู‹ุง ุนู„ู‰ ุฃุญุฏ ุงู„ุญุณุงุจุงุช.\n\n"
364
- "โž• ุฅุถุงูุฉ ู‚ู†ุงุฉ ู…ุฑุงู‚ุจุฉ: ุฃุนุทูŠู†ูŠ ูŠูˆุฒุฑ ู‚ู†ุงุฉ (ุจุฏูˆู† @)ุŒ ูˆุฑุงุญ ูŠุชูƒูู„ ููŠู‡ุง "
365
- "ุฃุญุฏ ุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ุงู„ู…ุถุงูุฉ ูˆูŠู†ุถู… ู„ู‡ุง ูˆูŠุฑุงู‚ุจ ู…ู†ุดูˆุฑุงุชู‡ุง.\n\n"
366
- "๐Ÿ“… ุชุนูŠูŠู† ุชุงุฑูŠุฎ ุงู„ู…ุฑุงู‚ุจุฉ: ุฃุนุทูŠู†ูŠ ุชุงุฑูŠุฎ ุจุตูŠุบุฉ YYYY|MM|DD ู…ุซู„ 2026|1|1ุŒ "
367
- "ูˆุฑุงุญ ุฃุณุญุจ ูƒู„ ู…ู†ุดูˆุฑ ููŠู‡ ุตูˆุฑุฉ ุญุณุงุจ ูˆุณุนุฑ ู…ู† ูƒู„ ุงู„ู‚ู†ูˆุงุช ุงู„ู…ุถุงูุฉุŒ ู…ู† ู‡ุฐุง "
368
- "ุงู„ุชุงุฑูŠุฎ ุฅู„ู‰ ุงู„ูŠูˆู….\n\n"
369
- "๐ŸŽฏ ุชู‚ูŠูŠู… ุญุณุงุจ: ุฃุฑุณู„ ุตูˆุฑุฉ ุณูƒุฑูŠู† ุดูˆุช ู…ู† ุฏุงุฎู„ ุงู„ุญุณุงุจุŒ ูˆุฑุงุญ ุฃุทู„ุนู„ูƒ ุณุนุฑ "
370
- "ุชู‚ุฑูŠุจูŠ ุจู†ุงุกู‹ ุนู„ู‰ ุฃู‚ุฑุจ ุงู„ุญุณุงุจุงุช ุงู„ู…ุดุงุจู‡ุฉ ุงู„ู…ุฎุฒู†ุฉ ุนู†ุฏูŠ."
371
- )
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
- def _is_owner(user_id: int) -> bool:
375
- return user_id == OWNER_ID
376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
 
378
- @bot_client.on(events.NewMessage(pattern="/start"))
379
- async def start_command(event):
380
- if not _is_owner(event.sender_id):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  return
382
- await event.respond("ุฃู‡ู„ุงู‹ ุจูŠูƒ ๐Ÿ‘‹ ุงุฎุชุฑ ุดู†ูˆ ุชุจูŠ ุชุณูˆูŠ:", buttons=MAIN_MENU)
 
 
 
 
 
 
 
 
 
 
383
 
384
 
385
- @bot_client.on(events.CallbackQuery)
386
- async def button_callback(event):
387
- if not _is_owner(event.sender_id):
 
 
 
388
  return
389
- data = event.data.decode()
390
- uid = event.sender_id
391
 
392
- if data == "help":
393
- await event.answer()
394
- await event.respond(HELP_TEXT)
 
395
 
396
- elif data == "add_account":
397
- pending_action[uid] = "await_add_account_phone"
398
- await event.answer()
399
- await event.respond(
400
- "ุฃุฑุณู„ ุฑู‚ู… ู‡ุงุชู ุญุณุงุจ ุงู„ู…ุฑุงู‚ุจุฉ ุจุตูŠุบุฉ ุฏูˆู„ูŠุฉ ูƒุงู…ู„ุฉุŒ ู…ุซุงู„:\n+9647701234567"
401
- )
402
 
403
- elif data == "add_channel":
404
- if not active_accounts:
405
- await event.answer()
406
- await event.respond(
407
- "ู…ุง ุนู†ุฏูƒ ุฃูŠ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู…ุถุงู ุจุนุฏ. ุงุถุบุท '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ' ุฃูˆู„."
408
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  return
410
- pending_action[uid] = "await_channel"
411
- await event.answer()
412
- await event.respond("ุงุฑุณู„ู„ูŠ ูŠูˆุฒุฑ ุงู„ู‚ู†ุงุฉ ุงู„ู„ูŠ ุชุจูŠู†ูŠ ุฃุฑุงู‚ุจู‡ุง (ุจุฏูˆู† @)ุŒ ู…ุซุงู„:\nbountyrush_market")
413
 
414
- elif data == "set_date":
415
- pending_action[uid] = "await_date"
416
- await event.answer()
417
- await event.respond("ุงุฑุณู„ ุชุงุฑูŠุฎ ุจุฏุงูŠุฉ ุงู„ู…ุฑุงู‚ุจุฉ ุจู‡ุฐูŠ ุงู„ุตูŠุบุฉ ุจุงู„ุถุจุท:\nYYYY|MM|DD\nู…ุซุงู„: 2026|1|1")
 
 
 
 
418
 
419
- elif data == "evaluate":
420
- pending_action[uid] = "await_photo"
421
- await event.answer()
422
- await event.respond("ุงุฑุณู„ู„ูŠ ุตูˆุฑุฉ ุณูƒุฑูŠู† ุดูˆุช ู…ู† ุงู„ุญุณุงุจ ูˆุฑุงุญ ุฃู‚ูŠู…ู‡ ๐Ÿ“ธ")
423
 
 
 
 
 
 
 
 
 
 
424
 
425
- @bot_client.on(events.NewMessage(func=lambda e: e.sender_id == OWNER_ID and e.photo))
426
- async def photo_message_handler(event):
427
- await event.respond("ุฌุงุฑูŠ ุชุญู„ูŠู„ ุงู„ุตูˆุฑุฉุŒ ู„ุญุธุงุช... โณ")
428
 
429
- image_bytes = await event.download_media(file=bytes)
430
- image = Image.open(io.BytesIO(image_bytes))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
- specs = extract_specs_from_image(image)
433
- embedding = get_image_embedding(image)
434
- result = find_similar_and_price(embedding, specs)
435
- pending_action.pop(event.sender_id, None)
 
 
 
 
 
 
436
 
437
- if not result["matches"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  await event.respond(
439
- "ู‚ุงุนุฏุฉ ุงู„ุจูŠุงู†ุงุช ู„ุณุง ู…ุง ููŠู‡ุง ุญุณุงุจุงุช ูƒุงููŠุฉ ุฃู‚ุงุฑู† ุนู„ูŠู‡ุง. "
440
- "ุฎู„ูŠ ุงู„ุจูˆุช ูŠุฑุงู‚ุจ ุงู„ู‚ู†ูˆุงุช ูุชุฑุฉ ูˆุจุนุฏูŠู† ุฌุฑุจ ู…ุฑุฉ ุซุงู†ูŠุฉ."
 
 
 
441
  )
442
  return
 
443
 
444
- lines = [f"๐Ÿ’ฐ ุงู„ุณุนุฑ ุงู„ุชู‚ุฑูŠุจูŠ: {result['estimated_price']}"]
445
- if specs.get("level"):
446
- lines.append(f"๐Ÿ“Š ุงู„ู„ูŠูู„ ุงู„ู…ู‚ุฑูˆุก ู…ู† ุตูˆุฑุชูƒ: {specs['level']}")
447
- if specs.get("gems"):
448
- lines.append(f"๐Ÿ’Ž ุงู„ุฌูˆุงู‡ุฑ ุงู„ู…ู‚ุฑูˆุกุฉ ู…ู† ุตูˆุฑุชูƒ: {specs['gems']}")
449
- lines.append("\nุฃู‚ุฑุจ ุญุณุงุจุงุช ู…ุดุงุจู‡ุฉ ู„ู‚ูŠุชู‡ุง:")
450
- for m in result["matches"]:
451
- lines.append(
452
- f"- ุชุดุงุจู‡ {m['similarity_percent']}% | ุณุนุฑ {m['price']} "
453
- f"| ู„ูŠูู„ {m.get('level', 'ุŸ')} | ุฌูˆุงู‡ุฑ {m.get('gems', 'ุŸ')}"
454
- )
455
 
456
- await event.respond("\n".join(lines))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
 
459
- async def _finalize_new_account(uid: int, client: TelegramClient, phone: str):
460
- session_string = client.session.save()
461
- me = await client.get_me()
462
- accounts_col.update_one(
463
- {"phone": phone},
464
- {"$set": {"phone": phone, "session": session_string, "user_id": me.id, "first_name": me.first_name}},
465
- upsert=True,
466
- )
467
- active_accounts[phone] = client
468
- task = asyncio.create_task(client.run_until_disconnected())
469
- background_tasks.append(task)
470
 
471
- await bot_client.send_message(
472
- uid, f"โœ… ุชู…ุช ุฅุถุงูุฉ ุญุณุงุจ ุงู„ู…ุฑุงู‚ุจุฉ: {me.first_name} ({phone})"
 
 
 
 
 
 
 
 
 
 
 
473
  )
474
- pending_login.pop(uid, None)
475
- pending_action.pop(uid, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
 
477
 
478
- @bot_client.on(
479
- events.NewMessage(
480
- func=lambda e: e.sender_id == OWNER_ID and e.text and not e.text.startswith("/") and not e.photo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  )
482
- )
483
- async def text_message_handler(event):
484
- uid = event.sender_id
485
- action = pending_action.get(uid)
486
- text = event.text.strip()
487
-
488
- # ---------------- ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ: ุฎุทูˆุฉ ุฑู‚ู… ุงู„ู‡ุงุชู ----------------
489
- if action == "await_add_account_phone":
490
- phone = text.strip()
491
- if not re.match(r"^\+\d{8,15}$", phone):
492
- await event.respond("ุตูŠุบุฉ ุงู„ุฑู‚ู… ุบู„ุท. ู„ุงุฒู… ูŠุจุฏุฃ ุจู€ + ูˆุฑู…ุฒ ุงู„ุฏูˆู„ุฉุŒ ู…ุซุงู„: +9647701234567")
 
 
 
 
 
 
 
493
  return
494
- temp_client = TelegramClient(StringSession(), API_ID, API_HASH)
495
- try:
496
- await temp_client.connect()
497
- result = await temp_client.send_code_request(phone)
498
- except FloodWaitError as e:
499
- await event.respond(f"ู…ุญุงูˆู„ุงุช ูƒุซูŠุฑุฉุŒ ู„ุงุฒู… ุชู†ุชุธุฑ {e.seconds} ุซุงู†ูŠุฉ ูˆุชุญุงูˆู„ ู…ุฑุฉ ุซุงู†ูŠุฉ.")
500
- await temp_client.disconnect()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  return
 
 
 
 
 
 
 
 
 
502
  except Exception:
503
- log.exception("ูุดู„ ุฅุฑุณุงู„ ูƒูˆุฏ ุงู„ุชุญู‚ู‚")
504
- await event.respond("ุตุงุฑ ุฎุทุฃ ุจุฅุฑุณุงู„ ูƒูˆุฏ ุงู„ุชุญู‚ู‚. ุชุฃูƒุฏ ู…ู† ุงู„ุฑู‚ู… ูˆุญุงูˆู„ ู…ุฑุฉ ุซุงู†ูŠุฉ.")
505
- await temp_client.disconnect()
506
- return
507
-
508
- pending_login[uid] = {
509
- "client": temp_client,
510
- "phone": phone,
511
- "phone_code_hash": result.phone_code_hash,
512
- }
513
- pending_action[uid] = "await_add_account_code"
514
- await event.respond("ูˆุตู„ูƒ ูƒูˆุฏ ุงู„ุชุญู‚ู‚ ุจุชุทุจูŠู‚ ุชูŠู„ูŠุฌุฑุงู…. ุงุฑุณู„ู‡ ู‡ู†ุง:")
515
-
516
- # ---------------- ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ: ุฎุทูˆุฉ ูƒูˆุฏ ุงู„ุชุญู‚ู‚ ----------------
517
- elif action == "await_add_account_code":
518
- login = pending_login.get(uid)
519
- if not login:
520
- pending_action.pop(uid, None)
521
- await event.respond("ุตุงุฑ ุฎุทุฃุŒ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ ุจุฒุฑ '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ'.")
522
  return
523
- code = text.strip()
524
- client = login["client"]
 
 
 
 
525
  try:
526
- await client.sign_in(phone=login["phone"], code=code, phone_code_hash=login["phone_code_hash"])
527
- except SessionPasswordNeededError:
528
- pending_action[uid] = "await_add_account_password"
529
- await event.respond("ุญุณุงุจูƒ ู…ูุนู„ ุนู„ูŠู‡ ุชุญู‚ู‚ ุจุฎุทูˆุชูŠู†. ุฃุฑุณู„ ูƒู„ู…ุฉ ุงู„ุณุฑ:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  return
531
- except (PhoneCodeInvalidError, PhoneCodeExpiredError):
532
- await event.respond("ุงู„ูƒูˆุฏ ุบู„ุท ุฃูˆ ู…ู†ุชู‡ูŠ. ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ ุจุฒุฑ '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ'.")
533
- await client.disconnect()
534
- pending_login.pop(uid, None)
535
- pending_action.pop(uid, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  return
537
- except Exception:
538
- log.exception("ูุดู„ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆ๏ฟฝ๏ฟฝ ุจุงู„ูƒูˆุฏ")
539
- await event.respond("ุตุงุฑ ุฎุทุฃ ุจุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„. ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ.")
540
- await client.disconnect()
541
- pending_login.pop(uid, None)
542
- pending_action.pop(uid, None)
543
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
- await _finalize_new_account(uid, client, login["phone"])
546
-
547
- # ---------------- ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ: ุฎุทูˆุฉ ูƒู„ู…ุฉ ุงู„ุณุฑ (2FA) ----------------
548
- elif action == "await_add_account_password":
549
- login = pending_login.get(uid)
550
- if not login:
551
- pending_action.pop(uid, None)
552
- await event.respond("ุตุงุฑ ุฎุทุฃุŒ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ ุจุฒุฑ '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ'.")
 
 
 
 
 
 
 
 
 
 
553
  return
554
- client = login["client"]
555
- try:
556
- await client.sign_in(password=text)
557
- except PasswordHashInvalidError:
558
- await event.respond("ูƒู„ู…ุฉ ุงู„ุณุฑ ุบู„ุท. ุญุงูˆู„ ุชุฑุณู„ู‡ุง ู…ุฑุฉ ุซุงู†ูŠุฉ:")
559
  return
560
- except Exception:
561
- log.exception("ูุดู„ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุจูƒู„ู…ุฉ ุงู„ุณุฑ")
562
- await event.respond("ุตุงุฑ ุฎุทุฃ. ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ ุจุฒุฑ '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ'.")
563
- await client.disconnect()
564
- pending_login.pop(uid, None)
565
- pending_action.pop(uid, None)
 
 
 
 
 
 
 
 
 
 
566
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
- await _finalize_new_account(uid, client, login["phone"])
569
 
570
- # ---------------- ุฅุถุงูุฉ ู‚ู†ุงุฉ ู…ุฑุงู‚ุจุฉ ----------------
571
- elif action == "await_channel":
572
- channel_username = text.lstrip("@").strip()
573
- account_phone = pick_account_for_new_channel()
574
- if not account_phone:
575
- await event.respond("ู…ุง ุนู†ุฏูƒ ุฃูŠ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ ู…ุถุงู. ุฃุถู ุญุณุงุจ ุฃูˆู„ ู…ู† ุฒุฑ '๐Ÿ‘ค ุฅุถุงูุฉ ุญุณุงุจ ู…ุฑุงู‚ุจุฉ'.")
576
- pending_action.pop(uid, None)
577
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
- channels_col.update_one(
580
- {"channel": channel_username},
581
- {"$set": {"channel": channel_username, "account_phone": account_phone}},
582
- upsert=True,
583
- )
584
- assigned_client = active_accounts[account_phone]
585
- await event.respond(f"โณ ุฌุงุฑูŠ ุงู„ุงู†ุถู…ุงู… ู„ู‚ู†ุงุฉ {channel_username} (ุนุจุฑ ุญุณุงุจ {account_phone})...")
586
- await register_channel_monitor(assigned_client, channel_username)
587
- await event.respond(f"โœ… ุชู…ุช ุฅุถุงูุฉ {channel_username} ู„ู„ู…ุฑุงู‚ุจุฉ ุงู„ู„ุญุธูŠุฉ.")
588
-
589
- cfg = config_col.find_one({"_id": "settings"})
590
- if cfg and cfg.get("monitor_start_date"):
591
- await event.respond("โณ ุฌุงุฑูŠ ุณุญุจ ุงู„ู…ู†ุดูˆุฑุงุช ุงู„ู‚ุฏูŠู…ุฉ ู„ู‡ุฐูŠ ุงู„ู‚ู†ุงุฉ ู…ู† ุงู„ุชุงุฑูŠุฎ ุงู„ู…ุญุฏุฏ...")
592
- asyncio.create_task(
593
- backfill_channel(assigned_client, channel_username, cfg["monitor_start_date"])
594
- )
595
- pending_action.pop(uid, None)
596
 
597
- # ---------------- ุชุนูŠูŠู† ุชุงุฑูŠุฎ ุงู„ู…ุฑุงู‚ุจุฉ ----------------
598
- elif action == "await_date":
599
- if not re.match(r"^\d{4}\|\d{1,2}\|\d{1,2}$", text):
600
- await event.respond("ุตูŠุบุฉ ุบู„ุท. ู„ุงุฒู… ุชูƒูˆู† ู‡ูŠูƒ ุจุงู„ุถุจุท: YYYY|MM|DD ู…ุซุงู„: 2026|1|1")
601
- return
602
- year, month, day = (int(x) for x in text.split("|"))
603
- try:
604
- start_date = datetime(year, month, day, tzinfo=timezone.utc)
605
- except ValueError:
606
- await event.respond("ุงู„ุชุงุฑูŠุฎ ุบูŠุฑ ุตุงู„ุญุŒ ุฌุฑุจ ู…ุฑุฉ ุซุงู†ูŠุฉ.")
607
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
 
609
- config_col.update_one(
610
- {"_id": "settings"}, {"$set": {"monitor_start_date": start_date}}, upsert=True
 
 
 
 
 
 
 
 
 
 
 
 
611
  )
612
- pending_action.pop(uid, None)
 
 
 
 
 
 
 
 
 
 
 
613
 
614
- all_channels = list(channels_col.find())
615
- if not all_channels:
616
- await event.respond(
617
- "โœ… ุชู… ุญูุธ ุงู„ุชุงุฑูŠุฎ. ุจุณ ู…ุง ุนู†ุฏูƒ ู‚ู†ูˆุงุช ู…ุถุงูุฉ ุจุนุฏ - ุฃุถู ู‚ู†ุงุฉ ุฃูˆู„ ุนุดุงู† ุฃุจุฏุฃ ุฃุณุญุจ ู…ู†ู‡ุง."
618
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
 
621
- await event.respond(
622
- f"โœ… ุชู… ุญูุธ ุงู„ุชุงุฑูŠุฎ. ุฌุงุฑูŠ ุณุญุจ ุงู„ู…ู†ุดูˆุฑุงุช ู…ู† {len(all_channels)} ู‚ู†ุงุฉ ุจุฏุกู‹ุง ู…ู† {text}..."
623
- )
624
- for ch in all_channels:
625
- client = active_accounts.get(ch.get("account_phone"))
626
- if client:
627
- asyncio.create_task(backfill_channel(client, ch["channel"], start_date))
628
 
629
- else:
630
- await event.respond("ุงูƒุชุจ /start ู„ุนุฑุถ ุงู„ู‚ุงุฆู…ุฉ.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
 
632
 
633
- # ---------------------------------------------------------------------------
634
- # ุงู„ุชุดุบูŠู„
635
- # ---------------------------------------------------------------------------
636
  async def main():
637
- # 1) ุดุบู‘ู„ ุงู„ุจูˆุช ุงู„ุชูุงุนู„ูŠ (ุชุณุฌูŠู„ ุฏุฎูˆู„ ุจุชูˆูƒู†ุŒ ุจุฏูˆู† ุฃูŠ ุชูุงุนู„ ุจุดุฑูŠ)
638
- await bot_client.start(bot_token=BOT_TOKEN)
639
- log.info("bot_client ู…ุชุตู„ (OWNER_ID=%s).", OWNER_ID)
 
 
 
 
640
 
641
- # 2) ุฃุนุฏ ุงู„ุงุชุตุงู„ ุจุญุณุงุจุงุช ุงู„ู…ุฑุงู‚ุจุฉ ุงู„ู…ุฎุฒู†ุฉ ู…ุณุจู‚ู‹ุง ุจู€ Mongo
642
- for acc in accounts_col.find():
643
- try:
644
- await connect_account_from_session(acc["phone"], acc["session"])
645
- log.info("ุชู… ุฅุนุงุฏุฉ ุงู„ุงุชุตุงู„ ุจุญุณุงุจ ุงู„ู…ุฑุงู‚ุจุฉ: %s", acc["phone"])
646
- except Exception:
647
- log.exception("ูุดู„ ุฅุนุงุฏุฉ ุงู„ุงุชุตุงู„ ุจุญุณุงุจ %s", acc.get("phone"))
648
 
649
- # 3) ุฃุนุฏ ุชูุนูŠู„ ู…ุฑุงู‚ุจุฉ ุงู„ู‚ู†ูˆุงุช ุงู„ู…ุฎุฒู†ุฉุŒ ูƒู„ ูˆุญุฏุฉ ุนุจุฑ ุงู„ุญุณุงุจ ุงู„ู…ุณุคูˆู„ ุนู†ู‡ุง
650
- for ch in channels_col.find():
651
- client = active_accounts.get(ch.get("account_phone"))
652
- if client:
653
- await register_channel_monitor(client, ch["channel"])
654
- else:
655
- log.warning("ุงู„ู‚ู†ุงุฉ %s ู…ุณุฌู„ุฉ ุนู„ู‰ ุญุณุงุจ ุบูŠุฑ ู…ุชุตู„ ุญุงู„ูŠู‹ุง", ch["channel"])
656
 
657
- await bot_client.run_until_disconnected()
 
658
 
659
 
660
  if __name__ == "__main__":
661
- asyncio.run(main())
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
  """
4
+ MF BLACKLIST - Telegram Bot (Telethon + MTProto + MongoDB + asyncio)
5
+ Production-ready single-file implementation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  import os
9
+ import sys
10
+ import time
 
11
  import logging
12
+ import asyncio
13
+ import datetime
14
+ from collections import defaultdict
 
 
 
 
 
15
 
16
  from telethon import TelegramClient, events, Button
 
 
17
  from telethon.errors import (
18
+ FloodWaitError, RPCError, UsernameNotOccupiedError, UsernameInvalidError,
19
+ UserIdInvalidError, ChatAdminRequiredError, ChannelPrivateError,
20
+ UserNotParticipantError, PeerIdInvalidError,
21
+ )
22
+ from telethon.tl.types import User, Channel, Chat, PeerChannel
23
+
24
+ from motor.motor_asyncio import AsyncIOMotorClient
25
+ from pymongo import ASCENDING, DESCENDING
26
+ from pymongo.errors import PyMongoError
27
+
28
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
+ # Logging
30
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
34
+ handlers=[logging.StreamHandler(sys.stdout)],
35
  )
36
+ log = logging.getLogger("mfblacklist")
37
+ logging.getLogger("telethon").setLevel(logging.WARNING)
38
+
39
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
40
+ # Environment variables
41
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
42
+ def _require_env(name: str) -> str:
43
+ val = os.environ.get(name)
44
+ if not val or not val.strip():
45
+ log.error("Missing required environment variable: %s", name)
46
+ sys.exit(1)
47
+ return val.strip()
48
+
49
+ API_ID_RAW = _require_env("API_ID")
50
+ API_HASH = _require_env("API_HASH")
51
+ BOT_TOKEN = _require_env("BOT_TOKEN")
52
+ MONGO_URI = _require_env("MONGO_URI")
53
+
54
+ try:
55
+ API_ID = int(API_ID_RAW)
56
+ except ValueError:
57
+ log.error("API_ID must be an integer")
58
+ sys.exit(1)
59
+
60
+ OWNER_ID = 6069597294 # fixed, cannot be changed from the bot panel
61
+
62
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
63
+ # Mongo
64
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
65
+ mongo_client = AsyncIOMotorClient(MONGO_URI)
66
+ db = mongo_client["mf_blacklist"]
67
+
68
+ col_admins = db["admins"]
69
+ col_cases = db["cases"]
70
+ col_users = db["users"]
71
+ col_settings = db["settings"]
72
+ col_logs = db["logs"]
73
+ col_pending = db["pending_reports"]
74
+ col_counters = db["counters"]
75
+
76
+ ALL_PERMISSIONS = [
77
+ "approve_reports",
78
+ "reject_reports",
79
+ "manage_reports",
80
+ "edit_templates",
81
+ "manage_settings",
82
+ "manage_admins",
83
+ ]
84
 
85
+ DEFAULT_TEMPLATE_THIEF = (
86
+ "๐Ÿšจ ุจู„ุงุบ ุณุงุฑู‚\n\n"
87
+ "๐Ÿ‘ค ุงู„ู…ุณุชุฎุฏู…: {username}\n"
88
+ "๐Ÿ†” ID: {user_id}\n\n"
89
+ "๐Ÿ“‹ ุงู„ุชูุงุตูŠู„:\n{description}\n\n"
90
+ "๐Ÿ”– ุฑู‚ู… ุงู„ุจู„ุงุบ: {case_id}\n"
91
+ "๐Ÿ“… ุงู„ุชุงุฑูŠุฎ: {date}"
92
+ )
93
+ DEFAULT_TEMPLATE_IMPERSONATOR = (
94
+ "โš ๏ธ ุจู„ุงุบ ู…ู†ุชุญู„\n\n"
95
+ "๐Ÿ‘ค ุงู„ู…ุณุชุฎุฏู…: {username}\n"
96
+ "๐Ÿ†” ID: {user_id}\n\n"
97
+ "๐Ÿ“‹ ุงู„ุชูุงุตูŠู„:\n{description}\n\n"
98
+ "๐Ÿ”– ุฑู‚ู… ุงู„ุจู„ุงุบ: {case_id}\n"
99
+ "๐Ÿ“… ุงู„ุชุงุฑูŠุฎ: {date}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  )
 
 
101
 
102
+ TYPE_LABEL = {"thief": "ุณุงุฑู‚", "impersonator": "ู…ู†ุชุญู„"}
103
 
104
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
105
+ # In-memory conversation state & anti-spam
106
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
+ STATE = {} # user_id -> dict(flow, step, data)
108
+ LAST_ACTION_TS = defaultdict(float) # user_id -> timestamp (cooldown)
109
+ ACTION_WINDOW = defaultdict(list) # user_id -> list[timestamps] (flood window)
 
110
 
111
+ COOLDOWN_SECONDS = 15
112
+ MAX_ACTIONS_PER_WINDOW = 8
113
+ WINDOW_SECONDS = 60
114
+ STATE_TTL_SECONDS = 1800 # stale state cleanup
115
 
 
 
 
 
 
 
 
116
 
117
+ def clear_state(user_id: int):
118
+ STATE.pop(user_id, None)
119
 
 
 
120
 
121
+ def set_state(user_id: int, flow: str, step: str, data: dict = None):
122
+ STATE[user_id] = {
123
+ "flow": flow,
124
+ "step": step,
125
+ "data": data or {},
126
+ "ts": time.time(),
127
+ }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ def get_state(user_id: int):
131
+ st = STATE.get(user_id)
132
+ if not st:
133
+ return None
134
+ if time.time() - st["ts"] > STATE_TTL_SECONDS:
135
+ clear_state(user_id)
136
+ return None
137
+ return st
138
 
 
 
 
 
 
 
 
139
 
140
+ def touch_state(user_id: int):
141
+ if user_id in STATE:
142
+ STATE[user_id]["ts"] = time.time()
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ def check_flood(user_id: int) -> bool:
146
+ """Return True if user is allowed to act now."""
147
+ now = time.time()
148
+ window = ACTION_WINDOW[user_id]
149
+ while window and now - window[0] > WINDOW_SECONDS:
150
+ window.pop(0)
151
+ if len(window) >= MAX_ACTIONS_PER_WINDOW:
152
+ return False
153
+ window.append(now)
154
+ return True
155
+
156
+
157
+ def check_cooldown(user_id: int) -> float:
158
+ """Return remaining cooldown seconds (0 if OK) for starting a new report."""
159
+ last = LAST_ACTION_TS.get(user_id, 0)
160
+ elapsed = time.time() - last
161
+ if elapsed < COOLDOWN_SECONDS:
162
+ return COOLDOWN_SECONDS - elapsed
163
+ return 0
164
+
165
+
166
+ def mark_action(user_id: int):
167
+ LAST_ACTION_TS[user_id] = time.time()
168
+
169
+
170
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
171
+ # Telethon client (runs as a bot account, not a user account)
172
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
173
+ SESSION_NAME = "mfblacklist_bot_session"
174
+ client = TelegramClient(SESSION_NAME, API_ID, API_HASH)
175
 
176
+
177
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
178
+ # Helpers: safe telegram calls with FloodWait handling
179
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
180
+ async def safe_call(coro_factory, *args, retries=3, **kwargs):
181
+ """coro_factory: a callable returning a coroutine (so it can be retried)."""
182
+ for attempt in range(retries):
183
+ try:
184
+ return await coro_factory(*args, **kwargs)
185
+ except FloodWaitError as e:
186
+ wait_for = e.seconds + 1
187
+ log.warning("FloodWait: sleeping for %s seconds", wait_for)
188
+ await asyncio.sleep(wait_for)
189
+ except RPCError as e:
190
+ log.error("RPCError in safe_call: %s", e)
191
+ raise
192
+ raise RuntimeError("safe_call: exceeded retries after repeated FloodWait")
193
+
194
+
195
+ async def log_action(user_id, action, details=None, case_id=None):
196
+ try:
197
+ await col_logs.insert_one({
198
+ "user_id": user_id,
199
+ "action": action,
200
+ "case_id": case_id,
201
+ "details": details or {},
202
+ "timestamp": datetime.datetime.utcnow(),
203
+ })
204
+ except PyMongoError as e:
205
+ log.error("Failed to write log entry: %s", e)
206
+
207
+
208
+ async def upsert_user(user_id, username=None, first_name=None):
209
+ try:
210
+ await col_users.update_one(
211
+ {"user_id": user_id},
212
+ {"$set": {
213
+ "user_id": user_id,
214
+ "username": username,
215
+ "first_name": first_name,
216
+ "last_seen": datetime.datetime.utcnow(),
217
+ },
218
+ "$setOnInsert": {"first_seen": datetime.datetime.utcnow()}},
219
+ upsert=True,
220
  )
221
+ except PyMongoError as e:
222
+ log.error("upsert_user failed: %s", e)
223
+
224
+
225
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
226
+ # Permissions
227
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
228
+ def is_owner(user_id: int) -> bool:
229
+ return user_id == OWNER_ID
230
+
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
+ async def get_admin_doc(user_id: int):
233
+ if is_owner(user_id):
234
+ return {
235
+ "user_id": OWNER_ID,
236
+ "owner": True,
237
+ "permissions": {p: True for p in ALL_PERMISSIONS},
238
+ }
239
+ return await col_admins.find_one({"user_id": user_id})
240
 
 
 
 
 
241
 
242
+ async def is_admin(user_id: int) -> bool:
243
+ if is_owner(user_id):
244
+ return True
245
+ doc = await col_admins.find_one({"user_id": user_id})
246
+ return doc is not None
247
 
248
 
249
+ async def has_permission(user_id: int, perm: str) -> bool:
250
+ if is_owner(user_id):
251
+ return True
252
+ doc = await col_admins.find_one({"user_id": user_id})
253
+ if not doc:
 
 
254
  return False
255
+ return bool(doc.get("permissions", {}).get(perm, False))
256
+
257
+
258
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
259
+ # Settings helpers
260
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
261
+ async def get_settings():
262
+ doc = await col_settings.find_one({"_id": "config"})
263
+ if not doc:
264
+ doc = {
265
+ "_id": "config",
266
+ "reports_enabled": True,
267
+ "search_enabled": True,
268
+ "maintenance_message": "",
269
+ "channel_id": None,
270
+ "channel_username": None,
271
+ "review_group_id": None,
272
+ "template_thief": DEFAULT_TEMPLATE_THIEF,
273
+ "template_impersonator": DEFAULT_TEMPLATE_IMPERSONATOR,
274
+ }
275
+ await col_settings.insert_one(doc)
276
+ return doc
277
 
278
+
279
+ async def update_settings(fields: dict):
280
+ await col_settings.update_one({"_id": "config"}, {"$set": fields}, upsert=True)
281
+
282
+
283
+ async def next_case_id() -> str:
284
+ doc = await col_counters.find_one_and_update(
285
+ {"_id": "case_seq"},
286
+ {"$inc": {"seq": 1}},
 
 
 
 
 
 
 
 
 
 
 
287
  upsert=True,
288
+ return_document=True,
289
  )
290
+ seq = doc["seq"] if doc else 1
291
+ return f"MF-{seq:06d}"
292
+
293
+
294
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
295
+ # Keyboards
296
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
297
+ async def main_menu_buttons(user_id: int):
298
+ rows = [
299
+ [Button.inline("๐Ÿ” ุงู„ุจุญุซ ุนู† ุดุฎุต", b"search:start")],
300
+ [Button.inline("๐Ÿšจ ู†ุดุฑ ุณุงุฑู‚", b"report:thief:start")],
301
+ [Button.inline("โš ๏ธ ู†ุดุฑ ู…ู†ุชุญู„", b"report:impersonator:start")],
302
+ [Button.inline("๐Ÿ“– ุดุฑุญ ุงู„ุจูˆุช", b"help:show")],
303
+ [Button.inline("๐Ÿ“ข ู‚ู†ุงุฉ ุงู„ู†ุดุฑ", b"channel:show")],
304
+ ]
305
+ if await is_admin(user_id):
306
+ rows.append([Button.inline("๐Ÿ›  ู„ูˆุญุฉ ุงู„ู…ุทูˆุฑูŠู†", b"admin:panel")])
307
+ return rows
308
 
309
 
310
+ def back_cancel_row(back_cb: bytes = b"menu:main"):
311
+ return [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", back_cb), Button.inline("โŒ ุฅู„ุบุงุก", b"flow:cancel")]
 
 
 
 
 
312
 
313
 
314
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
315
+ # Entity resolution helpers
316
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
317
+ async def resolve_username(username: str):
318
+ username = username.lstrip("@").strip()
319
  try:
320
+ entity = await client.get_entity(username)
321
+ if isinstance(entity, User):
322
+ return entity
323
+ return None
324
+ except (UsernameNotOccupiedError, UsernameInvalidError, ValueError):
325
+ return None
326
+ except FloodWaitError as e:
327
+ await asyncio.sleep(e.seconds + 1)
328
+ return await resolve_username(username)
329
+ except RPCError as e:
330
+ log.warning("resolve_username RPCError: %s", e)
331
+ return None
332
 
333
 
334
+ async def resolve_user_id(uid: int):
 
 
335
  try:
336
+ entity = await client.get_entity(uid)
337
+ if isinstance(entity, User):
338
+ return entity
339
+ return None
340
+ except FloodWaitError as e:
341
+ await asyncio.sleep(e.seconds + 1)
342
+ return await resolve_user_id(uid)
343
+ except (ValueError, RPCError):
344
+ return None
 
 
 
 
345
 
346
+
347
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
348
+ # /start
349
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
350
+ @client.on(events.NewMessage(pattern=r"^/start$"))
351
+ async def start_handler(event):
352
+ user_id = event.sender_id
353
+ sender = await event.get_sender()
354
+ await upsert_user(user_id, getattr(sender, "username", None), getattr(sender, "first_name", None))
355
+ clear_state(user_id)
356
+ text = (
357
+ "๐Ÿ–ค **MF BLACKLIST** ๐Ÿ–ค\n\n"
358
+ "ุจูˆุช ู…ุฑูƒุฒูŠ ู„ุฅุฏุงุฑุฉ ุจู„ุงุบุงุช ุงู„ุณุฑู‚ุฉ ูˆุงู„ุงู†ุชุญุงู„ ุฏุงุฎู„ ุชูŠู„ูŠุฌุฑุงู….\n"
359
+ "ุงุฎุชุฑ ุฃุญุฏ ุงู„ุฎูŠุงุฑุงุช:"
360
  )
361
+ await event.respond(text, buttons=await main_menu_buttons(user_id))
362
 
363
 
364
+ @client.on(events.NewMessage(pattern=r"^/search(?:\s+(.+))?$"))
365
+ async def search_command(event):
366
+ user_id = event.sender_id
367
+ settings = await get_settings()
368
+ if not settings.get("search_enabled", True):
369
+ await event.respond("๐Ÿ”ง ุงู„ุจุญุซ ู…ุชูˆู‚ู ุญุงู„ูŠุงู‹ ู…ู† ู‚ุจู„ ุงู„ุฅุฏุงุฑุฉ.")
370
+ return
371
+ query = event.pattern_match.group(1)
372
+ if not query:
373
+ set_state(user_id, "search", "await_query")
374
+ await event.respond(
375
+ "ุฃุฑุณู„ Username ุฃูˆ User ID ู„ู„ุจุญุซ ุนู†ู‡.\nู…ุซุงู„: `@username` ุฃูˆ `123456789`",
376
+ buttons=[back_cancel_row()],
377
+ )
378
+ return
379
+ await perform_search(event, query.strip())
380
+
381
+
382
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
383
+ # Callback query router
384
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
385
+ @client.on(events.CallbackQuery())
386
+ async def callback_router(event):
387
+ user_id = event.sender_id
388
+ data = event.data.decode("utf-8", errors="ignore")
389
+ try:
390
+ if not check_flood(user_id):
391
+ await event.answer("โณ ุงู„ุฑุฌุงุก ุงู„ุงู†๏ฟฝ๏ฟฝุธุงุฑ ู‚ู„ูŠู„ุงู‹ ู‚ุจู„ ุงู„ู…ุชุงุจุนุฉ.", alert=True)
392
+ return
393
+
394
+ parts = data.split(":")
395
+ root = parts[0]
396
+
397
+ if root == "menu" and parts[1] == "main":
398
+ clear_state(user_id)
399
+ await event.edit("๐Ÿ–ค **MF BLACKLIST** ๐Ÿ–ค\n\nุงุฎุชุฑ ุฃุญุฏ ุงู„ุฎูŠุงุฑุงุช:",
400
+ buttons=await main_menu_buttons(user_id))
401
+
402
+ elif root == "flow" and parts[1] == "cancel":
403
+ clear_state(user_id)
404
+ await event.edit("โŒ ุชู… ุฅู„ุบุงุก ุงู„ุนู…ู„ูŠุฉ.", buttons=await main_menu_buttons(user_id))
405
+
406
+ elif root == "help":
407
+ await show_help(event)
408
+
409
+ elif root == "channel" and parts[1] == "show":
410
+ await show_channel(event)
411
+
412
+ elif root == "report":
413
+ await handle_report_callback(event, parts)
414
+
415
+ elif root == "evidence":
416
+ await handle_evidence_callback(event, parts)
417
+
418
+ elif root == "confirm":
419
+ await handle_confirm_callback(event, parts)
420
+
421
+ elif root == "search":
422
+ await handle_search_callback(event, parts)
423
+
424
+ elif root == "case":
425
+ await handle_case_callback(event, parts)
426
+
427
+ elif root == "admin":
428
+ await handle_admin_callback(event, parts)
429
+
430
+ elif root == "review":
431
+ await handle_review_callback(event, parts)
432
+
433
+ elif root == "template":
434
+ await handle_template_callback(event, parts)
435
+
436
+ elif root == "settings":
437
+ await handle_settings_callback(event, parts)
438
+
439
+ elif root == "cases":
440
+ await handle_cases_callback(event, parts)
441
+
442
+ else:
443
+ await event.answer("โ“ ุฒุฑ ุบูŠุฑ ู…ุนุฑูˆู.")
444
+ except events.StopPropagation:
445
+ raise
446
+ except Exception as e:
447
+ log.exception("Error handling callback %s: %s", data, e)
448
+ try:
449
+ await event.answer("โš ๏ธ ุญุฏุซ ุฎุทุฃ ุบูŠุฑ ู…ุชูˆู‚ุน. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰.", alert=True)
450
+ except Exception:
451
+ pass
452
+
453
+
454
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
455
+ # Help / channel info
456
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
457
+ async def show_help(event):
458
+ text = (
459
+ "๐Ÿ“– **ุดุฑุญ ุงู„ุจูˆุช**\n\n"
460
+ "MF BLACKLIST ุจูˆุช ู„ู„ุฅุจู„ุงุบ ุนู† ุงู„ุณุฑู‚ุฉ ูˆุงู„ุงู†ุชุญุงู„ ููŠ ุชูŠู„ูŠุฌุฑุงู….\n\n"
461
+ "๐Ÿšจ ู†ุดุฑ ุณุงุฑู‚ / โš ๏ธ ู†ุดุฑ ู…ู†ุชุญู„: ู„ุชู‚ุฏูŠู… ุจู„ุงุบ ุฌุฏูŠุฏ ู…ุน ุฃุฏู„ุฉ.\n"
462
+ "๐Ÿ” ุงู„ุจุญุซ ุนู† ุดุฎุต: ู„ู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุจู„ุงุบุงุช ุณุงุจู‚ุฉ ุถุฏ ุดุฎุต.\n\n"
463
+ "ูƒู„ ุจู„ุงุบ ูŠูุฑุงุฌุน ู…ู† ุงู„ุฅุฏุงุฑุฉ ู‚ุจู„ ู†ุดุฑู‡ ููŠ ุงู„ู‚ู†ุงุฉ ุงู„ุฑุณู…ูŠุฉ."
464
  )
465
+ await event.edit(text, buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"menu:main")]])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
+ async def show_channel(event):
469
+ settings = await get_settings()
470
+ uname = settings.get("channel_username")
471
+ if uname:
472
+ text = f"๐Ÿ“ข ู‚ู†ุงุฉ ุงู„ู†ุดุฑ ุงู„ุฑุณู…ูŠุฉ:\nhttps://t.me/{uname}"
473
+ else:
474
+ text = "๐Ÿ“ข ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ู‚ู†ุงุฉ ุงู„ู†ุดุฑ ุจุนุฏ ู…ู† ู‚ุจู„ ุงู„ุฅุฏุงุฑุฉ."
475
+ await event.edit(text, buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"menu:main")]])
476
+
477
+
478
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
479
+ # Report flow (thief / impersonator share logic via report_type)
480
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
481
+ async def handle_report_callback(event, parts):
482
+ user_id = event.sender_id
483
+ settings = await get_settings()
484
+ if not settings.get("reports_enabled", True):
485
+ await event.answer("๐Ÿ”ง ุงุณุชู‚ุจุงู„ ุงู„ุจู„ุงุบุงุช ู…ุชูˆู‚ู ุญุงู„ูŠุงู‹.", alert=True)
486
+ return
487
 
488
+ report_type = parts[1] # thief | impersonator
489
+ action = parts[2] if len(parts) > 2 else None
490
 
491
+ if action == "start":
492
+ remaining = check_cooldown(user_id)
493
+ if remaining > 0:
494
+ await event.answer(f"โณ ุงู„ุฑุฌุงุก ุงู„ุงู†ุชุธุงุฑ {int(remaining)} ุซุงู†ูŠุฉ ู‚ุจู„ ุชู‚ุฏูŠู… ุจู„ุงุบ ุฌุฏูŠุฏ.", alert=True)
495
+ return
496
+ set_state(user_id, f"report_{report_type}", "await_username", {"type": report_type, "evidence": []})
497
+ label = TYPE_LABEL[report_type]
498
+ await event.edit(
499
+ f"๐Ÿšจ ุจู„ุงุบ {label} โ€” ุงู„ุฎุทูˆุฉ 1\n\nุฃุฑุณู„ Username ุงู„ุดุฎุต ู…ุซู„ @username",
500
+ buttons=[
501
+ [Button.inline("ู„ุง ูŠูˆุฌุฏ ู…ุนุฑู", f"report:{report_type}:no_id".encode())],
502
+ back_cancel_row(),
503
+ ],
504
+ )
505
 
506
+ elif action == "no_id":
507
+ st = get_state(user_id)
508
+ if not st or not st["flow"].startswith("report_"):
509
+ await event.answer("ุงู†ุชู‡ุช ุงู„ุฌู„ุณุฉุŒ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ.", alert=True)
510
+ return
511
+ st["step"] = "await_userid"
512
+ touch_state(user_id)
513
+ await event.edit(
514
+ "ุฃุฑุณู„ Telegram User ID (ุฃุฑู‚ุงู… ูู‚ุท).",
515
+ buttons=[back_cancel_row()],
516
+ )
517
+
518
+
519
+ async def handle_evidence_callback(event, parts):
520
+ user_id = event.sender_id
521
+ action = parts[1]
522
+ st = get_state(user_id)
523
+ if not st or not st["flow"].startswith("report_"):
524
+ await event.answer("ุงู†ุชู‡ุช ุงู„ุฌู„ุณุฉุŒ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ.", alert=True)
525
  return
526
+ if action == "done":
527
+ evidence = st["data"].get("evidence", [])
528
+ if not evidence:
529
+ await event.answer("ูŠุฌุจ ุฅุฑุณุงู„ ุฏู„ูŠู„ ูˆุงุญุฏ ุนู„ู‰ ุงู„ุฃู‚ู„.", alert=True)
530
+ return
531
+ st["step"] = "await_description"
532
+ touch_state(user_id)
533
+ await event.edit(
534
+ f"โœ… ุชู… ุงุณุชู„ุงู… {len(evidence)} ุฏู„ูŠู„/ุฃุฏู„ุฉ.\n\nุงูƒุชุจ ู…ูˆุถูˆุน ุงู„ุณุฑู‚ุฉ/ุงู„ุงู†ุชุญุงู„ ุจุงู„ุชูุตูŠู„.",
535
+ buttons=[back_cancel_row()],
536
+ )
537
 
538
 
539
+ async def handle_confirm_callback(event, parts):
540
+ user_id = event.sender_id
541
+ action = parts[1]
542
+ st = get_state(user_id)
543
+ if not st or not st["flow"].startswith("report_"):
544
+ await event.answer("ุงู†ุชู‡ุช ุงู„ุฌู„ุณุฉุŒ ุงุจุฏุฃ ู…ู† ุฌุฏูŠุฏ.", alert=True)
545
  return
 
 
546
 
547
+ if action == "cancel":
548
+ clear_state(user_id)
549
+ await event.edit("โŒ ุชู… ุฅู„ุบุงุก ุงู„ุจู„ุงุบ.", buttons=await main_menu_buttons(user_id))
550
+ return
551
 
552
+ if action == "edit":
553
+ st["step"] = "await_description"
554
+ touch_state(user_id)
555
+ await event.edit("โœ๏ธ ุฃุฑุณู„ ู…ูˆุถูˆุน ุงู„ุณุฑู‚ุฉ/ุงู„ุงู†ุชุญุงู„ ู…ู† ุฌุฏูŠุฏ.", buttons=[back_cancel_row()])
556
+ return
 
557
 
558
+ if action == "send":
559
+ data = st["data"]
560
+ report_type = data["type"]
561
+ case_id = await next_case_id()
562
+ now = datetime.datetime.utcnow()
563
+ case_doc = {
564
+ "case_id": case_id,
565
+ "type": report_type,
566
+ "user_id": data.get("target_user_id"),
567
+ "username": data.get("target_username"),
568
+ "submitter_id": user_id,
569
+ "description": data.get("description", ""),
570
+ "evidence": data.get("evidence", []),
571
+ "status": "pending",
572
+ "created_at": now,
573
+ "reviewed_at": None,
574
+ "reviewer_id": None,
575
+ "channel_id": None,
576
+ "channel_message_id": None,
577
+ "published_at": None,
578
+ }
579
+ try:
580
+ await col_cases.insert_one(case_doc)
581
+ await col_pending.insert_one({"case_id": case_id, "created_at": now})
582
+ except PyMongoError as e:
583
+ log.error("Failed to insert case: %s", e)
584
+ await event.edit("โš ๏ธ ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ุญูุธ ุงู„ุจู„ุงุบ. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰ ู„ุงุญู‚ุงู‹.",
585
+ buttons=await main_menu_buttons(user_id))
586
+ clear_state(user_id)
587
  return
 
 
 
588
 
589
+ mark_action(user_id)
590
+ clear_state(user_id)
591
+ await log_action(user_id, "create_case", {"type": report_type}, case_id=case_id)
592
+ await send_to_review_group(case_doc)
593
+ await event.edit(
594
+ f"โœ… ุชู… ุฅุฑุณุงู„ ุจู„ุงุบูƒ ุจู†ุฌุงุญ.\nุฑู‚ู… ุงู„ุจู„ุงุบ: {case_id}\nุณูŠุชู… ู…ุฑุงุฌุนุชู‡ ู…ู† ู‚ุจู„ ุงู„ุฅุฏุงุฑุฉ.",
595
+ buttons=await main_menu_buttons(user_id),
596
+ )
597
 
 
 
 
 
598
 
599
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
600
+ # Message handler (drives multi-step flows: text + media)
601
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
602
+ @client.on(events.NewMessage(func=lambda e: e.is_private and not (e.raw_text or "").startswith("/")))
603
+ async def message_router(event):
604
+ user_id = event.sender_id
605
+ st = get_state(user_id)
606
+ if not st:
607
+ return # ignore unexpected messages outside of a flow
608
 
609
+ if not check_flood(user_id):
610
+ await event.respond("โณ ุงู„ุฑุฌุงุก ุงู„ุงู†ุชุธุงุฑ ู‚ู„ูŠู„ุงู‹.")
611
+ return
612
 
613
+ flow = st["flow"]
614
+ try:
615
+ if flow.startswith("report_"):
616
+ await drive_report_flow(event, st)
617
+ elif flow == "search":
618
+ await drive_search_flow(event, st)
619
+ elif flow == "admin_add":
620
+ await drive_admin_add_flow(event, st)
621
+ elif flow == "template_edit":
622
+ await drive_template_edit_flow(event, st)
623
+ elif flow == "channel_set":
624
+ await drive_channel_set_flow(event, st)
625
+ elif flow == "review_group_set":
626
+ await drive_review_group_set_flow(event, st)
627
+ elif flow == "maintenance_msg":
628
+ await drive_maintenance_msg_flow(event, st)
629
+ elif flow == "case_search":
630
+ await drive_case_search_flow(event, st)
631
+ elif flow == "provide_more_info":
632
+ await drive_more_info_flow(event, st)
633
+ else:
634
+ clear_state(user_id)
635
+ except events.StopPropagation:
636
+ raise
637
+ except Exception as e:
638
+ log.exception("Error in message_router (flow=%s): %s", flow, e)
639
+ await event.respond("โš ๏ธ ุญุฏุซ ุฎุทุฃ ุบูŠุฑ ู…ุชูˆู‚ุนุŒ ุชู… ุฅู„ุบุงุก ุงู„ุนู…ู„ูŠุฉ ุงู„ุญุงู„ูŠุฉ.")
640
+ clear_state(user_id)
641
+
642
+
643
+ async def drive_report_flow(event, st):
644
+ user_id = event.sender_id
645
+ step = st["step"]
646
+ data = st["data"]
647
+ report_type = data["type"]
648
+
649
+ if step == "await_username":
650
+ text = (event.raw_text or "").strip()
651
+ if not text.startswith("@") and not text.replace("_", "").isalnum():
652
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ Username ุตุญูŠุญ ู…ุซู„ @usernameุŒ ุฃูˆ ุงุถุบุท 'ู„ุง ูŠูˆุฌุฏ ู…ุนุฑู'.")
653
+ return
654
+ entity = await resolve_username(text)
655
+ if entity:
656
+ data["target_username"] = getattr(entity, "username", text.lstrip("@"))
657
+ data["target_user_id"] = entity.id
658
+ await proceed_to_duplicate_check(event, st)
659
+ else:
660
+ data["target_username"] = text.lstrip("@")
661
+ data["target_user_id"] = None
662
+ st["step"] = "await_userid_fallback"
663
+ touch_state(user_id)
664
+ await event.respond(
665
+ "ุชุนุฐุฑ ุงู„ุญุตูˆู„ ุนู„ู‰ ู…ุนู„ูˆู…ุงุช ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… ุชู„ู‚ุงุฆูŠุงู‹.\n"
666
+ "ุฃุฑุณู„ Telegram User ID ุงู„ุฎุงุต ุจู‡ ุฅู† ูƒุงู† ู…ุนุฑูˆูุงู‹ุŒ ุฃูˆ ุงูƒุชุจ 'ุชุฎุทูŠ' ู„ู„ู…ุชุงุจุนุฉ ุจุฏูˆู† ID."
667
+ )
668
 
669
+ elif step == "await_userid_fallback":
670
+ text = (event.raw_text or "").strip()
671
+ if text in ("ุชุฎุทูŠ", "skip"):
672
+ await proceed_to_duplicate_check(event, st)
673
+ return
674
+ if not text.isdigit():
675
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ุฑู‚ู… ID ุตุญูŠุญุŒ ุฃูˆ ุงูƒุชุจ 'ุชุฎุทูŠ'.")
676
+ return
677
+ data["target_user_id"] = int(text)
678
+ await proceed_to_duplicate_check(event, st)
679
 
680
+ elif step == "await_userid":
681
+ text = (event.raw_text or "").strip()
682
+ if not text.isdigit():
683
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ Telegram User ID ูƒุฑู‚ู… ุตุญูŠุญ.")
684
+ return
685
+ uid = int(text)
686
+ entity = await resolve_user_id(uid)
687
+ data["target_user_id"] = uid
688
+ data["target_username"] = getattr(entity, "username", None) if entity else None
689
+ await proceed_to_duplicate_check(event, st)
690
+
691
+ elif step == "await_evidence":
692
+ if event.photo or event.video or event.document or event.file:
693
+ data.setdefault("evidence", []).append({
694
+ "message_id": event.id,
695
+ "chat_id": event.chat_id,
696
+ "media_type": (
697
+ "photo" if event.photo else
698
+ "video" if event.video else
699
+ "document"
700
+ ),
701
+ })
702
+ touch_state(user_id)
703
+ count = len(data["evidence"])
704
+ await event.respond(
705
+ f"โœ… ุชู… ุงุณุชู„ุงู… ุงู„ุฏู„ูŠู„ ุฑู‚ู… {count}. ุฃุฑุณู„ ุงู„ู…ุฒูŠุฏ ุฃูˆ ุงุถุบุท 'ุชู… ุฅุฑุณุงู„ ุฌู…ูŠุน ุงู„ุฃุฏู„ุฉ'.",
706
+ buttons=[[Button.inline("โœ… ุชู… ุฅุฑุณุงู„ ุฌู…ูŠุน ุงู„ุฃุฏู„ุฉ", b"evidence:done")], back_cancel_row()],
707
+ )
708
+ else:
709
+ await event.respond(
710
+ "ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ุตูˆุฑุฉุŒ ููŠุฏูŠูˆ ุฃูˆ ู…ู„ู ูƒุฏู„ูŠู„ุŒ ุฃูˆ ุงุถุบุท 'ุชู… ุฅุฑุณุงู„ ุฌู…ูŠุน ุงู„ุฃุฏู„ุฉ'.",
711
+ buttons=[[Button.inline("โœ… ุชู… ุฅุฑุณุงู„ ุฌู…ูŠุน ุงู„ุฃุฏู„ุฉ", b"evidence:done")], back_cancel_row()],
712
+ )
713
+
714
+ elif step == "await_description":
715
+ text = (event.raw_text or "").strip()
716
+ if not text:
717
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ู†ุต ูŠุตู ุงู„ู…ูˆุถูˆุน ุจุงู„ุชูุตูŠู„.")
718
+ return
719
+ data["description"] = text
720
+ st["step"] = "confirm"
721
+ touch_state(user_id)
722
+ await send_preview(event, data)
723
+
724
+
725
+ async def proceed_to_duplicate_check(event, st):
726
+ user_id = event.sender_id
727
+ data = st["data"]
728
+ dup = None
729
+ if data.get("target_user_id"):
730
+ dup = await col_cases.find_one({"user_id": data["target_user_id"]})
731
+ elif data.get("target_username"):
732
+ dup = await col_cases.find_one({"username": data["target_username"]})
733
+
734
+ if dup and not data.get("dup_ack"):
735
+ data["dup_ack_pending"] = True
736
+ st["step"] = "await_dup_ack"
737
+ touch_state(user_id)
738
  await event.respond(
739
+ f"โš ๏ธ ูŠูˆุฌุฏ ุจู„ุงุบ ู…ุณุฌู„ ู…ุณุจู‚ุงู‹ ู„ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… (ุฑู‚ู… ุงู„ุจู„ุงุบ: {dup.get('case_id')}).",
740
+ buttons=[
741
+ [Button.inline("ู…ุชุงุจุนุฉ ูˆุฅุฑุณุงู„ ุจู„ุงุบ ุฌุฏูŠุฏ", b"dup:continue")],
742
+ back_cancel_row(),
743
+ ],
744
  )
745
  return
746
+ await proceed_to_evidence(event, st)
747
 
 
 
 
 
 
 
 
 
 
 
 
748
 
749
+ @client.on(events.CallbackQuery(pattern=b"^dup:continue$"))
750
+ async def dup_continue_handler(event):
751
+ user_id = event.sender_id
752
+ st = get_state(user_id)
753
+ if not st or not st["flow"].startswith("report_"):
754
+ await event.answer("ุงู†ุชู‡ุช ุงู„ุฌู„ุณุฉ.", alert=True)
755
+ return
756
+ st["data"]["dup_ack"] = True
757
+ await proceed_to_evidence(event, st, is_callback=True)
758
+
759
+
760
+ async def proceed_to_evidence(event, st, is_callback=False):
761
+ user_id = event.sender_id if not is_callback else event.sender_id
762
+ st["step"] = "await_evidence"
763
+ st["data"].setdefault("evidence", [])
764
+ touch_state(user_id)
765
+ msg = "ุฃุฑุณู„ ุตูˆุฑ ูˆููŠุฏูŠูˆู‡ุงุช ูˆู…ู„ูุงุช ูˆุฃุฏู„ุฉ ุงู„ุณุฑู‚ุฉ.\nูŠู…ูƒู†ูƒ ุฅุฑุณุงู„ ุนุฏุฉ ุฃุฏู„ุฉ."
766
+ buttons = [[Button.inline("โœ… ุชู… ุฅุฑุณุงู„ ุฌู…ูŠุน ุงู„ุฃุฏู„ุฉ", b"evidence:done")], back_cancel_row()]
767
+ if is_callback:
768
+ await event.edit(msg, buttons=buttons)
769
+ else:
770
+ await event.respond(msg, buttons=buttons)
771
+
772
+
773
+ async def send_preview(event, data):
774
+ label = TYPE_LABEL[data["type"]]
775
+ uname = f"@{data['target_username']}" if data.get("target_username") else "ุบูŠุฑ ู…ุชูˆูุฑ"
776
+ uid = data.get("target_user_id") or "ุบูŠุฑ ู…ุชูˆูุฑ"
777
+ evidence_count = len(data.get("evidence", []))
778
+ text = (
779
+ "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n"
780
+ f"ู†ูˆุน ุงู„ุจู„ุงุบ: {label}\n"
781
+ f"Username: {uname}\n"
782
+ f"User ID: {uid}\n"
783
+ f"ุนุฏุฏ ุงู„ุฃุฏู„ุฉ: {evidence_count}\n\n"
784
+ f"ุงู„ู…ูˆุถูˆุน:\n{data.get('description','')}\n"
785
+ "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
786
+ )
787
+ await event.respond(
788
+ text,
789
+ buttons=[
790
+ [Button.inline("โœ… ุฅุฑุณุงู„ ุงู„ุจู„ุงุบ", b"confirm:send")],
791
+ [Button.inline("โœ๏ธ ุชุนุฏูŠู„", b"confirm:edit")],
792
+ [Button.inline("โŒ ุฅู„ุบุงุก", b"confirm:cancel")],
793
+ ],
794
+ )
795
 
796
 
797
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
798
+ # Sending report to review group / publishing to channel
799
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
800
+ async def send_to_review_group(case_doc):
801
+ settings = await get_settings()
802
+ group_id = settings.get("review_group_id")
803
+ if not group_id:
804
+ log.warning("No review group configured; case %s not forwarded", case_doc["case_id"])
805
+ return
 
 
806
 
807
+ label = TYPE_LABEL.get(case_doc["type"], case_doc["type"])
808
+ submitter = case_doc.get("submitter_id")
809
+ text = (
810
+ "๐Ÿ†• ุจู„ุงุบ ุฌุฏูŠุฏ\n\n"
811
+ f"Case ID: {case_doc['case_id']}\n"
812
+ f"ุงู„ู†ูˆุน: {label}\n"
813
+ f"Username: @{case_doc['username']}\n" if case_doc.get("username") else "Username: ุบูŠุฑ ู…ุชูˆูุฑ\n"
814
+ )
815
+ text += (
816
+ f"User ID: {case_doc.get('user_id') or 'ุบูŠุฑ ู…ุชูˆูุฑ'}\n"
817
+ f"ู…ู‚ุฏู… ุงู„ุจู„ุงุบ: {submitter}\n\n"
818
+ f"ุงู„ู…ูˆุถูˆุน:\n{case_doc.get('description','')}\n\n"
819
+ f"ุนุฏุฏ ุงู„ุฃุฏู„ุฉ: {len(case_doc.get('evidence', []))}"
820
  )
821
+ buttons = [
822
+ [Button.inline("โœ… ู…ูˆุงูู‚ุฉ ูˆู†ุดุฑ", f"review:approve:{case_doc['case_id']}".encode())],
823
+ [Button.inline("โŒ ุฑูุถ", f"review:reject:{case_doc['case_id']}".encode())],
824
+ [Button.inline("๐Ÿ“ ุทู„ุจ ู…ุนู„ูˆู…ุงุช", f"review:info:{case_doc['case_id']}".encode())],
825
+ ]
826
+ try:
827
+ await safe_call(client.send_message, group_id, text, buttons=buttons)
828
+ for ev in case_doc.get("evidence", []):
829
+ try:
830
+ await safe_call(client.forward_messages, group_id, ev["message_id"], ev["chat_id"])
831
+ except RPCError as e:
832
+ log.error("Failed forwarding evidence for %s: %s", case_doc["case_id"], e)
833
+ except RPCError as e:
834
+ log.error("Failed to send case %s to review group: %s", case_doc["case_id"], e)
835
+
836
+
837
+ async def publish_case(case_doc, admin_id):
838
+ settings = await get_settings()
839
+ channel_id = settings.get("channel_id")
840
+ if not channel_id:
841
+ return False, "ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ู‚ู†ุงุฉ ุงู„ู†ุดุฑ ุจุนุฏ."
842
+
843
+ template = settings.get("template_thief") if case_doc["type"] == "thief" else settings.get("template_impersonator")
844
+ template = template or (DEFAULT_TEMPLATE_THIEF if case_doc["type"] == "thief" else DEFAULT_TEMPLATE_IMPERSONATOR)
845
 
846
+ rendered = render_template(template, case_doc)
847
 
848
+ try:
849
+ evidence = case_doc.get("evidence", [])
850
+ sent_msg = None
851
+ if evidence:
852
+ first = evidence[0]
853
+ sent_msg = await safe_call(
854
+ client.send_message, channel_id, rendered,
855
+ file=await get_input_media(first),
856
+ )
857
+ for ev in evidence[1:]:
858
+ try:
859
+ await safe_call(client.forward_messages, channel_id, ev["message_id"], ev["chat_id"])
860
+ except RPCError as e:
861
+ log.error("Failed forwarding extra evidence to channel: %s", e)
862
+ else:
863
+ sent_msg = await safe_call(client.send_message, channel_id, rendered)
864
+
865
+ now = datetime.datetime.utcnow()
866
+ await col_cases.update_one(
867
+ {"case_id": case_doc["case_id"]},
868
+ {"$set": {
869
+ "status": "approved",
870
+ "reviewed_at": now,
871
+ "reviewer_id": admin_id,
872
+ "channel_id": channel_id,
873
+ "channel_message_id": sent_msg.id,
874
+ "published_at": now,
875
+ }},
876
+ )
877
+ await col_pending.delete_one({"case_id": case_doc["case_id"]})
878
+ return True, None
879
+ except RPCError as e:
880
+ log.error("Publishing failed for case %s: %s", case_doc["case_id"], e)
881
+ return False, str(e)
882
+
883
+
884
+ async def get_input_media(evidence_item):
885
+ try:
886
+ msg = await client.get_messages(evidence_item["chat_id"], ids=evidence_item["message_id"])
887
+ if msg and msg.media:
888
+ return msg.media
889
+ except RPCError:
890
+ pass
891
+ return None
892
+
893
+
894
+ def render_template(template: str, case_doc: dict) -> str:
895
+ return template.format(
896
+ case_id=case_doc.get("case_id", ""),
897
+ username=("@" + case_doc["username"]) if case_doc.get("username") else "ุบูŠุฑ ู…ุชูˆูุฑ",
898
+ user_id=case_doc.get("user_id") or "ุบูŠุฑ ู…ุชูˆูุฑ",
899
+ date=case_doc.get("created_at", datetime.datetime.utcnow()).strftime("%Y-%m-%d"),
900
+ description=case_doc.get("description", ""),
901
+ type=TYPE_LABEL.get(case_doc.get("type"), case_doc.get("type")),
902
  )
903
+
904
+
905
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
906
+ # Review callbacks (approve / reject / request info)
907
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
908
+ async def handle_review_callback(event, parts):
909
+ user_id = event.sender_id
910
+ action = parts[1]
911
+ case_id = parts[2]
912
+
913
+ case_doc = await col_cases.find_one({"case_id": case_id})
914
+ if not case_doc:
915
+ await event.answer("โš ๏ธ ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ุจู„ุงุบ.", alert=True)
916
+ return
917
+
918
+ if action == "approve":
919
+ if not await has_permission(user_id, "approve_reports"):
920
+ await event.answer("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ ุงู„ู…ูˆุงูู‚ุฉ ุนู„ู‰ ุงู„ุจู„ุงุบุงุช.", alert=True)
921
  return
922
+ ok, err = await publish_case(case_doc, user_id)
923
+ if ok:
924
+ await log_action(user_id, "approve", case_id=case_id)
925
+ await event.answer("โœ… ุชู… ุงู„ู†ุดุฑ ุจู†ุฌุงุญ.")
926
+ try:
927
+ await event.edit(event.text + "\n\nโœ… ุชู…ุช ุงู„ู…ูˆุงูู‚ุฉ ูˆุงู„ู†ุดุฑ.", buttons=None)
928
+ except Exception:
929
+ pass
930
+ try:
931
+ await safe_call(client.send_message, case_doc["submitter_id"],
932
+ "ุชู…ุช ู…ุฑุงุฌุนุฉ ุงู„ุจู„ุงุบ ูˆุงู„ู…ูˆุงูู‚ุฉ ุนู„ูŠู‡ ูˆู†ุดุฑู‡.")
933
+ except RPCError:
934
+ pass
935
+ else:
936
+ await event.answer(f"โš ๏ธ ูุดู„ ุงู„ู†ุดุฑ: {err}", alert=True)
937
+ try:
938
+ await safe_call(client.send_message, user_id, f"โš ๏ธ ูุดู„ ู†ุดุฑ ุงู„ุจู„ุงุบ {case_id}: {err}")
939
+ except RPCError:
940
+ pass
941
+
942
+ elif action == "reject":
943
+ if not await has_permission(user_id, "reject_reports"):
944
+ await event.answer("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ ุฑูุถ ุงู„ุจู„ุงุบุงุช.", alert=True)
945
  return
946
+ await col_cases.update_one(
947
+ {"case_id": case_id},
948
+ {"$set": {"status": "rejected", "reviewed_at": datetime.datetime.utcnow(), "reviewer_id": user_id}},
949
+ )
950
+ await col_pending.delete_one({"case_id": case_id})
951
+ await log_action(user_id, "reject", case_id=case_id)
952
+ await event.answer("โŒ ุชู… ุฑูุถ ุงู„ุจู„ุงุบ.")
953
+ try:
954
+ await event.edit(event.text + "\n\nโŒ ุชู… ุงู„ุฑูุถ.", buttons=None)
955
  except Exception:
956
+ pass
957
+ try:
958
+ await safe_call(client.send_message, case_doc["submitter_id"],
959
+ f"ุชู… ุฑูุถ ุงู„ุจู„ุงุบ {case_id} ู…ู† ู‚ุจู„ ุงู„ุฅุฏุงุฑุฉ.")
960
+ except RPCError:
961
+ pass
962
+
963
+ elif action == "info":
964
+ if not await has_permission(user_id, "manage_reports"):
965
+ await event.answer("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ ุทู„ุจ ู…ุนู„ูˆู…ุงุช.", alert=True)
 
 
 
 
 
 
 
 
 
966
  return
967
+ await col_cases.update_one(
968
+ {"case_id": case_id},
969
+ {"$set": {"status": "needs_more_info"}},
970
+ )
971
+ await log_action(user_id, "request_info", case_id=case_id)
972
+ set_state(case_doc["submitter_id"], "provide_more_info", "await_text", {"case_id": case_id})
973
  try:
974
+ await safe_call(client.send_message, case_doc["submitter_id"],
975
+ f"ุงู„ุฅุฏุงุฑุฉ ุชุญุชุงุฌ ู…ุนู„ูˆู…ุงุช ุฅุถุงููŠุฉ ุนู† ุงู„ุจู„ุงุบ {case_id}.\n"
976
+ "ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ุงู„ู…ุนู„ูˆู…ุงุช ุงู„ุฅุถุงููŠุฉ ุงู„ุขู†.")
977
+ except RPCError:
978
+ pass
979
+ await event.answer("๐Ÿ“ ุชู… ุฅุฑุณุงู„ ุทู„ุจ ุงู„ู…ุนู„ูˆู…ุงุช ู„ู„ู…ูุจู„ูู‘ุบ.")
980
+
981
+
982
+ async def drive_more_info_flow(event, st):
983
+ user_id = event.sender_id
984
+ case_id = st["data"]["case_id"]
985
+ extra_text = (event.raw_text or "").strip()
986
+ if not extra_text:
987
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ู†ุต ูŠุญุชูˆูŠ ุงู„ู…ุนู„ูˆู…ุงุช ุงู„ุฅุถุงููŠุฉ.")
988
+ return
989
+ case_doc = await col_cases.find_one({"case_id": case_id})
990
+ if not case_doc:
991
+ clear_state(user_id)
992
+ return
993
+ new_desc = case_doc.get("description", "") + f"\n\n[ู…ุนู„ูˆู…ุงุช ุฅุถุงููŠุฉ]: {extra_text}"
994
+ await col_cases.update_one({"case_id": case_id}, {"$set": {"description": new_desc, "status": "pending"}})
995
+ await col_pending.update_one({"case_id": case_id}, {"$set": {"created_at": datetime.datetime.utcnow()}}, upsert=True)
996
+ clear_state(user_id)
997
+ case_doc["description"] = new_desc
998
+ await send_to_review_group(case_doc)
999
+ await event.respond("โœ… ุชู… ุฅุฑุณุงู„ ุงู„ู…ุนู„ูˆู…ุงุช ุงู„ุฅุถุงููŠุฉุŒ ุณูŠูุนุงุฏ ูุญุต ุงู„ุจู„ุงุบ ู…ู† ู‚ุจู„ ุงู„ุฅุฏุงุฑุฉ.")
1000
+
1001
+
1002
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1003
+ # Search
1004
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1005
+ async def handle_search_callback(event, parts):
1006
+ user_id = event.sender_id
1007
+ action = parts[1]
1008
+ if action == "start":
1009
+ settings = await get_settings()
1010
+ if not settings.get("search_enabled", True):
1011
+ await event.answer("๐Ÿ”ง ุงู„ุจุญุซ ู…ุชูˆู‚ู ุญุงู„ูŠุงู‹.", alert=True)
1012
  return
1013
+ set_state(user_id, "search", "await_query")
1014
+ await event.edit(
1015
+ "ุฃุฑุณู„ Username ุฃูˆ User ID ู„ู„ุจุญุซ ุนู†ู‡.\nู…ุซุงู„: @username ุฃูˆ 123456789",
1016
+ buttons=[back_cancel_row()],
1017
+ )
1018
+
1019
+
1020
+ async def drive_search_flow(event, st):
1021
+ query = (event.raw_text or "").strip()
1022
+ if not query:
1023
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ Username ุฃูˆ User ID ุตุญูŠุญ.")
1024
+ return
1025
+ clear_state(event.sender_id)
1026
+ await perform_search(event, query)
1027
+
1028
+
1029
+ async def perform_search(event, query):
1030
+ query = query.strip()
1031
+ target_user_id = None
1032
+ target_username = None
1033
+
1034
+ if query.isdigit():
1035
+ target_user_id = int(query)
1036
+ else:
1037
+ target_username = query.lstrip("@")
1038
+ entity = await resolve_username(target_username)
1039
+ if entity:
1040
+ target_user_id = entity.id
1041
+ target_username = getattr(entity, "username", target_username)
1042
+ # if not resolvable, we still try searching stored username as-is
1043
+
1044
+ mongo_filter = {"$or": []}
1045
+ if target_user_id:
1046
+ mongo_filter["$or"].append({"user_id": target_user_id})
1047
+ if target_username:
1048
+ mongo_filter["$or"].append({"username": target_username})
1049
+
1050
+ if not mongo_filter["$or"]:
1051
+ await event.respond("ุชุนุฐุฑ ุงู„ุญุตูˆู„ ุนู„ู‰ ู…ุนู„ูˆู…ุงุช ู‡ุฐุง ุงู„ู€Username ุญุงู„ูŠุงู‹ุŒ ุญุงูˆู„ ุงุณุชุฎุฏุงู… User ID.")
1052
+ return
1053
+
1054
+ cases = await col_cases.find(mongo_filter).sort("created_at", ASCENDING).to_list(length=200)
1055
+ if not cases:
1056
+ await event.respond("ู„ุง ุชูˆุฌุฏ ุจู„ุงุบุงุช ู…ุณุฌู„ุฉ ู„ู‡ุฐุง ุงู„ุดุฎุต.")
1057
+ return
1058
+
1059
+ total = len(cases)
1060
+ thief_count = sum(1 for c in cases if c["type"] == "thief")
1061
+ imp_count = sum(1 for c in cases if c["type"] == "impersonator")
1062
+ first_date = cases[0]["created_at"].strftime("%Y-%m-%d")
1063
+ last_date = cases[-1]["created_at"].strftime("%Y-%m-%d")
1064
+ approved_count = sum(1 for c in cases if c["status"] == "approved")
1065
+
1066
+ uname_disp = f"@{cases[0]['username']}" if cases[0].get("username") else "ุบูŠุฑ ู…ุชูˆูุฑ"
1067
+ uid_disp = cases[0].get("user_id") or "ุบูŠุฑ ู…ุชูˆูุฑ"
1068
+
1069
+ text = (
1070
+ "๐Ÿ” ู†ุชุงุฆุฌ ุงู„ุจุญุซ\n\n"
1071
+ f"๐Ÿ‘ค Username: {uname_disp}\n"
1072
+ f"๐Ÿ†” User ID: {uid_disp}\n\n"
1073
+ f"๐Ÿšจ ุฅุฌู…ุงู„ูŠ ุงู„ุจู„ุงุบุงุช: {total}\n"
1074
+ f"๐Ÿ”ด ุณุงุฑู‚: {thief_count}\n"
1075
+ f"๐ŸŸ  ู…ู†ุชุญู„: {imp_count}\n"
1076
+ f"โœ… ู…ู‚ุจูˆู„ุฉ: {approved_count}\n"
1077
+ f"๐Ÿ“… ุฃูˆู„ ุจู„ุงุบ: {first_date}\n"
1078
+ f"๐Ÿ“… ุขุฎุฑ ุจู„ุงุบ: {last_date}"
1079
+ )
1080
+ buttons = []
1081
+ for c in cases[:25]:
1082
+ buttons.append([Button.inline(f"๐Ÿ“„ {c['case_id']}", f"case:view:{c['case_id']}".encode())])
1083
+ buttons.append([Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"menu:main")])
1084
+ await event.respond(text, buttons=buttons)
1085
+
1086
+
1087
+ async def handle_case_callback(event, parts):
1088
+ action = parts[1]
1089
+ if action == "view":
1090
+ case_id = parts[2]
1091
+ case_doc = await col_cases.find_one({"case_id": case_id})
1092
+ if not case_doc:
1093
+ await event.answer("ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ุจู„ุงุบ.", alert=True)
1094
  return
1095
+ if case_doc.get("status") != "approved" or not case_doc.get("channel_message_id"):
1096
+ await event.answer("ู‡ุฐุง ุงู„ุจู„ุงุบ ุบูŠุฑ ู…ู†ุดูˆุฑ ุจุนุฏ.", alert=True)
 
 
 
 
1097
  return
1098
+ settings = await get_settings()
1099
+ channel_username = settings.get("channel_username")
1100
+ msg_id = case_doc["channel_message_id"]
1101
+ if channel_username:
1102
+ link = f"https://t.me/{channel_username}/{msg_id}"
1103
+ else:
1104
+ raw_channel_id = str(case_doc["channel_id"]).replace("-100", "")
1105
+ link = f"https://t.me/c/{raw_channel_id}/{msg_id}"
1106
+ await event.answer(url=link)
1107
+
1108
+
1109
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1110
+ # Admin panel
1111
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1112
+ async def handle_admin_callback(event, parts):
1113
+ user_id = event.sender_id
1114
+ if not await is_admin(user_id):
1115
+ await event.answer("๐Ÿšซ ู‡ุฐุง ุงู„ู‚ุณู… ู„ู„ุฅุฏุงุฑุฉ ูู‚ุท.", alert=True)
1116
+ return
1117
 
1118
+ action = parts[1] if len(parts) > 1 else "panel"
1119
+
1120
+ if action == "panel":
1121
+ await show_admin_panel(event)
1122
+ elif action == "management":
1123
+ await show_management_menu(event)
1124
+ elif action == "add":
1125
+ if not await has_permission(user_id, "manage_admins") or not is_owner(user_id):
1126
+ if not is_owner(user_id):
1127
+ await event.answer("๐Ÿšซ ุฅุถุงูุฉ ุงู„ุฃุฏู…ู† ู„ู„ู€Owner ูู‚ุท.", alert=True)
1128
+ return
1129
+ set_state(user_id, "admin_add", "await_id")
1130
+ await event.edit("ุฃุฑุณู„ User ID ู„ู„ู…ุณุชุฎุฏู… ุงู„ุฐูŠ ุชุฑูŠุฏ ุฅุถุงูุชู‡ ูƒุฃุฏู…ู†.", buttons=[back_cancel_row(b"admin:management")])
1131
+ elif action == "list":
1132
+ await show_admin_list(event)
1133
+ elif action == "remove":
1134
+ if not is_owner(user_id):
1135
+ await event.answer("๐Ÿšซ ุฅุฒุงู„ุฉ ุงู„ุฃุฏู…ู† ู„ู„ู€Owner ูู‚ุท.", alert=True)
1136
  return
1137
+ target_id = int(parts[2])
1138
+ if target_id == OWNER_ID:
1139
+ await event.answer("๐Ÿšซ ู„ุง ูŠู…ูƒู† ุฅุฒุงู„ุฉ ุงู„ู€Owner.", alert=True)
 
 
1140
  return
1141
+ await col_admins.delete_one({"user_id": target_id})
1142
+ await log_action(user_id, "remove_admin", {"target": target_id})
1143
+ await event.answer("โœ… ุชู…ุช ุงู„ุฅุฒุงู„ุฉ.")
1144
+ await show_admin_list(event)
1145
+ elif action == "perms":
1146
+ target_id = int(parts[2])
1147
+ await show_admin_perms(event, target_id)
1148
+ elif action == "permtoggle":
1149
+ if not is_owner(user_id):
1150
+ await event.answer("๐Ÿšซ ู„ู„ู€Owner ูู‚ุท.", alert=True)
1151
+ return
1152
+ target_id = int(parts[2])
1153
+ perm = parts[3]
1154
+ doc = await col_admins.find_one({"user_id": target_id})
1155
+ if not doc:
1156
+ await event.answer("ุงู„ุฃุฏู…ู† ุบูŠุฑ ู…ูˆุฌูˆุฏ.", alert=True)
1157
  return
1158
+ current = doc.get("permissions", {}).get(perm, False)
1159
+ await col_admins.update_one({"user_id": target_id}, {"$set": {f"permissions.{perm}": not current}})
1160
+ await show_admin_perms(event, target_id)
1161
+ elif action == "publishing":
1162
+ await show_publishing_menu(event)
1163
+ elif action == "setchannel":
1164
+ set_state(user_id, "channel_set", "await_channel")
1165
+ await event.edit("ุฃุฑุณู„ ู…ุนุฑู/ูŠูˆุฒุฑ ู‚ู†ุงุฉ ุงู„ู†ุดุฑ (ู…ุซุงู„ @channelname) ุฃูˆ ID ุงู„ุฎุงุต ุจู‡ุง.",
1166
+ buttons=[back_cancel_row(b"admin:publishing")])
1167
+ elif action == "setreviewgroup":
1168
+ set_state(user_id, "review_group_set", "await_group")
1169
+ await event.edit("ุฃุฑุณู„ ู…ุนุฑู/ูŠูˆุฒุฑ ู…ุฌู…ูˆุนุฉ ุงู„ู…ุฑุงุฌุนุฉ ุฃูˆ ID ุงู„ุฎุงุตุฉ ุจู‡ุง.",
1170
+ buttons=[back_cancel_row(b"admin:publishing")])
1171
+ elif action == "templates":
1172
+ await show_templates_menu(event)
1173
+ elif action == "stats":
1174
+ await show_stats(event)
1175
+ elif action == "reports_menu":
1176
+ await show_reports_menu(event)
1177
+ elif action == "settings_menu":
1178
+ await show_settings_menu(event)
1179
+
1180
+
1181
+ async def show_admin_panel(event):
1182
+ rows = [
1183
+ [Button.inline("๐Ÿ‘‘ ุงู„ุฅุฏุงุฑุฉ", b"admin:management")],
1184
+ [Button.inline("๐Ÿ“ข ุงู„ู†ุดุฑ", b"admin:publishing")],
1185
+ [Button.inline("๐Ÿ“Š ุงู„ุฅุญุตุงุฆูŠุงุช", b"admin:stats")],
1186
+ [Button.inline("๐Ÿ“‹ ุฅุฏุงุฑุฉ ุงู„ุจู„ุงุบุงุช", b"admin:reports_menu")],
1187
+ [Button.inline("โš™๏ธ ุฅุนุฏุงุฏุงุช", b"admin:settings_menu")],
1188
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"menu:main")],
1189
+ ]
1190
+ await event.edit("๐Ÿ›  **ู„ูˆุญุฉ ุงู„ู…ุทูˆุฑูŠู†**", buttons=rows)
1191
 
 
1192
 
1193
+ async def show_management_menu(event):
1194
+ rows = [
1195
+ [Button.inline("โž• ุฅุถุงูุฉ ุฃุฏู…ู†", b"admin:add")],
1196
+ [Button.inline("โž– ุฅุฒุงู„ุฉ ุฃุฏู…ู†", b"admin:list")],
1197
+ [Button.inline("๐Ÿ‘ฅ ู‚ุงุฆู…ุฉ ุงู„ุฃุฏู…ู†ูŠุฉ", b"admin:list")],
1198
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:panel")],
1199
+ ]
1200
+ await event.edit("๐Ÿ‘‘ **ุฅุฏุงุฑุฉ ุงู„ุฃุฏู…ู†ูŠุฉ**", buttons=rows)
1201
+
1202
+
1203
+ async def show_admin_list(event):
1204
+ admins = await col_admins.find().to_list(length=200)
1205
+ rows = []
1206
+ rows.append([Button.inline(f"๐Ÿ‘‘ Owner ({OWNER_ID})", b"noop")])
1207
+ for a in admins:
1208
+ rows.append([
1209
+ Button.inline(f"โš™๏ธ {a['user_id']}", f"admin:perms:{a['user_id']}".encode()),
1210
+ Button.inline("โž–", f"admin:remove:{a['user_id']}".encode()),
1211
+ ])
1212
+ rows.append([Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:management")])
1213
+ await event.edit("๐Ÿ‘ฅ **ู‚ุงุฆู…ุฉ ุงู„ุฃุฏู…ู†ูŠุฉ**\n(ุงุถุบุท โš™๏ธ ู„ุนุฑุถ/ุชุนุฏูŠู„ ุงู„ุตู„ุงุญูŠุงุช)", buttons=rows)
1214
+
1215
+
1216
+ async def show_admin_perms(event, target_id):
1217
+ doc = await col_admins.find_one({"user_id": target_id})
1218
+ if not doc:
1219
+ await event.answer("ุงู„ุฃุฏู…ู† ุบูŠุฑ ู…ูˆุฌูˆุฏ.", alert=True)
1220
+ return
1221
+ perms = doc.get("permissions", {})
1222
+ rows = []
1223
+ for p in ALL_PERMISSIONS:
1224
+ state = "โœ…" if perms.get(p) else "โŒ"
1225
+ rows.append([Button.inline(f"{state} {p}", f"admin:permtoggle:{target_id}:{p}".encode())])
1226
+ rows.append([Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:list")])
1227
+ await event.edit(f"๐Ÿ” ุตู„ุงุญูŠุงุช ุงู„ุฃุฏู…ู† {target_id}:", buttons=rows)
1228
+
1229
+
1230
+ async def drive_admin_add_flow(event, st):
1231
+ user_id = event.sender_id
1232
+ text = (event.raw_text or "").strip()
1233
+ if not text.isdigit():
1234
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ User ID ุตุญูŠุญ (ุฃุฑู‚ุงู… ูู‚ุท).")
1235
+ return
1236
+ target_id = int(text)
1237
+ if target_id == OWNER_ID:
1238
+ await event.respond("ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… ู‡ูˆ ุงู„ู€Owner ุจุงู„ูุนู„.")
1239
+ clear_state(user_id)
1240
+ return
1241
+ existing = await col_admins.find_one({"user_id": target_id})
1242
+ if existing:
1243
+ await event.respond("ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… ุฃุฏู…ู† ุจุงู„ูุนู„.")
1244
+ clear_state(user_id)
1245
+ return
1246
+ await col_admins.insert_one({
1247
+ "user_id": target_id,
1248
+ "added_by": user_id,
1249
+ "added_at": datetime.datetime.utcnow(),
1250
+ "permissions": {p: False for p in ALL_PERMISSIONS},
1251
+ })
1252
+ await log_action(user_id, "add_admin", {"target": target_id})
1253
+ clear_state(user_id)
1254
+ await event.respond(f"โœ… ุชู…ุช ุฅุถุงูุฉ {target_id} ูƒุฃุฏู…ู†. ูŠู…ูƒู†ูƒ ุงู„ุขู† ุถุจุท ุตู„ุงุญูŠุงุชู‡ ู…ู† ู‚ุงุฆู…ุฉ ุงู„ุฃุฏู…ู†ูŠุฉ.")
1255
+
1256
+
1257
+ async def show_publishing_menu(event):
1258
+ settings = await get_settings()
1259
+ ch = settings.get("channel_username") or settings.get("channel_id") or "ุบูŠุฑ ู…ุญุฏุฏ"
1260
+ rg = settings.get("review_group_id") or "ุบูŠุฑ ู…ุญุฏุฏ"
1261
+ rows = [
1262
+ [Button.inline("๐Ÿ“ข ุชุญุฏูŠุฏ ู‚ู†ุงุฉ ุงู„ู†ุดุฑ", b"admin:setchannel")],
1263
+ [Button.inline("๐Ÿ“ฅ ุชุญุฏูŠุฏ ู…ุฌู…ูˆุนุฉ ุงู„ู…ุฑุงุฌุนุฉ", b"admin:setreviewgroup")],
1264
+ [Button.inline("๐Ÿ“ ูƒู„ูŠุดุฉ ุงู„ุณุงุฑู‚", b"template:edit:thief")],
1265
+ [Button.inline("๐Ÿ“ ูƒู„ูŠุดุฉ ุงู„ู…ู†ุชุญู„", b"template:edit:impersonator")],
1266
+ [Button.inline("๐Ÿ‘ ู…ุนุงูŠู†ุฉ ุงู„ูƒู„ูŠุดุงุช", b"template:preview:thief")],
1267
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:panel")],
1268
+ ]
1269
+ await event.edit(f"๐Ÿ“ข **ุงู„ู†ุดุฑ**\nู‚ู†ุงุฉ ุงู„ู†ุดุฑ ุงู„ุญุงู„ูŠุฉ: {ch}\nู…ุฌู…ูˆุนุฉ ุงู„ู…ุฑุงุฌุนุฉ ุงู„ุญุงู„ูŠุฉ: {rg}", buttons=rows)
1270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
 
1272
+ async def drive_channel_set_flow(event, st):
1273
+ user_id = event.sender_id
1274
+ if not await has_permission(user_id, "manage_settings") and not is_owner(user_id):
1275
+ await event.respond("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ.")
1276
+ clear_state(user_id)
1277
+ return
1278
+ text = (event.raw_text or "").strip()
1279
+ entity = None
1280
+ try:
1281
+ if text.lstrip("-").isdigit():
1282
+ entity = await client.get_entity(int(text))
1283
+ else:
1284
+ entity = await client.get_entity(text.lstrip("@"))
1285
+ except RPCError as e:
1286
+ await event.respond(f"ุชุนุฐุฑ ุงู„ูˆุตูˆู„ ู„ู‡ุฐู‡ ุงู„ู‚ู†ุงุฉ: {e}")
1287
+ clear_state(user_id)
1288
+ return
1289
+ if not isinstance(entity, Channel):
1290
+ await event.respond("ู‡ุฐุง ู„ูŠุณ ู…ุนุฑู ู‚ู†ุงุฉ ุตุงู„ุญ.")
1291
+ clear_state(user_id)
1292
+ return
1293
+ await update_settings({
1294
+ "channel_id": entity.id if entity.id > 0 else int(f"-100{entity.id}"),
1295
+ "channel_username": getattr(entity, "username", None),
1296
+ })
1297
+ clear_state(user_id)
1298
+ await event.respond("โœ… ุชู… ุชุญุฏูŠุฏ ู‚ู†ุงุฉ ุงู„ู†ุดุฑ ุจู†ุฌุงุญ.")
1299
+
1300
+
1301
+ async def drive_review_group_set_flow(event, st):
1302
+ user_id = event.sender_id
1303
+ if not await has_permission(user_id, "manage_settings") and not is_owner(user_id):
1304
+ await event.respond("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ.")
1305
+ clear_state(user_id)
1306
+ return
1307
+ text = (event.raw_text or "").strip()
1308
+ try:
1309
+ if text.lstrip("-").isdigit():
1310
+ entity = await client.get_entity(int(text))
1311
+ else:
1312
+ entity = await client.get_entity(text.lstrip("@"))
1313
+ except RPCError as e:
1314
+ await event.respond(f"ุชุนุฐุฑ ุงู„ูˆุตูˆู„ ู„ู‡ุฐู‡ ุงู„ู…ุฌู…ูˆุนุฉ: {e}")
1315
+ clear_state(user_id)
1316
+ return
1317
+ group_id = entity.id
1318
+ if isinstance(entity, Channel):
1319
+ group_id = int(f"-100{entity.id}")
1320
+ elif isinstance(entity, Chat):
1321
+ group_id = -entity.id
1322
+ await update_settings({"review_group_id": group_id})
1323
+ clear_state(user_id)
1324
+ await event.respond("โœ… ุชู… ุชุญุฏูŠุฏ ู…ุฌู…ูˆุนุฉ ุงู„ู…ุฑุงุฌุนุฉ ุจู†ุฌุงุญ.")
1325
+
1326
+
1327
+ async def show_templates_menu(event):
1328
+ rows = [
1329
+ [Button.inline("๐Ÿ“ ูƒู„ูŠุดุฉ ุงู„ุณุงุฑู‚", b"template:edit:thief")],
1330
+ [Button.inline("๐Ÿ“ ูƒู„ูŠุดุฉ ุงู„ู…ู†ุชุญู„", b"template:edit:impersonator")],
1331
+ [Button.inline("๐Ÿ‘ ู…ุนุงูŠู†ุฉ ูƒู„ูŠุดุฉ ุงู„ุณุงุฑู‚", b"template:preview:thief")],
1332
+ [Button.inline("๐Ÿ‘ ู…ุนุงูŠู†ุฉ ูƒู„ูŠุดุฉ ุงู„ู…ู†ุชุญู„", b"template:preview:impersonator")],
1333
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:publishing")],
1334
+ ]
1335
+ await event.edit("๐Ÿ“ **ุงู„ูƒู„ูŠุดุงุช**", buttons=rows)
1336
 
1337
+
1338
+ async def handle_template_callback(event, parts):
1339
+ user_id = event.sender_id
1340
+ action = parts[1]
1341
+ ttype = parts[2]
1342
+ if action == "edit":
1343
+ if not await has_permission(user_id, "edit_templates") and not is_owner(user_id):
1344
+ await event.answer("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ ุชุนุฏูŠู„ ุงู„ูƒู„ูŠุดุงุช.", alert=True)
1345
+ return
1346
+ set_state(user_id, "template_edit", "await_text", {"ttype": ttype})
1347
+ await event.edit(
1348
+ "ุฃุฑุณู„ ู†ุต ุงู„ูƒู„ูŠุดุฉ ุงู„ุฌุฏูŠุฏ. ุงู„ู…ุชุบูŠุฑุงุช ุงู„ู…ุชุงุญุฉ:\n"
1349
+ "{case_id} {username} {user_id} {date} {description} {type}",
1350
+ buttons=[back_cancel_row(b"admin:publishing")],
1351
  )
1352
+ elif action == "preview":
1353
+ settings = await get_settings()
1354
+ template = settings.get(f"template_{ttype}") or (
1355
+ DEFAULT_TEMPLATE_THIEF if ttype == "thief" else DEFAULT_TEMPLATE_IMPERSONATOR
1356
+ )
1357
+ fake_case = {
1358
+ "case_id": "MF-000000", "username": "example", "user_id": 123456789,
1359
+ "created_at": datetime.datetime.utcnow(), "description": "ู…ุซุงู„ ุชูˆุถูŠุญูŠ ู„ู„ู…ุนุงูŠู†ุฉ.",
1360
+ "type": ttype,
1361
+ }
1362
+ rendered = render_template(template, fake_case)
1363
+ await event.edit(f"๐Ÿ‘ ู…ุนุงูŠู†ุฉ:\n\n{rendered}", buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:publishing")]])
1364
 
1365
+
1366
+ async def drive_template_edit_flow(event, st):
1367
+ user_id = event.sender_id
1368
+ ttype = st["data"]["ttype"]
1369
+ text = event.raw_text or ""
1370
+ if not text.strip():
1371
+ await event.respond("ุงู„ุฑุฌุงุก ุฅุฑุณุงู„ ู†ุต ุตุงู„ุญ ู„ู„ูƒู„ูŠุดุฉ.")
1372
+ return
1373
+ await update_settings({f"template_{ttype}": text})
1374
+ clear_state(user_id)
1375
+ await log_action(user_id, "edit_template", {"ttype": ttype})
1376
+ await event.respond("โœ… ุชู… ุญูุธ ุงู„ูƒู„ูŠุดุฉ ุจู†ุฌุงุญ.")
1377
+
1378
+
1379
+ async def show_stats(event):
1380
+ total = await col_cases.count_documents({})
1381
+ thief = await col_cases.count_documents({"type": "thief"})
1382
+ imp = await col_cases.count_documents({"type": "impersonator"})
1383
+ pending = await col_cases.count_documents({"status": "pending"})
1384
+ approved = await col_cases.count_documents({"status": "approved"})
1385
+ rejected = await col_cases.count_documents({"status": "rejected"})
1386
+ users_count = await col_users.count_documents({})
1387
+ text = (
1388
+ "๐Ÿ“Š **ุงู„ุฅุญุตุงุฆูŠุงุช**\n\n"
1389
+ f"ุฅุฌู…ุงู„ูŠ ุงู„ุจู„ุงุบุงุช: {total}\n"
1390
+ f"ุจู„ุงุบุงุช ุงู„ุณุฑู‚ุฉ: {thief}\n"
1391
+ f"ุจู„ุงุบุงุช ุงู„ุงู†ุชุญุงู„: {imp}\n"
1392
+ f"ุงู„ู…ุนู„ู‚ุฉ: {pending}\n"
1393
+ f"ุงู„ู…ู‚ุจูˆู„ุฉ: {approved}\n"
1394
+ f"ุงู„ู…ุฑููˆุถุฉ: {rejected}\n"
1395
+ f"ุนุฏุฏ ุงู„ู…ุณุชุฎุฏู…ูŠู†: {users_count}"
1396
+ )
1397
+ await event.edit(text, buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:panel")]])
1398
+
1399
+
1400
+ async def show_reports_menu(event):
1401
+ rows = [
1402
+ [Button.inline("ุงู„ุจู„ุงุบุงุช ุงู„ู…ุนู„ู‚ุฉ", b"cases:list:pending")],
1403
+ [Button.inline("ุงู„ุจู„ุงุบุงุช ุงู„ู…ู‚ุจูˆู„ุฉ", b"cases:list:approved")],
1404
+ [Button.inline("ุงู„ุจู„ุงุบุงุช ุงู„ู…ุฑููˆุถุฉ", b"cases:list:rejected")],
1405
+ [Button.inline("ุงู„ุจุญุซ ุนู† Case ID", b"cases:search")],
1406
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:panel")],
1407
+ ]
1408
+ await event.edit("๐Ÿ“‹ **ุฅุฏุงุฑุฉ ุงู„ุจู„ุงุบุงุช**", buttons=rows)
1409
+
1410
+
1411
+ async def handle_cases_callback(event, parts):
1412
+ user_id = event.sender_id
1413
+ if not await is_admin(user_id):
1414
+ await event.answer("๐Ÿšซ ู„ู„ุฅุฏุงุฑุฉ ูู‚ุท.", alert=True)
1415
+ return
1416
+ action = parts[1]
1417
+ if action == "list":
1418
+ status = parts[2]
1419
+ cases = await col_cases.find({"status": status}).sort("created_at", DESCENDING).to_list(length=15)
1420
+ if not cases:
1421
+ await event.edit("ู„ุง ุชูˆุฌุฏ ุจู„ุงุบุงุช ููŠ ู‡ุฐู‡ ุงู„ูุฆุฉ.", buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:reports_menu")]])
1422
  return
1423
+ lines = [f"{c['case_id']} โ€” {TYPE_LABEL.get(c['type'], c['type'])} โ€” {c.get('username') or c.get('user_id')}" for c in cases]
1424
+ text = f"๐Ÿ“‹ ุขุฎุฑ {len(cases)} ุจู„ุงุบ ({status}):\n\n" + "\n".join(lines)
1425
+ await event.edit(text, buttons=[[Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:reports_menu")]])
1426
+ elif action == "search":
1427
+ set_state(user_id, "case_search", "await_id")
1428
+ await event.edit("ุฃุฑุณู„ ุฑู‚ู… Case ID (ู…ุซุงู„ MF-000001).", buttons=[back_cancel_row(b"admin:reports_menu")])
1429
+
1430
+
1431
+ async def drive_case_search_flow(event, st):
1432
+ user_id = event.sender_id
1433
+ case_id = (event.raw_text or "").strip().upper()
1434
+ case_doc = await col_cases.find_one({"case_id": case_id})
1435
+ clear_state(user_id)
1436
+ if not case_doc:
1437
+ await event.respond("ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุจู„ุงุบ ุจู‡ุฐุง ุงู„ุฑู‚ู….")
1438
+ return
1439
+ text = (
1440
+ f"Case ID: {case_doc['case_id']}\n"
1441
+ f"ุงู„ู†ูˆุน: {TYPE_LABEL.get(case_doc['type'], case_doc['type'])}\n"
1442
+ f"Username: @{case_doc.get('username')}\n" if case_doc.get("username") else "Username: ุบูŠุฑ ู…ุชูˆูุฑ\n"
1443
+ )
1444
+ text += (
1445
+ f"User ID: {case_doc.get('user_id') or 'ุบูŠุฑ ู…ุชูˆูุฑ'}\n"
1446
+ f"ุง๏ฟฝ๏ฟฝุญุงู„ุฉ: {case_doc['status']}\n"
1447
+ f"ู…ู‚ุฏู… ุงู„ุจู„ุงุบ: {case_doc.get('submitter_id')}\n\n"
1448
+ f"ุงู„ู…ูˆุถูˆุน:\n{case_doc.get('description','')}"
1449
+ )
1450
+ await event.respond(text)
1451
+
1452
+
1453
+ async def show_settings_menu(event):
1454
+ settings = await get_settings()
1455
+ reports_state = "โœ… ู…ูุนู‘ู„" if settings.get("reports_enabled", True) else "โŒ ู…ุชูˆู‚ู"
1456
+ search_state = "โœ… ู…ูุนู‘ู„" if settings.get("search_enabled", True) else "โŒ ู…ุชูˆู‚ู"
1457
+ rows = [
1458
+ [Button.inline(f"ุชุดุบูŠู„/ุฅูŠู‚ุงู ุงู„ุจู„ุงุบุงุช ({reports_state})", b"settings:toggle_reports")],
1459
+ [Button.inline(f"ุชุดุบูŠู„/ุฅูŠู‚ุงู ุงู„ุจุญุซ ({search_state})", b"settings:toggle_search")],
1460
+ [Button.inline("ุฑุณุงู„ุฉ ุงู„ุตูŠุงู†ุฉ", b"settings:maintenance")],
1461
+ [Button.inline("๐Ÿ”™ ุฑุฌูˆุน", b"admin:panel")],
1462
+ ]
1463
+ await event.edit("โš™๏ธ **ุงู„ุฅุนุฏุงุฏุงุช**", buttons=rows)
1464
 
 
 
 
 
 
 
 
1465
 
1466
+ async def handle_settings_callback(event, parts):
1467
+ user_id = event.sender_id
1468
+ if not await has_permission(user_id, "manage_settings"):
1469
+ await event.answer("๐Ÿšซ ู„ูŠุณ ู„ุฏูŠูƒ ุตู„ุงุญูŠุฉ ุชุนุฏูŠู„ ุงู„ุฅุนุฏุงุฏุงุช.", alert=True)
1470
+ return
1471
+ action = parts[1]
1472
+ settings = await get_settings()
1473
+ if action == "toggle_reports":
1474
+ await update_settings({"reports_enabled": not settings.get("reports_enabled", True)})
1475
+ await log_action(user_id, "toggle_reports")
1476
+ await show_settings_menu(event)
1477
+ elif action == "toggle_search":
1478
+ await update_settings({"search_enabled": not settings.get("search_enabled", True)})
1479
+ await log_action(user_id, "toggle_search")
1480
+ await show_settings_menu(event)
1481
+ elif action == "maintenance":
1482
+ set_state(user_id, "maintenance_msg", "await_text")
1483
+ await event.edit("ุฃุฑุณู„ ู†ุต ุฑุณุงู„ุฉ ุงู„ุตูŠุงู†ุฉ ุงู„ุฌุฏูŠุฏ.", buttons=[back_cancel_row(b"admin:settings_menu")])
1484
+
1485
+
1486
+ async def drive_maintenance_msg_flow(event, st):
1487
+ user_id = event.sender_id
1488
+ text = event.raw_text or ""
1489
+ await update_settings({"maintenance_message": text})
1490
+ clear_state(user_id)
1491
+ await log_action(user_id, "set_maintenance_message")
1492
+ await event.respond("โœ… ุชู… ุญูุธ ุฑุณุงู„ุฉ ุงู„ุตูŠุงู†ุฉ.")
1493
+
1494
+
1495
+ @client.on(events.CallbackQuery(pattern=b"^noop$"))
1496
+ async def noop_handler(event):
1497
+ await event.answer()
1498
+
1499
+
1500
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1501
+ # DB indexes
1502
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1503
+ async def ensure_indexes():
1504
+ await col_cases.create_index([("user_id", ASCENDING)])
1505
+ await col_cases.create_index([("username", ASCENDING)])
1506
+ await col_cases.create_index([("case_id", ASCENDING)], unique=True)
1507
+ await col_cases.create_index([("status", ASCENDING)])
1508
+ await col_cases.create_index([("type", ASCENDING)])
1509
+ await col_cases.create_index([("channel_message_id", ASCENDING)])
1510
+ await col_admins.create_index([("user_id", ASCENDING)], unique=True)
1511
+ await col_users.create_index([("user_id", ASCENDING)], unique=True)
1512
+ await col_logs.create_index([("timestamp", DESCENDING)])
1513
+
1514
+
1515
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1516
+ # Startup checks
1517
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1518
+ async def check_channel_permissions():
1519
+ settings = await get_settings()
1520
+ channel_id = settings.get("channel_id")
1521
+ if not channel_id:
1522
+ log.warning("Publishing channel not configured yet. Set it from the admin panel.")
1523
+ return
1524
+ try:
1525
+ perms = await client.get_permissions(channel_id, "me")
1526
+ if not perms.post_messages and not perms.is_admin:
1527
+ log.warning("Bot may lack post-message permission in the publishing channel.")
1528
+ except RPCError as e:
1529
+ log.warning("Could not verify channel permissions: %s", e)
1530
+
1531
+ review_group_id = settings.get("review_group_id")
1532
+ if not review_group_id:
1533
+ log.warning("Review group not configured yet. Set it from the admin panel.")
1534
 
1535
 
 
 
 
1536
  async def main():
1537
+ log.info("Connecting to MongoDB...")
1538
+ try:
1539
+ await mongo_client.admin.command("ping")
1540
+ except PyMongoError as e:
1541
+ log.error("Could not connect to MongoDB: %s", e)
1542
+ sys.exit(1)
1543
+ log.info("MongoDB connection OK.")
1544
 
1545
+ await ensure_indexes()
1546
+ await get_settings()
 
 
 
 
 
1547
 
1548
+ log.info("Starting Telegram client (bot mode)...")
1549
+ await client.start(bot_token=BOT_TOKEN)
1550
+ me = await client.get_me()
1551
+ log.info("Bot connected as @%s (id=%s)", me.username, me.id)
1552
+
1553
+ await check_channel_permissions()
 
1554
 
1555
+ log.info("MF BLACKLIST bot is up and running.")
1556
+ await client.run_until_disconnected()
1557
 
1558
 
1559
  if __name__ == "__main__":
1560
+ try:
1561
+ asyncio.run(main())
1562
+ except KeyboardInterrupt:
1563
+ log.info("Shutting down.")
requirements.txt CHANGED
@@ -1,8 +1,3 @@
1
  telethon==1.36.0
 
2
  pymongo==4.8.0
3
- torch==2.3.1
4
- open-clip-torch==2.24.0
5
- pytesseract==0.3.13
6
- pillow==10.4.0
7
- numpy==1.26.4
8
- python-dotenv==1.0.1
 
1
  telethon==1.36.0
2
+ motor==3.5.1
3
  pymongo==4.8.0