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