Rubka 2.11.13__py3-none-any.whl → 7.1.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. rubka/__init__.py +72 -3
  2. rubka/adaptorrubka/__init__.py +4 -0
  3. rubka/adaptorrubka/client/__init__.py +1 -0
  4. rubka/adaptorrubka/client/client.py +60 -0
  5. rubka/adaptorrubka/crypto/__init__.py +1 -0
  6. rubka/adaptorrubka/crypto/crypto.py +82 -0
  7. rubka/adaptorrubka/enums.py +36 -0
  8. rubka/adaptorrubka/exceptions.py +22 -0
  9. rubka/adaptorrubka/methods/__init__.py +1 -0
  10. rubka/adaptorrubka/methods/methods.py +90 -0
  11. rubka/adaptorrubka/network/__init__.py +3 -0
  12. rubka/adaptorrubka/network/helper.py +22 -0
  13. rubka/adaptorrubka/network/network.py +221 -0
  14. rubka/adaptorrubka/network/socket.py +31 -0
  15. rubka/adaptorrubka/sessions/__init__.py +1 -0
  16. rubka/adaptorrubka/sessions/sessions.py +72 -0
  17. rubka/adaptorrubka/types/__init__.py +1 -0
  18. rubka/adaptorrubka/types/socket/__init__.py +1 -0
  19. rubka/adaptorrubka/types/socket/message.py +187 -0
  20. rubka/adaptorrubka/utils/__init__.py +2 -0
  21. rubka/adaptorrubka/utils/configs.py +18 -0
  22. rubka/adaptorrubka/utils/utils.py +251 -0
  23. rubka/api.py +1450 -95
  24. rubka/asynco.py +2515 -0
  25. rubka/button.py +404 -0
  26. rubka/context.py +744 -34
  27. rubka/exceptions.py +35 -1
  28. rubka/filters.py +321 -0
  29. rubka/helpers.py +1461 -0
  30. rubka/keypad.py +255 -5
  31. rubka/metadata.py +117 -0
  32. rubka/rubino.py +1231 -0
  33. rubka/tv.py +145 -0
  34. rubka/update.py +1038 -0
  35. rubka-7.1.17.dist-info/METADATA +1048 -0
  36. rubka-7.1.17.dist-info/RECORD +45 -0
  37. rubka-7.1.17.dist-info/entry_points.txt +2 -0
  38. rubka-2.11.13.dist-info/METADATA +0 -315
  39. rubka-2.11.13.dist-info/RECORD +0 -15
  40. {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/WHEEL +0 -0
  41. {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/top_level.txt +0 -0
rubka/asynco.py ADDED
@@ -0,0 +1,2515 @@
1
+ import asyncio,aiohttp,aiofiles,time,datetime,json,tempfile,os,sys,subprocess,mimetypes,time, hashlib,sqlite3,re
2
+ from typing import List, Optional, Dict, Any, Literal, Callable, Union,Set
3
+ from collections import OrderedDict
4
+ from .exceptions import APIRequestError,raise_for_status,InvalidAccessError,InvalidInputError,TooRequestError,InvalidTokenError
5
+ from .adaptorrubka import Client as Client_get
6
+ from .logger import logger
7
+ from .metadata import Track_parsed as GlyphWeaver
8
+ from .rubino import Bot as Rubino
9
+ from . import filters
10
+ try:from .context import Message, InlineMessage
11
+ except (ImportError, ModuleNotFoundError):from context import Message, InlineMessage
12
+ try:from .button import ChatKeypadBuilder, InlineBuilder
13
+ except (ImportError, ModuleNotFoundError):from button import ChatKeypadBuilder, InlineBuilder
14
+ class FeatureNotAvailableError(Exception):
15
+ pass
16
+
17
+ from tqdm.asyncio import tqdm
18
+ from urllib.parse import urlparse, parse_qs
19
+
20
+ from pathlib import Path
21
+ from tqdm import tqdm
22
+ API_URL = "https://botapi.rubika.ir/v3"
23
+
24
+ def install_package(package_name: str) -> bool:
25
+ try:
26
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
27
+ return True
28
+ except Exception:
29
+ return False
30
+
31
+ def metadata_CallBack(text: str, mode: str) -> Dict[str, Any]:
32
+ meta_data_parts = []
33
+ if mode == "Markdown":
34
+ for match in re.finditer(r"\*\*(.*?)\*\*", text):
35
+ meta_data_parts.append({
36
+ "type": "Bold",
37
+ "from_index": match.start(1),
38
+ "length": len(match.group(1))
39
+ })
40
+ for match in re.finditer(r"(?<!\*)\*(?!\*)(.*?)\*(?!\*)", text):
41
+ meta_data_parts.append({
42
+ "type": "Italic",
43
+ "from_index": match.start(1),
44
+ "length": len(match.group(1))
45
+ })
46
+ for match in re.finditer(r"~~(.*?)~~", text):
47
+ meta_data_parts.append({
48
+ "type": "Strike",
49
+ "from_index": match.start(1),
50
+ "length": len(match.group(1))
51
+ })
52
+ for match in re.finditer(r"__(.*?)__", text):
53
+ meta_data_parts.append({
54
+ "type": "Underline",
55
+ "from_index": match.start(1),
56
+ "length": len(match.group(1))
57
+ })
58
+ for match in re.finditer(r"^> (.*)$", text, re.MULTILINE):
59
+ meta_data_parts.append({
60
+ "type": "Quote",
61
+ "from_index": match.start(1),
62
+ "length": len(match.group(1))
63
+ })
64
+ for match in re.finditer(r"\|\|(.*?)\|\|", text):
65
+ meta_data_parts.append({
66
+ "type": "Spoiler",
67
+ "from_index": match.start(1),
68
+ "length": len(match.group(1))
69
+ })
70
+ for match in re.finditer(r"`(.*?)`", text):
71
+ meta_data_parts.append({
72
+ "type": "Mono",
73
+ "from_index": match.start(1),
74
+ "length": len(match.group(1))
75
+ })
76
+
77
+ for match in re.finditer(r"```(.*?)```", text, re.DOTALL):
78
+ meta_data_parts.append({
79
+ "type": "Pre",
80
+ "from_index": match.start(1),
81
+ "length": len(match.group(1))
82
+ })
83
+ for match in re.finditer(r"\[(.*?)\]\((.*?)\)", text):
84
+ meta_data_parts.append({
85
+ "type": "Link",
86
+ "from_index": match.start(1),
87
+ "length": len(match.group(1)),
88
+ "link_url": match.group(2)
89
+ })
90
+
91
+ elif mode == "HTML":
92
+ for match in re.finditer(r"<b>(.*?)</b>", text):
93
+ meta_data_parts.append({
94
+ "type": "Bold",
95
+ "from_index": match.start(1),
96
+ "length": len(match.group(1))
97
+ })
98
+ for match in re.finditer(r"<i>(.*?)</i>", text):
99
+ meta_data_parts.append({
100
+ "type": "Italic",
101
+ "from_index": match.start(1),
102
+ "length": len(match.group(1))
103
+ })
104
+ for match in re.finditer(r"<u>(.*?)</u>", text):
105
+ meta_data_parts.append({
106
+ "type": "Underline",
107
+ "from_index": match.start(1),
108
+ "length": len(match.group(1))
109
+ })
110
+ for match in re.finditer(r"<s>(.*?)</s>|<strike>(.*?)</strike>", text):
111
+ inner = match.group(1) or match.group(2)
112
+ meta_data_parts.append({
113
+ "type": "Strike",
114
+ "from_index": match.start(),
115
+ "length": len(inner)
116
+ })
117
+ for match in re.finditer(r"<blockquote>(.*?)</blockquote>", text, re.DOTALL):
118
+ meta_data_parts.append({
119
+ "type": "Quote",
120
+ "from_index": match.start(1),
121
+ "length": len(match.group(1))
122
+ })
123
+ for match in re.finditer(r'<span class="spoiler">(.*?)</span>', text):
124
+ meta_data_parts.append({
125
+ "type": "Spoiler",
126
+ "from_index": match.start(1),
127
+ "length": len(match.group(1))
128
+ })
129
+ for match in re.finditer(r"<code>(.*?)</code>", text):
130
+ meta_data_parts.append({
131
+ "type": "Pre",
132
+ "from_index": match.start(1),
133
+ "length": len(match.group(1))
134
+ })
135
+
136
+ for match in re.finditer(r"<pre>(.*?)</pre>", text, re.DOTALL):
137
+ meta_data_parts.append({
138
+ "type": "Pre",
139
+ "from_index": match.start(1),
140
+ "length": len(match.group(1))
141
+ })
142
+ for match in re.finditer(r'<a href="(.*?)">(.*?)</a>', text):
143
+ meta_data_parts.append({
144
+ "type": "Link",
145
+ "from_index": match.start(2),
146
+ "length": len(match.group(2)),
147
+ "link_url": match.group(1)
148
+ })
149
+
150
+ return {"meta_data_parts": meta_data_parts} if meta_data_parts else {}
151
+
152
+ def get_importlib_metadata():
153
+ try:
154
+ from importlib.metadata import version, PackageNotFoundError
155
+ return version, PackageNotFoundError
156
+ except ImportError:
157
+ if install_package("importlib-metadata"):
158
+ try:
159
+ from importlib_metadata import version, PackageNotFoundError
160
+ return version, PackageNotFoundError
161
+ except ImportError:
162
+ return None, None
163
+ return None, None
164
+
165
+ version, PackageNotFoundError = get_importlib_metadata()
166
+
167
+ def get_installed_version(package_name: str) -> Optional[str]:
168
+ if version is None:
169
+ return "unknown"
170
+ try:
171
+ return version(package_name)
172
+ except PackageNotFoundError:
173
+ return None
174
+ BASE_URLS = {
175
+ "botapi": "https://botapi.rubika.ir/v3",
176
+ "messenger": "https://messengerg2b1.iranlms.ir/v3"
177
+ }
178
+ async def get_latest_version(package_name: str) -> Optional[str]:
179
+ url = f"https://pypi.org/pypi/{package_name}/json"
180
+ try:
181
+ async with aiohttp.ClientSession() as session:
182
+ async with session.get(url, timeout=5) as resp:
183
+ resp.raise_for_status()
184
+ data = await resp.json()
185
+ return data.get("info", {}).get("version")
186
+ except Exception:
187
+ return None
188
+
189
+ async def check_rubka_version():
190
+ package_name = "rubka"
191
+ installed_version = get_installed_version(package_name)
192
+ if installed_version is None:
193
+ return
194
+
195
+ latest_version = await get_latest_version(package_name)
196
+ if latest_version is None:
197
+ return
198
+
199
+ if installed_version != latest_version:
200
+ print(f"CRITICAL WARNING: Your installed version of '{package_name}' is outdated.")
201
+ print("This poses a serious risk to stability, security, and compatibility with current features.")
202
+ print(f"- Installed version : {installed_version}")
203
+ print(f"- Latest version : {latest_version}")
204
+ print("\nImmediate action is required.")
205
+ print(f"Run the following command to update safely:")
206
+ print(f"\npip install {package_name}=={latest_version}\n")
207
+ print("Delaying this update may result in unexpected crashes, data loss, or broken functionality.")
208
+ print("Stay up-to-date to ensure full support and access to the latest improvements.")
209
+ print("For new methods and updates, visit: @rubka_library\n")
210
+
211
+
212
+
213
+ def show_last_six_words(text: str) -> str:
214
+ text = text.strip()
215
+ return text[-6:]
216
+ class AttrDict(dict):
217
+ def __getattr__(self, item):
218
+ value = self.get(item)
219
+ if isinstance(value, dict):
220
+ return AttrDict(value)
221
+ return value
222
+
223
+ class Robot:
224
+ """
225
+ Main asynchronous class to interact with the Rubika Bot API.
226
+
227
+ This class handles sending and receiving messages, inline queries, callbacks,
228
+ and manages sessions and API interactions. It is initialized with a bot token
229
+ and provides multiple optional parameters for configuration.
230
+
231
+ Attributes:
232
+ token (str): Bot token used for authentication with Rubika Bot API.
233
+ session_name (str | None): Optional session name for storing session data.
234
+ auth (str | None): Optional authentication string for advanced features related to account key.
235
+ Key (str | None): Optional account key for additional authorization if required.
236
+ platform (str): Platform type, default is 'web'.
237
+ web_hook (str | None): Optional webhook URL for receiving updates.
238
+ timeout (int): Timeout for API requests in seconds (default 10).
239
+ show_progress (bool): Whether to show progress for long operations (default False).
240
+ raise_errors (bool): Whether to raise exceptions on API errors (default True).
241
+ proxy (str | None): Optional proxy URL to route requests through.
242
+ retries (int): Number of times to retry a failed API request (default 2).
243
+ retry_delay (float): Delay between retries in seconds (default 0.5).
244
+ user_agent (str | None): Custom User-Agent header for requests.
245
+ safeSendMode (bool): If True, messages are sent safely. If reply fails using message_id, sends without message_id (default False).
246
+ max_cache_size (int): Maximum number of processed messages stored to prevent duplicates (default 1000).
247
+ max_msg_age (int): Maximum age of messages in seconds to consider for processing (default 20).
248
+
249
+ Example:
250
+ ```python
251
+ import asyncio
252
+ from rubka.asynco import Robot, filters, Message
253
+
254
+ bot = Robot(token="YOUR_BOT_TOKEN", safeSendMode=False, max_cache_size=1000)
255
+
256
+ @bot.on_message(filters.is_command.start)
257
+ async def start_command(bot: Robot, message: Message):
258
+ await message.reply("Hello!")
259
+
260
+ asyncio.run(bot.run())
261
+ ```
262
+ Notes:
263
+
264
+ token is mandatory, all other parameters are optional.
265
+
266
+ safeSendMode ensures reliable message sending even if replying by message_id fails.
267
+
268
+ max_cache_size and max_msg_age help manage duplicate message processing efficiently.
269
+ """
270
+
271
+
272
+ def __init__(self, token: str, session_name: str = None, auth: str = None, Key: str = None, platform: str = "web", web_hook: str = None, timeout: int = 10, show_progress: bool = False, raise_errors: bool = True,proxy: str = None,retries: int = 2,retry_delay: float = 0.5,user_agent: str = None,safeSendMode = False,max_cache_size: int = 2000,max_msg_age : int = 60,chunk_size : int = 64 * 1024,parse_mode: Optional[Literal["HTML", "Markdown"]] = "Markdown",api_endpoint: Optional[Literal["botapi", "messenger"]] = "botapi"):
273
+ self.token = token
274
+ self._inline_query_handlers: List[dict] = []
275
+ self.timeout = timeout
276
+ self.auth = auth
277
+ self.chunk_size = chunk_size
278
+ self.safeSendMode = safeSendMode
279
+ self.user_agent = user_agent
280
+ self.proxy = proxy
281
+ self.max_msg_age = max_msg_age
282
+ self.retries = retries
283
+
284
+ self.retry_delay = retry_delay
285
+ self.raise_errors = raise_errors
286
+ self.show_progress = show_progress
287
+ self.session_name = session_name
288
+ self.Key = Key
289
+ self.platform = platform
290
+ self.web_hook = web_hook
291
+ self.parse_mode = parse_mode
292
+ self._offset_id: Optional[str] = None
293
+ self._aiohttp_session: aiohttp.ClientSession = None
294
+ self.sessions: Dict[str, Dict[str, Any]] = {}
295
+ self._callback_handler = None
296
+ self._processed_message_ids = OrderedDict()
297
+ self._max_cache_size = max_cache_size
298
+ self._callback_handlers: List[dict] = []
299
+ self._edited_message_handlers = []
300
+ self._message_saver_enabled = False
301
+ self._max_messages = None
302
+ self._db_path = os.path.join(os.getcwd(), "RubkaSaveMessage.db")
303
+ self._ensure_db()
304
+ self._message_handlers: List[dict] = []
305
+ if api_endpoint not in BASE_URLS:raise ValueError(f"api_endpoint must be one of {list(BASE_URLS.keys())}")
306
+ self.api_endpoint = api_endpoint
307
+
308
+ logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
309
+ async def _get_session(self) -> aiohttp.ClientSession:
310
+ if self._aiohttp_session is None or self._aiohttp_session.closed:
311
+ connector = aiohttp.TCPConnector(limit=100, ssl=False)
312
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
313
+ self._aiohttp_session = aiohttp.ClientSession(connector=connector, timeout=timeout)
314
+ return self._aiohttp_session
315
+ async def close(self):
316
+ if self._aiohttp_session and not self._aiohttp_session.closed:
317
+ await self._aiohttp_session.close()
318
+ logger.debug("aiohttp session closed successfully.")
319
+
320
+ async def _initialize_webhook(self):
321
+ """Initializes and sets the webhook endpoint if provided."""
322
+ if not self.web_hook:
323
+ return
324
+
325
+ session = await self._get_session()
326
+ try:
327
+ async with session.get(self.web_hook, timeout=self.timeout) as response:
328
+ response.raise_for_status()
329
+ data = await response.json()
330
+ if data:print(f"[INFO] Retrieving WebHook URL information...")
331
+ json_url = data.get('url', self.web_hook)
332
+ for endpoint_type in [
333
+ "ReceiveUpdate",
334
+ "ReceiveInlineMessage",
335
+ "ReceiveQuery",
336
+ "GetSelectionItem",
337
+ "SearchSelectionItems"
338
+ ]:
339
+ result = await self.update_bot_endpoint(self.web_hook, endpoint_type)
340
+ if result['status'] =="OK":print(f"✔ Set endpoint type to '{endpoint_type}' — Operation succeeded with status: {result['status']}")
341
+ else:print(f"[ERROR] Failed to set endpoint type '{endpoint_type}': Status code {result['status']}")
342
+ self.web_hook = json_url
343
+ except Exception as e:
344
+ logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
345
+ self.web_hook = None
346
+ async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
347
+ base_url = BASE_URLS[self.api_endpoint]
348
+ url = f"{base_url}/{self.token}/{method}"
349
+ print(url)
350
+ session = await self._get_session()
351
+ for attempt in range(1, self.retries + 1):
352
+ try:
353
+ headers = {}
354
+ if self.user_agent:headers["User-Agent"] = self.user_agent
355
+ async with session.post(url, json=data, proxy=self.proxy,headers=headers) as response:
356
+ if response.status in (429, 500, 502, 503, 504):
357
+ logger.warning(f"[{method}] Got status {response.status}, retry {attempt}/{self.retries}...")
358
+ if attempt < self.retries:
359
+ await asyncio.sleep(self.retry_delay)
360
+ continue
361
+ response.raise_for_status()
362
+
363
+ response.raise_for_status()
364
+ try:
365
+ json_resp = await response.json(content_type=None)
366
+ except Exception:
367
+ text_resp = await response.text()
368
+ logger.error(f"[{method}] Invalid JSON response: {text_resp}")
369
+ raise APIRequestError(f"Invalid JSON response: {text_resp}")
370
+
371
+ status = json_resp.get("status")
372
+ if status in {"INVALID_ACCESS", "INVALID_INPUT", "TOO_REQUESTS"}:
373
+ if self.raise_errors:
374
+ raise_for_status(json_resp)
375
+ return AttrDict(json_resp)
376
+ return AttrDict({**json_resp, **data,"message_id":json_resp.get("data").get("message_id")})
377
+
378
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
379
+ logger.warning(f"[{method}] Attempt {attempt}/{self.retries} failed: {e}")
380
+ if attempt < self.retries:
381
+ await asyncio.sleep(self.retry_delay)
382
+ continue
383
+ logger.error(f"[{method}] API request failed after {self.retries} retries: {e}")
384
+ raise APIRequestError(f"API request failed: {e}") from e
385
+ def _make_dup_key(self, message_id: str, update_type: str, msg_data: dict) -> str:
386
+ raw = f"{message_id}:{update_type}:{msg_data.get('text','')}:{msg_data.get('author_guid','')}"
387
+ return hashlib.sha1(raw.encode()).hexdigest()
388
+ async def get_me(self) -> Dict[str, Any]:
389
+ return await self._post("getMe", {})
390
+ async def geteToken(self):
391
+ try:
392
+ if (await self.get_me())['status'] != "OK":
393
+ raise InvalidTokenError("The provided bot token is invalid or expired.")
394
+ except Exception as e:
395
+ print(e)
396
+ from typing import Callable, Any, Optional, List
397
+
398
+
399
+ #save message database __________________________
400
+
401
+ def _ensure_db(self):
402
+ conn = sqlite3.connect(self._db_path)
403
+ cur = conn.cursor()
404
+ cur.execute("""
405
+ CREATE TABLE IF NOT EXISTS messages (
406
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
407
+ chat_id TEXT NOT NULL,
408
+ message_id TEXT NOT NULL,
409
+ sender_id TEXT,
410
+ text TEXT,
411
+ raw_data TEXT,
412
+ time TEXT,
413
+ saved_at INTEGER
414
+ );
415
+ """)
416
+ cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_message ON messages(chat_id, message_id);")
417
+ conn.commit()
418
+ conn.close()
419
+
420
+ def _insert_message(self, record: dict):
421
+ conn = sqlite3.connect(self._db_path)
422
+ cur = conn.cursor()
423
+ cur.execute("""
424
+ INSERT OR IGNORE INTO messages
425
+ (chat_id, message_id, sender_id, text, raw_data, time, saved_at)
426
+ VALUES (?, ?, ?, ?, ?, ?, ?)
427
+ """, (
428
+ record.get("chat_id"),
429
+ record.get("message_id"),
430
+ record.get("sender_id"),
431
+ record.get("text"),
432
+ json.dumps(record.get("raw_data") or {}, ensure_ascii=False),
433
+ record.get("time"),
434
+ int(time.time())
435
+ ))
436
+ conn.commit()
437
+ if getattr(self, "_max_messages", None) is not None:
438
+ cur.execute("SELECT COUNT(*) FROM messages")
439
+ total = cur.fetchone()[0]
440
+ if total > self._max_messages:
441
+ remove_count = total - self._max_messages
442
+ cur.execute(
443
+ "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
444
+ (remove_count,)
445
+ )
446
+ conn.commit()
447
+
448
+ conn.close()
449
+
450
+ def _fetch_message(self, chat_id: str, message_id: str):
451
+ conn = sqlite3.connect(self._db_path)
452
+ cur = conn.cursor()
453
+ cur.execute(
454
+ "SELECT chat_id, message_id, sender_id, text, raw_data, time, saved_at FROM messages WHERE chat_id=? AND message_id=?",
455
+ (chat_id, message_id)
456
+ )
457
+ row = cur.fetchone()
458
+ conn.close()
459
+ if not row:
460
+ return None
461
+ chat_id, message_id, sender_id, text, raw_data_json, time_val, saved_at = row
462
+ try:
463
+ raw = json.loads(raw_data_json)
464
+ except:
465
+ raw = {}
466
+ return {
467
+ "chat_id": chat_id,
468
+ "message_id": message_id,
469
+ "sender_id": sender_id,
470
+ "text": text,
471
+ "raw_data": raw,
472
+ "time": time_val,
473
+ "saved_at": saved_at
474
+ }
475
+ async def save_message(self, message: Message):
476
+ try:
477
+ record = {
478
+ "chat_id": getattr(message, "chat_id", None),
479
+ "message_id": getattr(message, "message_id", None),
480
+ "sender_id": getattr(message, "author_guid", None),
481
+ "text": getattr(message, "text", None),
482
+ "raw_data": getattr(message, "raw_data", {}),
483
+ "time": getattr(message, "time", None),
484
+ }
485
+ await asyncio.to_thread(self._insert_message, record)
486
+ except Exception as e:
487
+ print(f"[DB] Error saving message: {e}")
488
+
489
+ async def get_message(self, chat_id: str, message_id: str):
490
+ return await asyncio.to_thread(self._fetch_message, chat_id, message_id)
491
+
492
+ def start_save_message(self, max_messages: int = 1000):
493
+ if self._message_saver_enabled:
494
+ return
495
+ self._message_saver_enabled = True
496
+ self._max_messages = max_messages
497
+ decorators = [
498
+ "on_message", "on_edited_message", "on_message_file", "on_message_forwarded",
499
+ "on_message_reply", "on_message_text", "on_update", "on_callback",
500
+ "on_callback_query", "callback_query_handler", "callback_query",
501
+ "on_inline_query", "on_inline_query_prefix", "on_message_private", "on_message_group"
502
+ ]
503
+
504
+ for decorator_name in decorators:
505
+ if hasattr(self, decorator_name):
506
+ original_decorator = getattr(self, decorator_name)
507
+
508
+ def make_wrapper(orig_decorator):
509
+ def wrapper(*args, **kwargs):
510
+ decorator = orig_decorator(*args, **kwargs)
511
+ def inner_wrapper(func):
512
+ async def inner(bot, message, *a, **kw):
513
+ try:
514
+ await bot.save_message(message)
515
+ if getattr(self, "_max_messages", None) is not None:
516
+ await asyncio.to_thread(self._prune_old_messages)
517
+ except Exception as e:
518
+ print(f"[DB] Save error: {e}")
519
+ return await func(bot, message, *a, **kw)
520
+ return decorator(inner)
521
+ return inner_wrapper
522
+ return wrapper
523
+
524
+ setattr(self, decorator_name, make_wrapper(original_decorator))
525
+ def _prune_old_messages(self):
526
+ if not hasattr(self, "_max_messages") or self._max_messages is None:
527
+ return
528
+ conn = sqlite3.connect(self._db_path)
529
+ cur = conn.cursor()
530
+ cur.execute("SELECT COUNT(*) FROM messages")
531
+ total = cur.fetchone()[0]
532
+ if total > self._max_messages:
533
+ remove_count = total - self._max_messages
534
+ cur.execute(
535
+ "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
536
+ (remove_count,)
537
+ )
538
+ conn.commit()
539
+ conn.close()
540
+
541
+ #save message database __________________________ end
542
+
543
+ #decorator#
544
+
545
+ def on_message_private(
546
+ self,
547
+ chat_id: Optional[Union[str, List[str]]] = None,
548
+ commands: Optional[List[str]] = None,
549
+ filters: Optional[Callable[[Message], bool]] = None,
550
+ sender_id: Optional[Union[str, List[str]]] = None,
551
+ sender_type: Optional[str] = None,
552
+ allow_forwarded: bool = True,
553
+ allow_files: bool = True,
554
+ allow_stickers: bool = True,
555
+ allow_polls: bool = True,
556
+ allow_contacts: bool = True,
557
+ allow_locations: bool = True,
558
+ min_text_length: Optional[int] = None,
559
+ max_text_length: Optional[int] = None,
560
+ contains: Optional[str] = None,
561
+ startswith: Optional[str] = None,
562
+ endswith: Optional[str] = None,
563
+ case_sensitive: bool = False
564
+ ):
565
+ """
566
+ Advanced decorator for handling only private messages with extended filters.
567
+ """
568
+
569
+ def decorator(func: Callable[[Any, Message], None]):
570
+ async def wrapper(bot, message: Message):
571
+
572
+ if not message.is_private:
573
+ return
574
+ if chat_id:
575
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
576
+ return
577
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
578
+ return
579
+ if sender_id:
580
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
581
+ return
582
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
583
+ return
584
+ if sender_type and message.sender_type != sender_type:
585
+ return
586
+ if not allow_forwarded and message.forwarded_from:
587
+ return
588
+ if not allow_files and message.file:
589
+ return
590
+ if not allow_stickers and message.sticker:
591
+ return
592
+ if not allow_polls and message.poll:
593
+ return
594
+ if not allow_contacts and message.contact_message:
595
+ return
596
+ if not allow_locations and (message.location or message.live_location):
597
+ return
598
+ if message.text:
599
+ text = message.text if case_sensitive else message.text.lower()
600
+ if min_text_length and len(message.text) < min_text_length:
601
+ return
602
+ if max_text_length and len(message.text) > max_text_length:
603
+ return
604
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
605
+ return
606
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
607
+ return
608
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
609
+ return
610
+ if commands:
611
+ if not message.text:
612
+ return
613
+ parts = message.text.strip().split()
614
+ cmd = parts[0].lstrip("/")
615
+ if cmd not in commands:
616
+ return
617
+ message.args = parts[1:]
618
+ if filters and not filters(message):
619
+ return
620
+ return await func(bot, message)
621
+ self._message_handlers.append({
622
+ "func": wrapper,
623
+ "filters": filters,
624
+ "commands": commands,
625
+ "chat_id": chat_id,
626
+ "private_only": True,
627
+ "sender_id": sender_id,
628
+ "sender_type": sender_type
629
+ })
630
+ return wrapper
631
+ return decorator
632
+ def on_message_channel(
633
+ self,
634
+ chat_id: Optional[Union[str, List[str]]] = None,
635
+ commands: Optional[List[str]] = None,
636
+ filters: Optional[Callable[[Message], bool]] = None,
637
+ sender_id: Optional[Union[str, List[str]]] = None,
638
+ sender_type: Optional[str] = None,
639
+ allow_forwarded: bool = True,
640
+ allow_files: bool = True,
641
+ allow_stickers: bool = True,
642
+ allow_polls: bool = True,
643
+ allow_contacts: bool = True,
644
+ allow_locations: bool = True,
645
+ min_text_length: Optional[int] = None,
646
+ max_text_length: Optional[int] = None,
647
+ contains: Optional[str] = None,
648
+ startswith: Optional[str] = None,
649
+ endswith: Optional[str] = None,
650
+ case_sensitive: bool = False
651
+ ):
652
+ """
653
+ Advanced decorator for handling only channel messages with extended filters.
654
+ """
655
+
656
+ def decorator(func: Callable[[Any, Message], None]):
657
+ async def wrapper(bot, message: Message):
658
+
659
+ if not message.is_channel:
660
+ return
661
+ if chat_id:
662
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
663
+ return
664
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
665
+ return
666
+ if sender_id:
667
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
668
+ return
669
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
670
+ return
671
+ if sender_type and message.sender_type != sender_type:
672
+ return
673
+ if not allow_forwarded and message.forwarded_from:
674
+ return
675
+ if not allow_files and message.file:
676
+ return
677
+ if not allow_stickers and message.sticker:
678
+ return
679
+ if not allow_polls and message.poll:
680
+ return
681
+ if not allow_contacts and message.contact_message:
682
+ return
683
+ if not allow_locations and (message.location or message.live_location):
684
+ return
685
+ if message.text:
686
+ text = message.text if case_sensitive else message.text.lower()
687
+ if min_text_length and len(message.text) < min_text_length:
688
+ return
689
+ if max_text_length and len(message.text) > max_text_length:
690
+ return
691
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
692
+ return
693
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
694
+ return
695
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
696
+ return
697
+ if commands:
698
+ if not message.text:
699
+ return
700
+ parts = message.text.strip().split()
701
+ cmd = parts[0].lstrip("/")
702
+ if cmd not in commands:
703
+ return
704
+ message.args = parts[1:]
705
+ if filters and not filters(message):
706
+ return
707
+ return await func(bot, message)
708
+ self._message_handlers.append({
709
+ "func": wrapper,
710
+ "filters": filters,
711
+ "commands": commands,
712
+ "chat_id": chat_id,
713
+ "group_only": True,
714
+ "sender_id": sender_id,
715
+ "sender_type": sender_type
716
+ })
717
+ return wrapper
718
+ return decorator
719
+ def on_message_group(
720
+ self,
721
+ chat_id: Optional[Union[str, List[str]]] = None,
722
+ commands: Optional[List[str]] = None,
723
+ filters: Optional[Callable[[Message], bool]] = None,
724
+ sender_id: Optional[Union[str, List[str]]] = None,
725
+ sender_type: Optional[str] = None,
726
+ allow_forwarded: bool = True,
727
+ allow_files: bool = True,
728
+ allow_stickers: bool = True,
729
+ allow_polls: bool = True,
730
+ allow_contacts: bool = True,
731
+ allow_locations: bool = True,
732
+ min_text_length: Optional[int] = None,
733
+ max_text_length: Optional[int] = None,
734
+ contains: Optional[str] = None,
735
+ startswith: Optional[str] = None,
736
+ endswith: Optional[str] = None,
737
+ case_sensitive: bool = False
738
+ ):
739
+ """
740
+ Advanced decorator for handling only group messages with extended filters.
741
+ """
742
+
743
+ def decorator(func: Callable[[Any, Message], None]):
744
+ async def wrapper(bot, message: Message):
745
+
746
+ if not message.is_group:
747
+ return
748
+ if chat_id:
749
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
750
+ return
751
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
752
+ return
753
+ if sender_id:
754
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
755
+ return
756
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
757
+ return
758
+ if sender_type and message.sender_type != sender_type:
759
+ return
760
+ if not allow_forwarded and message.forwarded_from:
761
+ return
762
+ if not allow_files and message.file:
763
+ return
764
+ if not allow_stickers and message.sticker:
765
+ return
766
+ if not allow_polls and message.poll:
767
+ return
768
+ if not allow_contacts and message.contact_message:
769
+ return
770
+ if not allow_locations and (message.location or message.live_location):
771
+ return
772
+ if message.text:
773
+ text = message.text if case_sensitive else message.text.lower()
774
+ if min_text_length and len(message.text) < min_text_length:
775
+ return
776
+ if max_text_length and len(message.text) > max_text_length:
777
+ return
778
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
779
+ return
780
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
781
+ return
782
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
783
+ return
784
+ if commands:
785
+ if not message.text:
786
+ return
787
+ parts = message.text.strip().split()
788
+ cmd = parts[0].lstrip("/")
789
+ if cmd not in commands:
790
+ return
791
+ message.args = parts[1:]
792
+ if filters and not filters(message):
793
+ return
794
+ return await func(bot, message)
795
+ self._message_handlers.append({
796
+ "func": wrapper,
797
+ "filters": filters,
798
+ "commands": commands,
799
+ "chat_id": chat_id,
800
+ "group_only": True,
801
+ "sender_id": sender_id,
802
+ "sender_type": sender_type
803
+ })
804
+ return wrapper
805
+ return decorator
806
+ def remove_handler(self, func: Callable):
807
+ """
808
+ Remove a message handler by its original function reference.
809
+ """
810
+ self._message_handlers = [
811
+ h for h in self._message_handlers if h["func"].__wrapped__ != func
812
+ ]
813
+ def on_edited_message(
814
+ self,
815
+ filters: Optional[Callable[[Message], bool]] = None,
816
+ commands: Optional[List[str]] = None
817
+ ):
818
+ def decorator(func: Callable[[Any, Message], None]):
819
+ async def wrapper(bot, message: Message):
820
+ if filters and not filters(message):
821
+ return
822
+ if commands:
823
+ if not message.is_command:
824
+ return
825
+ cmd = message.text.split()[0].lstrip("/")
826
+ if cmd not in commands:
827
+ return
828
+ return await func(bot, message)
829
+
830
+ self._edited_message_handlers.append({
831
+ "func": wrapper,
832
+ "filters": filters,
833
+ "commands": commands
834
+ })
835
+ return wrapper
836
+ return decorator
837
+ def on_message(
838
+ self,
839
+ filters: Optional[Callable[[Message], bool]] = None,
840
+ commands: Optional[List[str]] = None):
841
+ def decorator(func: Callable[[Any, Message], None]):
842
+ async def wrapper(bot, message: Message):
843
+ if filters and not filters(message):
844
+ return
845
+ if commands:
846
+ if not message.is_command:
847
+ return
848
+ cmd = message.text.split()[0].lstrip("/")
849
+ if cmd not in commands:
850
+ return
851
+
852
+ return await func(bot, message)
853
+ self._message_handlers.append({
854
+ "func": wrapper,
855
+ "filters": filters,
856
+ "commands": commands
857
+ })
858
+ self._edited_message_handlers.append({
859
+ "func": wrapper,
860
+ "filters": filters,
861
+ "commands": commands
862
+ })
863
+
864
+ return wrapper
865
+ return decorator
866
+
867
+
868
+ def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
869
+ def decorator(func: Callable[[Any, Message], None]):
870
+ async def wrapper(bot, message: Message):
871
+ if not message.file:return
872
+ if filters and not filters(message):return
873
+ return await func(bot, message)
874
+
875
+ self._message_handlers.append({
876
+ "func": wrapper,
877
+ "filters": filters,
878
+ "file_only": True,
879
+ "commands": commands
880
+ })
881
+ return wrapper
882
+ return decorator
883
+ def on_message_forwarded(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
884
+ def decorator(func: Callable[[Any, Message], None]):
885
+ async def wrapper(bot, message: Message):
886
+ if not message.is_forwarded:return
887
+ if filters and not filters(message):return
888
+ return await func(bot, message)
889
+
890
+ self._message_handlers.append({
891
+ "func": wrapper,
892
+ "filters": filters,
893
+ "forwarded_only": True,
894
+ "commands": commands
895
+ })
896
+ return wrapper
897
+ return decorator
898
+ def on_message_reply(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
899
+ def decorator(func: Callable[[Any, Message], None]):
900
+ async def wrapper(bot, message: Message):
901
+ if not message.is_reply:return
902
+ if filters and not filters(message):return
903
+ return await func(bot, message)
904
+
905
+ self._message_handlers.append({
906
+ "func": wrapper,
907
+ "filters": filters,
908
+ "reply_only": True,
909
+ "commands": commands
910
+ })
911
+ return wrapper
912
+ return decorator
913
+ def on_message_text(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
914
+ def decorator(func: Callable[[Any, Message], None]):
915
+ async def wrapper(bot, message: Message):
916
+ if not message.text:return
917
+ if filters and not filters(message):return
918
+ return await func(bot, message)
919
+
920
+ self._message_handlers.append({
921
+ "func": wrapper,
922
+ "filters": filters,
923
+ "text_only": True,
924
+ "commands": commands
925
+ })
926
+ return wrapper
927
+ return decorator
928
+ def on_message_media(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
929
+ def decorator(func: Callable[[Any, Message], None]):
930
+ async def wrapper(bot, message: Message):
931
+ if not message.is_media:return
932
+ if filters and not filters(message):return
933
+ return await func(bot, message)
934
+
935
+ self._message_handlers.append({
936
+ "func": wrapper,
937
+ "filters": filters,
938
+ "media_only": True,
939
+ "commands": commands
940
+ })
941
+ return wrapper
942
+ return decorator
943
+ def on_message_sticker(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
944
+ def decorator(func: Callable[[Any, Message], None]):
945
+ async def wrapper(bot, message: Message):
946
+ if not message.sticker:return
947
+ if filters and not filters(message):return
948
+ return await func(bot, message)
949
+
950
+ self._message_handlers.append({
951
+ "func": wrapper,
952
+ "filters": filters,
953
+ "sticker_only": True,
954
+ "commands": commands
955
+ })
956
+ return wrapper
957
+ return decorator
958
+ def on_message_contact(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
959
+ def decorator(func: Callable[[Any, Message], None]):
960
+ async def wrapper(bot, message: Message):
961
+ if not message.is_contact:return
962
+ if filters and not filters(message):return
963
+ return await func(bot, message)
964
+
965
+ self._message_handlers.append({
966
+ "func": wrapper,
967
+ "filters": filters,
968
+ "contact_only": True,
969
+ "commands": commands
970
+ })
971
+ return wrapper
972
+ return decorator
973
+ def on_message_location(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
974
+ def decorator(func: Callable[[Any, Message], None]):
975
+ async def wrapper(bot, message: Message):
976
+ if not message.is_location:return
977
+ if filters and not filters(message):return
978
+ return await func(bot, message)
979
+
980
+ self._message_handlers.append({
981
+ "func": wrapper,
982
+ "filters": filters,
983
+ "location_only": True,
984
+ "commands": commands
985
+ })
986
+ return wrapper
987
+ return decorator
988
+ def on_message_poll(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
989
+ def decorator(func: Callable[[Any, Message], None]):
990
+ async def wrapper(bot, message: Message):
991
+ if not message.is_poll:return
992
+ if filters and not filters(message):return
993
+ return await func(bot, message)
994
+
995
+ self._message_handlers.append({
996
+ "func": wrapper,
997
+ "filters": filters,
998
+ "poll_only": True,
999
+ "commands": commands
1000
+ })
1001
+ return wrapper
1002
+ return decorator
1003
+ def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
1004
+ def decorator(func: Callable[[Any, Message], None]):
1005
+ self._message_handlers.append({
1006
+ "func": func,
1007
+ "filters": filters,
1008
+ "commands": commands
1009
+ })
1010
+ return func
1011
+ return decorator
1012
+
1013
+ def on_callback(self, button_id: Optional[str] = None):
1014
+ def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
1015
+ if not hasattr(self, "_callback_handlers"):
1016
+ self._callback_handlers = []
1017
+ self._callback_handlers.append({
1018
+ "func": func,
1019
+ "button_id": button_id
1020
+ })
1021
+ return func
1022
+ return decorator
1023
+ def on_callback_query(self, button_id: Optional[str] = None):
1024
+ def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
1025
+ if not hasattr(self, "_callback_handlers"):
1026
+ self._callback_handlers = []
1027
+ self._callback_handlers.append({
1028
+ "func": func,
1029
+ "button_id": button_id
1030
+ })
1031
+ return func
1032
+ return decorator
1033
+ def callback_query_handler(self, button_id: Optional[str] = None):
1034
+ def decorator(func: Callable[[Any, Message], None]):
1035
+ if not hasattr(self, "_callback_handlers"):
1036
+ self._callback_handlers = []
1037
+ self._callback_handlers.append({
1038
+ "func": func,
1039
+ "button_id": button_id
1040
+ })
1041
+ return func
1042
+ return decorator
1043
+ def callback_query(self, button_id: Optional[str] = None):
1044
+ def decorator(func: Callable[[Any, Message], None]):
1045
+ if not hasattr(self, "_callback_handlers"):
1046
+ self._callback_handlers = []
1047
+ self._callback_handlers.append({
1048
+ "func": func,
1049
+ "button_id": button_id
1050
+ })
1051
+ return func
1052
+ return decorator
1053
+
1054
+ async def _handle_inline_query(self, inline_message: InlineMessage):
1055
+ aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
1056
+ for handler in self._inline_query_handlers:
1057
+ if handler["button_id"] is None or handler["button_id"] == aux_button_id:
1058
+ try:
1059
+ await handler["func"](self, inline_message)
1060
+ except Exception as e:
1061
+ raise Exception(f"Error in inline query handler: {e}")
1062
+
1063
+ def on_inline_query(self, button_id: Optional[str] = None):
1064
+ def decorator(func: Callable[[Any, InlineMessage], None]):
1065
+ self._inline_query_handlers.append({
1066
+ "func": func,
1067
+ "button_id": button_id
1068
+ })
1069
+ return func
1070
+ return decorator
1071
+ def on_inline_query_prefix(self, prefix: str, button_id: Optional[str] = None):
1072
+ if not prefix.startswith('/'):
1073
+ prefix = '/' + prefix
1074
+ def decorator(func: Callable[[Any, InlineMessage], None]):
1075
+ async def handler_wrapper(bot_instance, inline_message: InlineMessage):
1076
+ if not inline_message.raw_data or 'text' not in inline_message.raw_data:
1077
+ return
1078
+ query_text = inline_message.raw_data['text']
1079
+ if query_text.startswith(prefix):
1080
+ try:
1081
+ await func(bot_instance, inline_message)
1082
+ except Exception as e:
1083
+ raise Exception(f"Error in inline query prefix handler '{prefix}': {e}")
1084
+ self._inline_query_handlers.append({
1085
+ "func": handler_wrapper,
1086
+ "button_id": button_id
1087
+ })
1088
+ return func
1089
+ return decorator
1090
+ async def _process_update(self, update: dict):
1091
+ if update.get("type") == "ReceiveQuery":
1092
+ msg = update.get("inline_message", {})
1093
+ context = InlineMessage(bot=self, raw_data=msg)
1094
+ if hasattr(self, "_callback_handlers"):
1095
+ for handler in self._callback_handlers:
1096
+ if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
1097
+ asyncio.create_task(handler["func"](self, context))
1098
+ asyncio.create_task(self._handle_inline_query(context))
1099
+ return
1100
+
1101
+ if update.get("type") == "NewMessage":
1102
+ msg = update.get("new_message", {})
1103
+ try:
1104
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:return
1105
+ except (ValueError, TypeError):return
1106
+ context = Message(bot=self,
1107
+ chat_id=update.get("chat_id"),
1108
+ message_id=msg.get("message_id"),
1109
+ sender_id=msg.get("sender_id"),
1110
+ text=msg.get("text"),
1111
+ raw_data=msg)
1112
+ if context.aux_data and self._callback_handlers:
1113
+ for handler in self._callback_handlers:
1114
+ if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
1115
+ asyncio.create_task(handler["func"](self, context))
1116
+ return
1117
+ if self._message_handlers:
1118
+ for handler_info in self._message_handlers:
1119
+
1120
+ if handler_info["commands"]:
1121
+ if not context.text or not context.text.startswith("/"):
1122
+ continue
1123
+ parts = context.text.split()
1124
+ cmd = parts[0][1:]
1125
+ if cmd not in handler_info["commands"]:
1126
+ continue
1127
+ context.args = parts[1:]
1128
+ if handler_info["filters"]:
1129
+ if not handler_info["filters"](context):
1130
+ continue
1131
+ if not handler_info["commands"] and not handler_info["filters"]:
1132
+ asyncio.create_task(handler_info["func"](self, context))
1133
+ continue
1134
+ if handler_info["commands"] or handler_info["filters"]:
1135
+ asyncio.create_task(handler_info["func"](self, context))#kir baba kir
1136
+ continue
1137
+ elif update.get("type") == "UpdatedMessage":
1138
+ msg = update.get("updated_message", {})
1139
+ if not msg:
1140
+ return
1141
+
1142
+ context = Message(
1143
+ bot=self,
1144
+ chat_id=update.get("chat_id"),
1145
+ message_id=msg.get("message_id"),
1146
+ text=msg.get("text"),
1147
+ sender_id=msg.get("sender_id"),
1148
+ raw_data=msg
1149
+ )
1150
+ if self._edited_message_handlers:
1151
+ for handler_info in self._edited_message_handlers:
1152
+ if handler_info["commands"]:
1153
+ if not context.text or not context.text.startswith("/"):
1154
+ continue
1155
+ parts = context.text.split()
1156
+ cmd = parts[0][1:]
1157
+ if cmd not in handler_info["commands"]:
1158
+ continue
1159
+ context.args = parts[1:]
1160
+ if handler_info["filters"]:
1161
+ if not handler_info["filters"](context):
1162
+ continue
1163
+ asyncio.create_task(handler_info["func"](self, context))
1164
+
1165
+ async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
1166
+ data = {}
1167
+ if offset_id: data["offset_id"] = offset_id
1168
+ if limit: data["limit"] = limit
1169
+ return await self._post("getUpdates", data)
1170
+
1171
+ async def update_webhook(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
1172
+ session = await self._get_session()
1173
+ params = {}
1174
+ if offset_id: params['offset_id'] = offset_id
1175
+ if limit: params['limit'] = limit
1176
+ async with session.get(self.web_hook, params=params) as response:
1177
+ response.raise_for_status()
1178
+ return await response.json()
1179
+
1180
+ def _is_duplicate(self, key: str, max_age_sec: int = 300) -> bool:
1181
+ now = time.time()
1182
+ expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
1183
+ for mid in expired:
1184
+ del self._processed_message_ids[mid]
1185
+ if key in self._processed_message_ids:
1186
+ return True
1187
+ self._processed_message_ids[key] = now
1188
+ if len(self._processed_message_ids) > self._max_cache_size:
1189
+ self._processed_message_ids.popitem(last=False)
1190
+ return False
1191
+
1192
+ async def run_progelry(
1193
+ self,
1194
+ debug: bool = False,
1195
+ sleep_time: float = 0.1,
1196
+ webhook_timeout: int = 20,
1197
+ update_limit: int = 100,
1198
+ retry_delay: float = 5.0,
1199
+ stop_on_error: bool = False,
1200
+ max_errors: int = 0,
1201
+ auto_restart: bool = False,
1202
+ max_runtime: Optional[float] = None,
1203
+ loop_forever: bool = True,
1204
+ allowed_update_types: Optional[List[str]] = None,
1205
+ ignore_duplicate_messages: bool = True,
1206
+ skip_inline_queries: bool = False,
1207
+ skip_channel_posts: bool = False,
1208
+ skip_service_messages: bool = False,
1209
+ skip_edited_messages: bool = False,
1210
+ skip_bot_messages: bool = False,
1211
+ log_file: Optional[str] = None,
1212
+ log_level: str = "info",
1213
+ print_exceptions: bool = True,
1214
+ error_handler: Optional[Callable[[Exception], Any]] = None,
1215
+ shutdown_hook: Optional[Callable[[], Any]] = None,
1216
+ save_unprocessed_updates: bool = False,
1217
+ log_to_console: bool = True,
1218
+ rate_limit: Optional[float] = None,
1219
+ max_message_size: Optional[int] = None,
1220
+ ignore_users: Optional[Set[str]] = None,
1221
+ ignore_groups: Optional[Set[str]] = None,
1222
+ require_auth_token: bool = False,
1223
+ only_private_chats: bool = False,
1224
+ only_groups: bool = False,
1225
+ require_admin_rights: bool = False,
1226
+ custom_update_fetcher: Optional[Callable[[], Any]] = None,
1227
+ custom_update_processor: Optional[Callable[[Any], Any]] = None,
1228
+ process_in_background: bool = False,
1229
+ max_queue_size: int = 1000,
1230
+ thread_workers: int = 3,
1231
+ message_filter: Optional[Callable[[Any], bool]] = None,
1232
+ pause_on_idle: bool = False,
1233
+ max_concurrent_tasks: Optional[int] = None,
1234
+ metrics_enabled: bool = False,
1235
+ metrics_handler: Optional[Callable[[dict], Any]] = None,
1236
+ notify_on_error: bool = False,
1237
+ notification_handler: Optional[Callable[[str], Any]] = None,
1238
+ watchdog_timeout: Optional[float] = None,
1239
+ ):
1240
+ """
1241
+ Starts the bot's main execution loop with extensive configuration options.
1242
+
1243
+ This function handles:
1244
+ - Update fetching and processing with optional filters for types and sources.
1245
+ - Error handling, retry mechanisms, and automatic restart options.
1246
+ - Logging to console and/or files with configurable log levels.
1247
+ - Message filtering based on users, groups, chat types, admin rights, and more.
1248
+ - Custom update fetchers and processors for advanced use cases.
1249
+ - Background processing with threading and task concurrency controls.
1250
+ - Metrics collection and error notifications.
1251
+ - Optional runtime limits, sleep delays, rate limiting, and watchdog monitoring.
1252
+
1253
+ Parameters
1254
+ ----------
1255
+ debug : bool
1256
+ Enable debug mode for detailed logging and runtime checks.
1257
+ sleep_time : float
1258
+ Delay between update fetch cycles (seconds).
1259
+ webhook_timeout : int
1260
+ Timeout for webhook requests (seconds).
1261
+ update_limit : int
1262
+ Maximum updates to fetch per request.
1263
+ retry_delay : float
1264
+ Delay before retrying after failure (seconds).
1265
+ stop_on_error : bool
1266
+ Stop bot on unhandled errors.
1267
+ max_errors : int
1268
+ Maximum consecutive errors before stopping (0 = unlimited).
1269
+ auto_restart : bool
1270
+ Automatically restart the bot if it stops unexpectedly.
1271
+ max_runtime : float | None
1272
+ Maximum runtime in seconds before stopping.
1273
+ loop_forever : bool
1274
+ Keep the bot running continuously.
1275
+
1276
+ allowed_update_types : list[str] | None
1277
+ Limit processing to specific update types.
1278
+ ignore_duplicate_messages : bool
1279
+ Skip identical messages.
1280
+ skip_inline_queries : bool
1281
+ Ignore inline query updates.
1282
+ skip_channel_posts : bool
1283
+ Ignore channel post updates.
1284
+ skip_service_messages : bool
1285
+ Ignore service messages.
1286
+ skip_edited_messages : bool
1287
+ Ignore edited messages.
1288
+ skip_bot_messages : bool
1289
+ Ignore messages from other bots.
1290
+
1291
+ log_file : str | None
1292
+ File path for logging.
1293
+ log_level : str
1294
+ Logging level (debug, info, warning, error).
1295
+ print_exceptions : bool
1296
+ Print exceptions to console.
1297
+ error_handler : callable | None
1298
+ Custom function to handle errors.
1299
+ shutdown_hook : callable | None
1300
+ Function to execute on shutdown.
1301
+ save_unprocessed_updates : bool
1302
+ Save updates that failed processing.
1303
+ log_to_console : bool
1304
+ Enable/disable console logging.
1305
+
1306
+ rate_limit : float | None
1307
+ Minimum delay between processing updates from the same user/group.
1308
+ max_message_size : int | None
1309
+ Maximum allowed message size.
1310
+ ignore_users : set[str] | None
1311
+ User IDs to ignore.
1312
+ ignore_groups : set[str] | None
1313
+ Group IDs to ignore.
1314
+ require_auth_token : bool
1315
+ Require users to provide authentication token.
1316
+ only_private_chats : bool
1317
+ Process only private chats.
1318
+ only_groups : bool
1319
+ Process only group chats.
1320
+ require_admin_rights : bool
1321
+ Process only if sender is admin.
1322
+
1323
+ custom_update_fetcher : callable | None
1324
+ Custom update fetching function.
1325
+ custom_update_processor : callable | None
1326
+ Custom update processing function.
1327
+ process_in_background : bool
1328
+ Run processing in background threads.
1329
+ max_queue_size : int
1330
+ Maximum updates in processing queue.
1331
+ thread_workers : int
1332
+ Number of background worker threads.
1333
+ message_filter : callable | None
1334
+ Function to filter messages.
1335
+ pause_on_idle : bool
1336
+ Pause processing if idle.
1337
+ max_concurrent_tasks : int | None
1338
+ Maximum concurrent processing tasks.
1339
+
1340
+ metrics_enabled : bool
1341
+ Enable metrics collection.
1342
+ metrics_handler : callable | None
1343
+ Function to handle metrics.
1344
+ notify_on_error : bool
1345
+ Send notifications on errors.
1346
+ notification_handler : callable | None
1347
+ Function to send error notifications.
1348
+ watchdog_timeout : float | None
1349
+ Maximum idle time before triggering watchdog restart.
1350
+ """
1351
+ import asyncio, time, datetime, traceback
1352
+ from collections import deque
1353
+ def _log(msg: str, level: str = "info"):
1354
+ level_order = {"debug": 10, "info": 20, "warning": 30, "error": 40}
1355
+ if level not in level_order:
1356
+ level = "info"
1357
+ if level_order[level] < level_order.get(log_level, 20):
1358
+ return
1359
+ line = f"[{level.upper()}] {datetime.datetime.now().isoformat()} - {msg}"
1360
+ if log_to_console:
1361
+ print(msg)
1362
+ if log_file:
1363
+ try:
1364
+ with open(log_file, "a", encoding="utf-8") as f:
1365
+ f.write(line + "\n")
1366
+ except Exception:
1367
+ pass
1368
+
1369
+ def _get_sender_and_chat(update: dict):
1370
+
1371
+ sender = None
1372
+ chat = None
1373
+ t = update.get("type")
1374
+ if t == "NewMessage":
1375
+ nm = update.get("new_message", {})
1376
+ sender = nm.get("author_object_guid") or nm.get("author_guid") or nm.get("from_id")
1377
+ chat = nm.get("object_guid") or nm.get("chat_id")
1378
+ elif t == "ReceiveQuery":
1379
+ im = update.get("inline_message", {})
1380
+ sender = im.get("author_object_guid") or im.get("author_guid")
1381
+ chat = im.get("object_guid") or im.get("chat_id")
1382
+ elif t == "UpdatedMessage":
1383
+ im = update.get("updated_message", {})
1384
+ sender = im.get("author_object_guid") or im.get("author_guid")
1385
+ chat = im.get("object_guid") or im.get("chat_id")
1386
+ else:
1387
+ sender = update.get("author_guid") or update.get("from_id")
1388
+ chat = update.get("object_guid") or update.get("chat_id")
1389
+ return str(sender) if sender is not None else None, str(chat) if chat is not None else None
1390
+
1391
+ def _is_group_chat(chat_guid: Optional[str]) -> Optional[bool]:
1392
+
1393
+ if chat_guid is None:
1394
+ return None
1395
+ if hasattr(self, "_is_group_chat") and callable(getattr(self, "_is_group_chat")):
1396
+ try:
1397
+ return bool(self._is_group_chat(chat_guid))
1398
+ except Exception:
1399
+ return None
1400
+ return None
1401
+
1402
+ async def _maybe_notify(err: Exception, context: dict):
1403
+ if notify_on_error and notification_handler:
1404
+ try:
1405
+ if asyncio.iscoroutinefunction(notification_handler):
1406
+ await notification_handler(err, context)
1407
+ else:
1408
+
1409
+ notification_handler(err, context)
1410
+ except Exception:
1411
+ pass
1412
+
1413
+ async def _handle_error(err: Exception, context: dict):
1414
+ if print_exceptions:
1415
+ _log("Exception occurred:\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__)), "error")
1416
+ else:
1417
+ _log(f"Exception occurred: {err}", "error")
1418
+ await _maybe_notify(err, context)
1419
+ if error_handler:
1420
+ try:
1421
+ if asyncio.iscoroutinefunction(error_handler):
1422
+ await error_handler(err, context)
1423
+ else:
1424
+ error_handler(err, context)
1425
+ except Exception as e2:
1426
+ _log(f"Error in error_handler: {e2}", "error")
1427
+
1428
+
1429
+ rate_window = deque()
1430
+ def _rate_ok():
1431
+ if rate_limit is None or rate_limit <= 0:
1432
+ return True
1433
+ now = time.time()
1434
+
1435
+ while rate_window and now - rate_window[0] > 1.0:
1436
+ rate_window.popleft()
1437
+ if len(rate_window) < int(rate_limit):
1438
+ rate_window.append(now)
1439
+ return True
1440
+ return False
1441
+
1442
+
1443
+ queue = asyncio.Queue(maxsize=max_queue_size) if process_in_background else None
1444
+ active_workers = []
1445
+
1446
+ sem = asyncio.Semaphore(max_concurrent_tasks) if max_concurrent_tasks and max_concurrent_tasks > 0 else None
1447
+
1448
+ async def _process(update: dict):
1449
+
1450
+ if allowed_update_types and update.get("type") not in allowed_update_types:
1451
+ return False
1452
+
1453
+
1454
+ t = update.get("type")
1455
+ if skip_inline_queries and t == "ReceiveQuery":
1456
+ return False
1457
+ if skip_service_messages and t == "ServiceMessage":
1458
+ return False
1459
+ if skip_channel_posts and t == "ChannelPost":
1460
+ return False
1461
+
1462
+
1463
+ sender, chat = _get_sender_and_chat(update)
1464
+ if ignore_users and sender and sender in ignore_users:
1465
+ return False
1466
+ if ignore_groups and chat and chat in ignore_groups:
1467
+ return False
1468
+ if require_auth_token and not getattr(self, "_has_auth_token", False):
1469
+ return False
1470
+ if only_private_chats:
1471
+ is_group = _is_group_chat(chat)
1472
+ if is_group is True:
1473
+ return False
1474
+ if only_groups:
1475
+ is_group = _is_group_chat(chat)
1476
+ if is_group is False:
1477
+ return False
1478
+ if skip_bot_messages and getattr(self, "_is_bot_guid", None) and sender == self._is_bot_guid:
1479
+ return False
1480
+
1481
+ if max_message_size is not None and max_message_size > 0:
1482
+
1483
+ content = None
1484
+ if t == "NewMessage":
1485
+ content = (update.get("new_message") or {}).get("text")
1486
+ elif t == "ReceiveQuery":
1487
+ content = (update.get("inline_message") or {}).get("text")
1488
+ elif t == "UpdatedMessage":
1489
+ content = (update.get("updated_message") or {}).get("text")
1490
+ elif "text" in update:
1491
+ content = update.get("text")
1492
+ if content and isinstance(content, str) and len(content) > max_message_size:
1493
+ return False
1494
+
1495
+ if message_filter:
1496
+ try:
1497
+ if not message_filter(update):
1498
+ return False
1499
+ except Exception:
1500
+
1501
+ pass
1502
+
1503
+
1504
+ if not _rate_ok():
1505
+ return False
1506
+
1507
+
1508
+ if custom_update_processor:
1509
+ if asyncio.iscoroutinefunction(custom_update_processor):
1510
+ await custom_update_processor(update)
1511
+ else:
1512
+
1513
+ await asyncio.get_running_loop().run_in_executor(None, custom_update_processor, update)
1514
+ else:
1515
+
1516
+ await self._process_update(update)
1517
+ return True
1518
+
1519
+ async def _worker():
1520
+ while True:
1521
+ update = await queue.get()
1522
+ try:
1523
+ if sem:
1524
+ async with sem:
1525
+ await _process(update)
1526
+ else:
1527
+ await _process(update)
1528
+ except Exception as e:
1529
+ await _handle_error(e, {"stage": "worker_process", "update": update})
1530
+ finally:
1531
+ queue.task_done()
1532
+
1533
+
1534
+ start_ts = time.time()
1535
+ error_count = 0
1536
+ last_loop_tick = time.time()
1537
+ processed_count = 0
1538
+ skipped_count = 0
1539
+ enqueued_count = 0
1540
+ unprocessed_storage = []
1541
+
1542
+
1543
+ if process_in_background:
1544
+ n_workers = max(1, int(thread_workers))
1545
+ for _ in range(n_workers):
1546
+ active_workers.append(asyncio.create_task(_worker()))
1547
+
1548
+
1549
+ await check_rubka_version()
1550
+ await self._initialize_webhook()
1551
+ await self.geteToken()
1552
+ _log("Bot is up and running...", "info")
1553
+
1554
+ try:
1555
+ while True:
1556
+ try:
1557
+
1558
+ if max_runtime is not None and (time.time() - start_ts) >= max_runtime:
1559
+ _log("Max runtime reached. Stopping loop.", "warning")
1560
+ break
1561
+
1562
+
1563
+ now = time.time()
1564
+ if watchdog_timeout and (now - last_loop_tick) > watchdog_timeout:
1565
+ _log(f"Watchdog triggered (> {watchdog_timeout}s)", "warning")
1566
+ if auto_restart:
1567
+ break
1568
+ last_loop_tick = now
1569
+
1570
+
1571
+ received_updates = None
1572
+ if custom_update_fetcher:
1573
+ received_updates = await custom_update_fetcher()
1574
+ elif self.web_hook:
1575
+ webhook_data = await self.update_webhook()
1576
+ received_updates = []
1577
+ if isinstance(webhook_data, list):
1578
+ for item in webhook_data:
1579
+ data = item.get("data", {})
1580
+
1581
+ received_at_str = item.get("received_at")
1582
+ if received_at_str:
1583
+ try:
1584
+ received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
1585
+ if time.time() - received_at_ts > webhook_timeout:
1586
+ if debug:
1587
+ _log(f"Skipped old webhook update ({received_at_str})", "debug")
1588
+ continue
1589
+ except (ValueError, TypeError):
1590
+ pass
1591
+
1592
+ update = None
1593
+ if "update" in data:
1594
+ update = data["update"]
1595
+ elif "inline_message" in data:
1596
+ update = {"type": "ReceiveQuery", "inline_message": data["inline_message"]}
1597
+ else:
1598
+ continue
1599
+
1600
+
1601
+ message_id = None
1602
+ if update.get("type") == "NewMessage":
1603
+ message_id = update.get("new_message", {}).get("message_id")
1604
+ elif update.get("type") == "ReceiveQuery":
1605
+ message_id = update.get("inline_message", {}).get("message_id")
1606
+ elif update.get("type") == "UpdatedMessage":
1607
+ message_id = update.get("updated_message", {}).get("message_id")
1608
+ elif "message_id" in update:
1609
+ message_id = update.get("message_id")
1610
+
1611
+
1612
+ dup_ok = True
1613
+ if ignore_duplicate_messages:
1614
+ key = str(received_at_str) if received_at_str else str(message_id)
1615
+ dup_ok = (not self._is_duplicate(str(key))) if key else True
1616
+
1617
+ if message_id and dup_ok:
1618
+ received_updates.append(update)
1619
+ else:
1620
+ get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=update_limit)
1621
+ received_updates = []
1622
+ if get_updates_response and get_updates_response.get("data"):
1623
+ updates = get_updates_response["data"].get("updates", [])
1624
+ self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
1625
+ for update in updates:
1626
+ message_id = None
1627
+ if update.get("type") == "NewMessage":
1628
+ msg_data = update.get("new_message", {})
1629
+ message_id = msg_data.get("message_id")
1630
+ text_content = msg_data.get("text", "")
1631
+ msg_time = int(msg_data.get("time", 0))
1632
+ elif update.get("type") == "ReceiveQuery":
1633
+ msg_data = update.get("inline_message", {})
1634
+ message_id = msg_data.get("message_id")
1635
+ text_content = msg_data.get("text", "")
1636
+ msg_time = int(msg_data.get("time", 0))
1637
+ elif update.get("type") == "UpdatedMessage":
1638
+ msg_data = update.get("updated_message", {})
1639
+ message_id = msg_data.get("message_id")
1640
+ text_content = msg_data.get("text", "")
1641
+ msg_time = int(msg_data.get("time", 0))
1642
+ elif "message_id" in update:
1643
+ message_id = update.get("message_id")
1644
+ else:
1645
+ msg_time = time.time()
1646
+ msg_data = update.get("updated_message", {})
1647
+ message_id = msg_data.get("message_id")
1648
+ text_content = msg_data.get("text", "")
1649
+ now = int(time.time())
1650
+
1651
+ if msg_time and (now - msg_time > self.max_msg_age):
1652
+ continue
1653
+ dup_ok = True
1654
+ if ignore_duplicate_messages and message_id:
1655
+ dup_key = self._make_dup_key(message_id, update.get("type", ""), msg_data)
1656
+ dup_ok = not self._is_duplicate(dup_key)
1657
+ if message_id and dup_ok:
1658
+ received_updates.append(update)
1659
+ if not received_updates:
1660
+ if pause_on_idle and sleep_time == 0:await asyncio.sleep(0.005)
1661
+ else:await asyncio.sleep(sleep_time)
1662
+ if not loop_forever and max_runtime is None:break
1663
+ continue
1664
+
1665
+
1666
+ for update in received_updates:
1667
+ if require_admin_rights:
1668
+
1669
+ sender, _ = _get_sender_and_chat(update)
1670
+ if hasattr(self, "is_admin") and callable(getattr(self, "is_admin")):
1671
+ try:
1672
+ if not await self.is_admin(sender) if asyncio.iscoroutinefunction(self.is_admin) else not self.is_admin(sender):
1673
+ skipped_count += 1
1674
+ continue
1675
+ except Exception:
1676
+ pass
1677
+
1678
+ if process_in_background:
1679
+ try:
1680
+ queue.put_nowait(update)
1681
+ enqueued_count += 1
1682
+ except asyncio.QueueFull:
1683
+
1684
+ skipped_count += 1
1685
+ if save_unprocessed_updates:
1686
+ unprocessed_storage.append(update)
1687
+ else:
1688
+ try:
1689
+ if sem:
1690
+ async with sem:
1691
+ ok = await _process(update)
1692
+ else:
1693
+ ok = await _process(update)
1694
+ processed_count += 1 if ok else 0
1695
+ skipped_count += 0 if ok else 1
1696
+ except Exception as e:
1697
+ await _handle_error(e, {"stage": "inline_process", "update": update})
1698
+ error_count += 1
1699
+ if save_unprocessed_updates:
1700
+ unprocessed_storage.append(update)
1701
+ if stop_on_error or (max_errors and error_count >= max_errors):
1702
+ raise
1703
+
1704
+
1705
+ if process_in_background and queue.qsize() > 0:
1706
+ await asyncio.sleep(0)
1707
+
1708
+
1709
+ if debug:
1710
+ _log(f"Loop stats — processed: {processed_count}, enqueued: {enqueued_count}, skipped: {skipped_count}, queue: {queue.qsize() if queue else 0}", "debug")
1711
+
1712
+
1713
+ await asyncio.sleep(sleep_time)
1714
+
1715
+ except Exception as e:
1716
+ await _handle_error(e, {"stage": "run_loop"})
1717
+ error_count += 1
1718
+ if stop_on_error or (max_errors and error_count >= max_errors):
1719
+ break
1720
+ await asyncio.sleep(retry_delay)
1721
+
1722
+
1723
+ if not loop_forever and max_runtime is None:
1724
+ break
1725
+
1726
+ finally:
1727
+
1728
+ if process_in_background and queue:
1729
+ try:
1730
+ await queue.join()
1731
+ except Exception:
1732
+ pass
1733
+ for w in active_workers:
1734
+ w.cancel()
1735
+
1736
+ for w in active_workers:
1737
+ try:
1738
+ await w
1739
+ except Exception:
1740
+ pass
1741
+
1742
+
1743
+ if self._aiohttp_session:
1744
+ await self._aiohttp_session.close()
1745
+
1746
+
1747
+ stats = {
1748
+ "processed": processed_count,
1749
+ "skipped": skipped_count,
1750
+ "enqueued": enqueued_count,
1751
+ "errors": error_count,
1752
+ "uptime_sec": round(time.time() - start_ts, 3),
1753
+ }
1754
+ if metrics_enabled and metrics_handler:
1755
+ try:
1756
+ if asyncio.iscoroutinefunction(metrics_handler):
1757
+ await metrics_handler(stats)
1758
+ else:
1759
+ metrics_handler(stats)
1760
+ except Exception:
1761
+ pass
1762
+
1763
+ if shutdown_hook:
1764
+ try:
1765
+ if asyncio.iscoroutinefunction(shutdown_hook):
1766
+ await shutdown_hook(stats)
1767
+ else:
1768
+ shutdown_hook(stats)
1769
+ except Exception:
1770
+ pass
1771
+
1772
+ print("Bot stopped and session closed.")
1773
+
1774
+
1775
+ if auto_restart:
1776
+
1777
+
1778
+ _log("Auto-restart requested. You can call run(...) again as needed.", "warning")
1779
+ def run(self, sleep_time: float = 0.1, *args, **kwargs):
1780
+ print("Connecting to the server...")
1781
+ try:
1782
+ loop = asyncio.get_running_loop()
1783
+ return loop.create_task(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
1784
+ except RuntimeError:return asyncio.run(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
1785
+ async def _delete_after_task(self, chat_id: str, message_id: str, delay: int):
1786
+ try:
1787
+ await asyncio.sleep(delay)
1788
+ await self.delete_message(chat_id=chat_id, message_id=message_id)
1789
+ except Exception:
1790
+ return False
1791
+ async def _edit_after_task(self, chat_id: str, message_id: str, text:str, delay: int):
1792
+ try:
1793
+ await asyncio.sleep(delay)
1794
+ await self.edit_message_text(chat_id=chat_id, message_id=message_id,text=text)
1795
+ except Exception:
1796
+ return False
1797
+
1798
+ async def delete_after(self, chat_id: str, message_id: str, delay: int = 30) -> asyncio.Task:
1799
+ async def _task():
1800
+ await asyncio.sleep(delay)
1801
+ try:
1802
+ await self.delete_message(chat_id, message_id)
1803
+ except Exception:
1804
+ pass
1805
+
1806
+ try:
1807
+ loop = asyncio.get_running_loop()
1808
+ except RuntimeError:
1809
+ loop = asyncio.new_event_loop()
1810
+ asyncio.set_event_loop(loop)
1811
+
1812
+ task = loop.create_task(_task())
1813
+ return task
1814
+
1815
+ async def edit_after(self, chat_id: str, message_id: str, text : str, delay: int = 30) -> asyncio.Task:
1816
+ async def _task():
1817
+ await asyncio.sleep(delay)
1818
+ try:
1819
+ await self.edit_message_text(chat_id, message_id,text)
1820
+ except Exception:
1821
+ pass
1822
+
1823
+ try:
1824
+ loop = asyncio.get_running_loop()
1825
+ except RuntimeError:
1826
+ loop = asyncio.new_event_loop()
1827
+ asyncio.set_event_loop(loop)
1828
+
1829
+ task = loop.create_task(_task())
1830
+ return task
1831
+ def _parse_text_metadata(self, text: str, parse_mode: str):
1832
+ formatter = GlyphWeaver()
1833
+ parsed = formatter.parse(text, parse_mode)
1834
+ return parsed.get("text"), parsed.get("metadata")
1835
+
1836
+ async def send_message(
1837
+ self,
1838
+ chat_id: str,
1839
+ text: str,
1840
+ chat_keypad: Optional[Dict[str, Any]] = None,
1841
+ inline_keypad: Optional[Dict[str, Any]] = None,
1842
+ disable_notification: bool = False,
1843
+ reply_to_message_id: Optional[str] = None,
1844
+ chat_keypad_type: Optional[Literal["New", "Remove"]] = None,
1845
+ delete_after: Optional[int] = None,
1846
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
1847
+ ) -> Dict[str, Any]:
1848
+
1849
+ payload = {
1850
+ "chat_id": chat_id,
1851
+ "text": text,
1852
+ "disable_notification": disable_notification,
1853
+ }
1854
+ parse_mode_to_use = parse_mode or self.parse_mode
1855
+ if text:
1856
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
1857
+ payload["text"] = text
1858
+ if metadata:
1859
+ payload["metadata"] = metadata
1860
+ if chat_keypad:
1861
+ payload["chat_keypad"] = chat_keypad
1862
+ payload["chat_keypad_type"] = chat_keypad_type or "New"
1863
+ if inline_keypad:
1864
+ payload["inline_keypad"] = inline_keypad
1865
+ if reply_to_message_id:
1866
+ payload["reply_to_message_id"] = reply_to_message_id
1867
+ try:
1868
+ state = await self._post("sendMessage", payload)
1869
+ except Exception:
1870
+ if self.safeSendMode and reply_to_message_id:
1871
+ payload.pop("reply_to_message_id", None)
1872
+ state = await self._post("sendMessage", payload)
1873
+ else:
1874
+ raise
1875
+ if delete_after:
1876
+ await self.delete_after(chat_id, state.message_id, delete_after)
1877
+ return state
1878
+
1879
+
1880
+ async def send_sticker(
1881
+ self,
1882
+ chat_id: str,
1883
+ sticker_id: str,
1884
+ chat_keypad: Optional[Dict[str, Any]] = None,
1885
+ disable_notification: bool = False,
1886
+ inline_keypad: Optional[Dict[str, Any]] = None,
1887
+ reply_to_message_id: Optional[str] = None,
1888
+ chat_keypad_type: Optional[Literal['New', 'Remove']] = None,
1889
+ ) -> str:
1890
+ """
1891
+ Send a sticker to a chat.
1892
+
1893
+ Args:
1894
+ token: Bot token.
1895
+ chat_id: Target chat ID.
1896
+ sticker_id: ID of the sticker to send.
1897
+ chat_keypad: Optional chat keypad data.
1898
+ disable_notification: If True, disables notification.
1899
+ inline_keypad: Optional inline keyboard data.
1900
+ reply_to_message_id: Optional message ID to reply to.
1901
+ chat_keypad_type: Type of chat keypad change ('New' or 'Remove').
1902
+
1903
+ Returns:
1904
+ API response as a string.
1905
+ """
1906
+ data = {
1907
+ 'chat_id': chat_id,
1908
+ 'sticker_id': sticker_id,
1909
+ 'chat_keypad': chat_keypad,
1910
+ 'disable_notification': disable_notification,
1911
+ 'inline_keypad': inline_keypad,
1912
+ 'reply_to_message_id': reply_to_message_id,
1913
+ 'chat_keypad_type': chat_keypad_type,
1914
+ }
1915
+ return await self._post("sendSticker", data)
1916
+
1917
+
1918
+ async def get_url_file(self,file_id):
1919
+ data = await self._post("getFile", {'file_id': file_id})
1920
+ return data.get("data").get("download_url")
1921
+
1922
+ def _get_client(self) -> Client_get:
1923
+ if self.session_name:
1924
+ return Client_get(self.session_name, self.auth, self.Key, self.platform)
1925
+ else:
1926
+ return Client_get(show_last_six_words(self.token), self.auth, self.Key, self.platform)
1927
+ async def send_button_join(
1928
+ self,
1929
+ chat_id,
1930
+ title_button : Union[str, list],
1931
+ username : Union[str, list],
1932
+ text,
1933
+ reply_to_message_id=None,
1934
+ id="None"):
1935
+ from .button import InlineBuilder
1936
+ builder = InlineBuilder()
1937
+ if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1938
+ for t, u in zip(title_button, username):
1939
+ builder = builder.row(
1940
+ InlineBuilder().button_join_channel(
1941
+ text=t,
1942
+ id=id,
1943
+ username=u
1944
+ )
1945
+ )
1946
+ elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
1947
+ for u in username:
1948
+ builder = builder.row(
1949
+ InlineBuilder().button_join_channel(
1950
+ text=title_button,
1951
+ id=id,
1952
+ username=u
1953
+ )
1954
+ )
1955
+ else:
1956
+ builder = builder.row(
1957
+ InlineBuilder().button_join_channel(
1958
+ text=title_button,
1959
+ id=id,
1960
+ username=username
1961
+ )
1962
+ )
1963
+ return await self.send_message(
1964
+ chat_id=chat_id,
1965
+ text=text,
1966
+ inline_keypad=builder.build(),
1967
+ reply_to_message_id=reply_to_message_id
1968
+ )
1969
+ async def send_button_link(
1970
+ self,
1971
+ chat_id,
1972
+ title_button: Union[str, list],
1973
+ url: Union[str, list],
1974
+ text,
1975
+ reply_to_message_id=None,
1976
+ id="None"
1977
+ ):
1978
+ from .button import InlineBuilder
1979
+ builder = InlineBuilder()
1980
+ if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
1981
+ for t, u in zip(title_button, url):
1982
+ builder = builder.row(
1983
+ InlineBuilder().button_url_link(
1984
+ text=t,
1985
+ id=id,
1986
+ url=u
1987
+ )
1988
+ )
1989
+ elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
1990
+ for u in url:
1991
+ builder = builder.row(
1992
+ InlineBuilder().button_url_link(
1993
+ text=title_button,
1994
+ id=id,
1995
+ url=u
1996
+ )
1997
+ )
1998
+ else:
1999
+ builder = builder.row(
2000
+ InlineBuilder().button_url_link(
2001
+ text=title_button,
2002
+ id=id,
2003
+ url=url
2004
+ )
2005
+ )
2006
+ return await self.send_message(
2007
+ chat_id=chat_id,
2008
+ text=text,
2009
+ inline_keypad=builder.build(),
2010
+ reply_to_message_id=reply_to_message_id
2011
+ )
2012
+
2013
+ async def close_poll(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2014
+ return await self._post("closePoll", {"chat_id": chat_id, "message_id": message_id})
2015
+ async def send_location(self, chat_id: str, latitude: str, longitude: str, disable_notification: bool = False, inline_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Remove"]] = None) -> Dict[str, Any]:
2016
+ payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
2017
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
2018
+ if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
2019
+ if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
2020
+ return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
2021
+ async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
2022
+ session = await self._get_session()
2023
+ is_temp_file = False
2024
+ if isinstance(path, str) and path.startswith("http"):
2025
+ async with session.get(path) as response:
2026
+ if response.status != 200:
2027
+ raise Exception(f"Failed to download file from URL ({response.status})")
2028
+ content = await response.read()
2029
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
2030
+ tmp.write(content)
2031
+ path = tmp.name
2032
+ is_temp_file = True
2033
+
2034
+ file_size = os.path.getsize(path)
2035
+ chunk_size = self.chunk_size
2036
+
2037
+ progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024,
2038
+ desc=f'Uploading: {name}', colour='cyan', disable=not getattr(self, 'show_progress', True))
2039
+
2040
+ async def file_generator(file_path):
2041
+ async with aiofiles.open(file_path, 'rb') as f:
2042
+ while chunk := await f.read(chunk_size):
2043
+ progress_bar.update(len(chunk))
2044
+ yield chunk
2045
+
2046
+ form = aiohttp.FormData()
2047
+ form.add_field('file', file_generator(path), filename=name, content_type='application/octet-stream')
2048
+
2049
+ try:
2050
+ async with session.post(upload_url, data=form, timeout=aiohttp.ClientTimeout(total=None)) as response:
2051
+ progress_bar.close()
2052
+ if response.status != 200:
2053
+ text = await response.text()
2054
+ raise Exception(f"Upload failed ({response.status}): {text}")
2055
+ return (await response.json()).get('data', {}).get('file_id')
2056
+ except Exception as e:
2057
+ raise FeatureNotAvailableError(f"File upload not supported: {e}")
2058
+ finally:
2059
+ if is_temp_file:
2060
+ os.remove(path)
2061
+ def get_extension(content_type: str) -> str:
2062
+ ext = mimetypes.guess_extension(content_type)
2063
+ return ext if ext else ''
2064
+ async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
2065
+ """
2066
+ Download a file from server using its file_id with chunked transfer,
2067
+ progress bar, file extension detection, custom filename, and timeout.
2068
+
2069
+ If save_as is not provided, filename will be extracted from
2070
+ Content-Disposition header or Content-Type header extension.
2071
+
2072
+ Parameters:
2073
+ file_id (str): The file ID to fetch the download URL.
2074
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
2075
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
2076
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
2077
+ verbose (bool, optional): Show progress messages. Default True.
2078
+
2079
+ Returns:
2080
+ bool: True if success, raises exceptions otherwise.
2081
+ """
2082
+ try:
2083
+ url = await self.get_url_file(file_id)
2084
+ if not url:raise ValueError("Download URL not found in response.")
2085
+ except Exception as e:raise ValueError(f"Failed to get download URL: {e}")
2086
+ timeout = aiohttp.ClientTimeout(total=timeout_sec)
2087
+ try:
2088
+ async with aiohttp.ClientSession(timeout=timeout) as session:
2089
+ async with session.get(url) as resp:
2090
+ if resp.status != 200:
2091
+ raise aiohttp.ClientResponseError(
2092
+ request_info=resp.request_info,
2093
+ history=resp.history,
2094
+ status=resp.status,
2095
+ message="Failed to download file.",
2096
+ headers=resp.headers
2097
+ )
2098
+ if not save_as:
2099
+ content_disp = resp.headers.get("Content-Disposition", "")
2100
+ import re
2101
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
2102
+ if match:save_as = match.group(1)
2103
+ else:
2104
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
2105
+ extension = mimetypes.guess_extension(content_type) or ".bin"
2106
+ save_as = f"{file_id}{extension}"
2107
+ total_size = int(resp.headers.get("Content-Length", 0))
2108
+ progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
2109
+ async with aiofiles.open(save_as, "wb") as f:
2110
+ async for chunk in resp.content.iter_chunked(chunk_size):
2111
+ await f.write(chunk)
2112
+ progress.update(len(chunk))
2113
+
2114
+ progress.close()
2115
+ if verbose:
2116
+ print(f"✅ File saved as: {save_as}")
2117
+
2118
+ return True
2119
+
2120
+ except aiohttp.ClientError as e:
2121
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
2122
+ except asyncio.TimeoutError:
2123
+ raise asyncio.TimeoutError("Download timed out.")
2124
+ except Exception as e:
2125
+ raise Exception(f"Error downloading file: {e}")
2126
+
2127
+ except aiohttp.ClientError as e:
2128
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
2129
+ except asyncio.TimeoutError:
2130
+ raise asyncio.TimeoutError("The download operation timed out.")
2131
+ except Exception as e:
2132
+ raise Exception(f"An error occurred while downloading the file: {e}")
2133
+
2134
+ except aiohttp.ClientError as e:
2135
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
2136
+ except asyncio.TimeoutError:
2137
+ raise asyncio.TimeoutError("The download operation timed out.")
2138
+ except Exception as e:
2139
+ raise Exception(f"An error occurred while downloading the file: {e}")
2140
+ async def get_upload_url(self, media_type: Literal['File', 'Image', 'voice', 'Music', 'Gif', 'Video']) -> str:
2141
+ allowed = ['File', 'Image', 'voice', 'Music', 'Gif', 'Video']
2142
+ if media_type not in allowed:
2143
+ raise ValueError(f"Invalid media type. Must be one of {allowed}")
2144
+ result = await self._post("requestSendFile", {"type": media_type})
2145
+ return result.get("data", {}).get("upload_url")
2146
+ async def _send_uploaded_file(self, chat_id: str, file_id: str,type_file : str = "file",text: Optional[str] = None, chat_keypad: Optional[Dict[str, Any]] = None, inline_keypad: Optional[Dict[str, Any]] = None, disable_notification: bool = False, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2147
+ payload = {"chat_id": chat_id, "file_id": file_id, "text": text, "disable_notification": disable_notification, "chat_keypad_type": chat_keypad_type}
2148
+ if chat_keypad: payload["chat_keypad"] = chat_keypad
2149
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
2150
+ if reply_to_message_id: payload["reply_to_message_id"] = str(reply_to_message_id)
2151
+ parse_mode_to_use = parse_mode or self.parse_mode
2152
+ if text:
2153
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
2154
+ payload["text"] = text
2155
+ if metadata:payload["metadata"] = metadata
2156
+ payload["time"] = "10"
2157
+ resp = await self._post("sendFile", payload)
2158
+ message_id_put = resp["data"]["message_id"]
2159
+ result = {
2160
+ "status": resp.get("status"),
2161
+ "status_det": resp.get("status_det"),
2162
+ "file_id": file_id,
2163
+ "text":text,
2164
+ "message_id": message_id_put,
2165
+ "send_to_chat_id": chat_id,
2166
+ "reply_to_message_id": reply_to_message_id,
2167
+ "disable_notification": disable_notification,
2168
+ "type_file": type_file,
2169
+ "raw_response": resp,
2170
+ "chat_keypad":chat_keypad,
2171
+ "inline_keypad":inline_keypad,
2172
+ "chat_keypad_type":chat_keypad_type
2173
+ }
2174
+ return AttrDict(result)
2175
+ async def _send_file_generic(self, media_type, chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode: Optional[Literal["HTML", "Markdown"]] = None):
2176
+ if path:
2177
+ file_name = file_name or Path(path).name
2178
+ upload_url = await self.get_upload_url(media_type)
2179
+ file_id = await self.upload_media_file(upload_url, file_name, path)
2180
+ if not file_id:
2181
+ raise ValueError("Either path or file_id must be provided.")
2182
+ return await self._send_uploaded_file(chat_id=chat_id, file_id=file_id, text=text, inline_keypad=inline_keypad, chat_keypad=chat_keypad, reply_to_message_id=reply_to_message_id, disable_notification=disable_notification, chat_keypad_type=chat_keypad_type,type_file=media_type,parse_mode=parse_mode)
2183
+ async def send_document(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2184
+ return await self._send_file_generic("File", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2185
+ async def send_file(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2186
+ return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2187
+ async def re_send(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2188
+ return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2189
+ async def send_video(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2190
+ return await self._send_file_generic("Video", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2191
+ async def send_voice(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2192
+ return await self._send_file_generic("voice", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2193
+ async def send_image(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2194
+ return await self._send_file_generic("Image", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode)
2195
+ async def send_music(
2196
+ self,
2197
+ chat_id: str,
2198
+ path: Optional[Union[str, Path]] = None,
2199
+ file_id: Optional[str] = None,
2200
+ text: Optional[str] = None,
2201
+ file_name: Optional[str] = None,
2202
+ inline_keypad: Optional[Dict[str, Any]] = None,
2203
+ chat_keypad: Optional[Dict[str, Any]] = None,
2204
+ reply_to_message_id: Optional[str] = None,
2205
+ disable_notification: bool = False,
2206
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
2207
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
2208
+ ) -> Dict[str, Any]:
2209
+ valid_extensions = {"ogg", "oga", "opus", "flac"}
2210
+ extension = "flac"
2211
+ if path:
2212
+ path_str = str(path)
2213
+ if path_str.startswith("http://") or path_str.startswith("https://"):
2214
+ parsed = urlparse(path_str)
2215
+ base_name = os.path.basename(parsed.path)
2216
+ else:
2217
+ base_name = os.path.basename(path_str)
2218
+ name, ext = os.path.splitext(base_name)
2219
+
2220
+ if file_name is None or not file_name.strip():
2221
+ file_name = name or "music"
2222
+ ext = ext.lower().replace(".", "")
2223
+ if ext in valid_extensions:
2224
+ extension = ext
2225
+ else:
2226
+ if file_name is None:
2227
+ file_name = "music"
2228
+ return await self._send_file_generic(
2229
+ "File",
2230
+ chat_id,
2231
+ path,
2232
+ file_id,
2233
+ text,
2234
+ f"{file_name}.{extension}",
2235
+ inline_keypad,
2236
+ chat_keypad,
2237
+ reply_to_message_id,
2238
+ disable_notification,
2239
+ chat_keypad_type,
2240
+ parse_mode=parse_mode
2241
+ )
2242
+ async def send_gif(
2243
+ self,
2244
+ chat_id: str,
2245
+ path: Optional[Union[str, Path]] = None,
2246
+ file_id: Optional[str] = None,
2247
+ text: Optional[str] = None,
2248
+ file_name: Optional[str] = None,
2249
+ inline_keypad: Optional[Dict[str, Any]] = None,
2250
+ chat_keypad: Optional[Dict[str, Any]] = None,
2251
+ reply_to_message_id: Optional[str] = None,
2252
+ disable_notification: bool = False,
2253
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
2254
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
2255
+ ) -> Dict[str, Any]:
2256
+ valid_extensions = {"gif"}
2257
+ extension = "gif"
2258
+ if path:
2259
+ path_str = str(path)
2260
+ if path_str.startswith("http://") or path_str.startswith("https://"):
2261
+ parsed = urlparse(path_str)
2262
+ base_name = os.path.basename(parsed.path)
2263
+ else:
2264
+ base_name = os.path.basename(path_str)
2265
+ name, ext = os.path.splitext(base_name)
2266
+
2267
+ if file_name is None or not file_name.strip():
2268
+ file_name = name or "gif"
2269
+ ext = ext.lower().replace(".", "")
2270
+ if ext in valid_extensions:
2271
+ extension = ext
2272
+ else:
2273
+ if file_name is None:
2274
+ file_name = "gif"
2275
+ return await self._send_file_generic(
2276
+ "File",
2277
+ chat_id,
2278
+ path,
2279
+ file_id,
2280
+ text,
2281
+ f"{file_name}.{extension}",
2282
+ inline_keypad,
2283
+ chat_keypad,
2284
+ reply_to_message_id,
2285
+ disable_notification,
2286
+ chat_keypad_type,
2287
+ parse_mode=parse_mode
2288
+ )
2289
+
2290
+ async def get_avatar_me(self, save_as: str = None) -> str:
2291
+ session = None
2292
+ try:
2293
+ me_info = await self.get_me()
2294
+ avatar = me_info.get('data', {}).get('bot', {}).get('avatar', {})
2295
+ file_id = avatar.get('file_id')
2296
+ if not file_id:
2297
+ return "null"
2298
+
2299
+ file_info = await self.get_url_file(file_id)
2300
+ url = file_info.get("download_url") if isinstance(file_info, dict) else file_info
2301
+
2302
+ if save_as:
2303
+ session = aiohttp.ClientSession()
2304
+ async with session.get(url) as resp:
2305
+ if resp.status == 200:
2306
+ content = await resp.read()
2307
+ with open(save_as, "wb") as f:
2308
+ f.write(content)
2309
+
2310
+ return url
2311
+ except Exception as e:
2312
+ print(f"[get_avatar_me] Error: {e}")
2313
+ return "null"
2314
+ finally:
2315
+ if session and not session.closed:
2316
+ await session.close()
2317
+
2318
+ async def get_name(self, chat_id: str) -> str:
2319
+ try:
2320
+ chat = await self.get_chat(chat_id)
2321
+ chat_info = chat.get("data", {}).get("chat", {})
2322
+ chat_type = chat_info.get("chat_type", "").lower()
2323
+ if chat_type == "user":
2324
+ first_name = chat_info.get("first_name", "")
2325
+ last_name = chat_info.get("last_name", "")
2326
+ full_name = f"{first_name} {last_name}".strip()
2327
+ return full_name if full_name else "null"
2328
+ elif chat_type in ["group", "channel"]:
2329
+ title = chat_info.get("title", "")
2330
+ return title if title else "null"
2331
+ else:return "null"
2332
+ except Exception:return "null"
2333
+ async def get_username(self, chat_id: str) -> str:
2334
+ chat_info = await self.get_chat(chat_id)
2335
+ return chat_info.get("data", {}).get("chat", {}).get("username", "None")
2336
+ async def send_bulk_message(
2337
+ self,
2338
+ chat_ids: List[str],
2339
+ text: str,
2340
+ concurrency: int = 5,
2341
+ delay_between: float = 0.0,
2342
+ log_errors: bool = True,
2343
+ **kwargs
2344
+ ) -> Dict[str, Optional[Dict]]:
2345
+ if not chat_ids:return {}
2346
+ semaphore = asyncio.Semaphore(concurrency)
2347
+ results: Dict[str, Optional[Dict]] = {}
2348
+ async def _send(chat_id: str):
2349
+ async with semaphore:
2350
+ try:
2351
+ res = await self.send_message(chat_id, text, **kwargs)
2352
+ results[chat_id] = res
2353
+ except Exception as e:
2354
+ results[chat_id] = None
2355
+ if log_errors:print(f"[send_bulk_message] Error {chat_id} : {e}")
2356
+ if delay_between > 0:await asyncio.sleep(delay_between)
2357
+ await asyncio.gather(*[_send(cid) for cid in chat_ids])
2358
+ return results
2359
+ async def delete_bulk_message(self, chat_id: str, message_ids: list[str]):
2360
+ tasks = [self.delete_message(chat_id, mid) for mid in message_ids]
2361
+ return await asyncio.gather(*tasks, return_exceptions=True)
2362
+ async def edit_bulk_message(self, chat_id: str, messages: dict[str, str]):
2363
+ tasks = [self.edit_message_text(chat_id, mid, new_text) for mid, new_text in messages.items()]
2364
+ return await asyncio.gather(*tasks, return_exceptions=True)
2365
+ async def send_scheduled_message(self, chat_id: str, text: str, delay: int, **kwargs):
2366
+ await asyncio.sleep(delay)
2367
+ return await self.send_message(chat_id, text, **kwargs)
2368
+ async def disable_inline_keyboard(
2369
+ self,
2370
+ chat_id: str,
2371
+ message_id: str,
2372
+ text: Optional[str] = "~",
2373
+ delay: float = 5.0,
2374
+ ) -> Dict[str, any]:
2375
+ if text is not None:await self.edit_inline_keypad(chat_id, message_id, inline_keypad={}, text=text)
2376
+ if delay > 0:
2377
+ await asyncio.sleep(delay)
2378
+ response = await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
2379
+ return response
2380
+ else:return await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
2381
+ async def get_chat_admins(self, chat_id: str) -> Dict[str, Any]:
2382
+ return await self._post("getChatAdmins", {"chat_id": chat_id})
2383
+ async def get_chat_members(self, chat_id: str, start_id: str = "") -> Dict[str, Any]:
2384
+ return await self._post("getChatMembers", {"chat_id": chat_id, "start_id": start_id})
2385
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
2386
+ return await self._post("getChatInfo", {"chat_id": chat_id})
2387
+ async def set_chat_title(self, chat_id: str, title: str) -> Dict[str, Any]:
2388
+ return await self._post("editChatTitle", {"chat_id": chat_id, "title": title})
2389
+ async def set_chat_description(self, chat_id: str, description: str) -> Dict[str, Any]:
2390
+ return await self._post("editChatDescription", {"chat_id": chat_id, "description": description})
2391
+ async def set_chat_photo(self, chat_id: str, file_id: str) -> Dict[str, Any]:
2392
+ return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": file_id})
2393
+ async def remove_chat_photo(self, chat_id: str) -> Dict[str, Any]:
2394
+ return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": "Remove"})
2395
+ async def add_member_chat(self, chat_id: str, user_ids: list[str]) -> Dict[str, Any]:
2396
+ return await self._post("addChatMembers", {"chat_id": chat_id, "member_ids": user_ids})
2397
+ async def ban_member_chat(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2398
+ return await self._post("banChatMember", {"chat_id": chat_id, "member_id": user_id})
2399
+ async def unban_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2400
+ return await self._post("unbanChatMember", {"chat_id": chat_id, "member_id": user_id})
2401
+ async def restrict_chat_member(self, chat_id: str, user_id: str, until: int = 0) -> Dict[str, Any]:
2402
+ return await self._post("restrictChatMember", {"chat_id": chat_id, "member_id": user_id, "until_date": until})
2403
+ async def get_chat_member(self, chat_id: str, user_id: str):
2404
+ return await self._post("getChatMember", {"chat_id": chat_id, "user_id": user_id})
2405
+ async def get_admin_chat(self, chat_id: str):
2406
+ return await self._post("getChatAdministrators", {"chat_id": chat_id})
2407
+ async def get_chat_member_count(self, chat_id: str):
2408
+ return await self._post("getChatMemberCount", {"chat_id": chat_id})
2409
+ async def ban_chat_member(self, chat_id: str, user_id: str):
2410
+ return await self._post("banChatMember", {"chat_id": chat_id, "user_id": user_id})
2411
+ async def promote_chat_member(self, chat_id: str, user_id: str, rights: dict) -> Dict[str, Any]:
2412
+ return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": rights})
2413
+ async def demote_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2414
+ return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": {}})
2415
+ async def pin_chat_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2416
+ return await self._post("pinChatMessage", {"chat_id": chat_id, "message_id": message_id})
2417
+ async def unpin_chat_message(self, chat_id: str, message_id: str = "") -> Dict[str, Any]:
2418
+ return await self._post("unpinChatMessage", {"chat_id": chat_id, "message_id": message_id})
2419
+ async def export_chat_invite_link(self, chat_id: str) -> Dict[str, Any]:
2420
+ return await self._post("exportChatInviteLink", {"chat_id": chat_id})
2421
+ async def revoke_chat_invite_link(self, chat_id: str, link: str) -> Dict[str, Any]:
2422
+ return await self._post("revokeChatInviteLink", {"chat_id": chat_id, "invite_link": link})
2423
+ async def create_group(self, title: str, user_ids: list[str]) -> Dict[str, Any]:
2424
+ return await self._post("createGroup", {"title": title, "user_ids": user_ids})
2425
+ async def create_channel(self, title: str, description: str = "") -> Dict[str, Any]:
2426
+ return await self._post("createChannel", {"title": title, "description": description})
2427
+ async def leave_chat(self, chat_id: str) -> Dict[str, Any]:
2428
+ return await self._post("leaveChat", {"chat_id": chat_id})
2429
+ async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
2430
+ return await self._post("forwardMessage", {"from_chat_id": from_chat_id, "message_id": message_id, "to_chat_id": to_chat_id, "disable_notification": disable_notification})
2431
+ async def edit_message_text(self, chat_id: str, message_id: str, text: str, parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2432
+ payload = {
2433
+ "chat_id": chat_id,
2434
+ "message_id": message_id,
2435
+ "text": text,
2436
+ }
2437
+ parse_mode_to_use = parse_mode or self.parse_mode
2438
+ if text:
2439
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
2440
+ payload["text"] = text
2441
+ if metadata:
2442
+ payload["metadata"] = metadata
2443
+ return await self._post("editMessageText", payload)
2444
+ async def edit_inline_keypad(self,chat_id: str,message_id: str,inline_keypad: Dict[str, Any],text: str = None) -> Dict[str, Any]:
2445
+ if text is not None:await self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
2446
+ return await self._post("editMessageKeypad", {"chat_id": chat_id,"message_id": message_id,"inline_keypad": inline_keypad})
2447
+ async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2448
+ return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
2449
+ async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
2450
+ return await self._post("setCommands", {"bot_commands": bot_commands})
2451
+ async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
2452
+ return await self._post("updateBotEndpoints", {"url": url, "type": type})
2453
+ async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
2454
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Remove"})
2455
+ async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
2456
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
2457
+ async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str,inline_keypad: Optional[Dict[str, Any]] = None,chat_keypad: Optional[Dict[str, Any]] = None,chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = None,) -> Dict[str, Any]:
2458
+ return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number,"inline_keypad": inline_keypad,"chat_keypad": chat_keypad,"chat_keypad_type": chat_keypad_type})
2459
+ async def get_chat(self, chat_id: str) -> Dict[str, Any]:
2460
+ return await self._post("getChat", {"chat_id": chat_id})
2461
+
2462
+ def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
2463
+ client = self._get_client()
2464
+ return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
2465
+ async def send_poll(
2466
+ self,
2467
+ chat_id: str,
2468
+ question: str,
2469
+ options: List[str],
2470
+ type: Literal["Regular", "Quiz"] = "Regular",
2471
+ allows_multiple_answers: bool = False,
2472
+ is_anonymous: bool = True,
2473
+ correct_option_index: Optional[int] = None,
2474
+ hint: Optional[str] = None,
2475
+ reply_to_message_id: Optional[str] = None,
2476
+ disable_notification: bool = False,
2477
+ inline_keypad: Optional[Dict[str, Any]] = None,
2478
+ chat_keypad: Optional[Dict[str, Any]] = None,
2479
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = None,
2480
+ ) -> AttrDict:
2481
+
2482
+ payload = {
2483
+ "chat_id": chat_id,
2484
+ "question": question,
2485
+ "options": options,
2486
+ "type": type,
2487
+ "allows_multiple_answers": allows_multiple_answers,
2488
+ "is_anonymous": is_anonymous,
2489
+ "correct_option_index": correct_option_index,
2490
+ "explanation": hint,
2491
+ "reply_to_message_id": reply_to_message_id,
2492
+ "disable_notification": disable_notification,
2493
+ "inline_keypad": inline_keypad,
2494
+ "chat_keypad": chat_keypad,
2495
+ "chat_keypad_type": chat_keypad_type,
2496
+ }
2497
+ payload = {k: v for k, v in payload.items() if v is not None or (k in ["is_anonymous", "disable_notification"] and v is False)}
2498
+ return await self._post("sendPoll", payload)
2499
+
2500
+ async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
2501
+ client = self._get_client()
2502
+ if chat_id:
2503
+ chat_info_data = await self.get_chat(chat_id)
2504
+ chat_info = chat_info_data.get('data', {}).get('chat', {})
2505
+ username = chat_info.get('username')
2506
+ first_name = chat_info.get("first_name", "")
2507
+ if username:
2508
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
2509
+ members = result.get('in_chat_members', [])
2510
+ return any(m.get('username') == username for m in members)
2511
+ elif first_name:
2512
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=first_name)
2513
+ members = result.get('in_chat_members', [])
2514
+ return any(m.get('first_name') == first_name for m in members)
2515
+ return False