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/api.py ADDED
@@ -0,0 +1,1723 @@
1
+ import requests
2
+ from typing import List, Optional, Dict, Any, Literal
3
+ from .exceptions import APIRequestError
4
+ from .adaptorrubka import Client as Client_get
5
+ from .logger import logger
6
+ from . import filters
7
+ from . import helpers
8
+ from typing import Callable
9
+ from .context import Message,InlineMessage
10
+ from typing import Optional, Union, Literal, Dict, Any
11
+ from pathlib import Path
12
+ import time
13
+ import datetime
14
+ import tempfile
15
+ from tqdm import tqdm
16
+ import os
17
+ API_URL = "https://botapi.rubika.ir/v3"
18
+ import mimetypes
19
+ import re
20
+ import sys
21
+ import subprocess
22
+ class InvalidTokenError(Exception):pass
23
+ def install_package(package_name):
24
+ try:
25
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
26
+ return True
27
+ except Exception:return False
28
+
29
+ def get_importlib_metadata():
30
+ try:
31
+ from importlib.metadata import version, PackageNotFoundError
32
+ return version, PackageNotFoundError
33
+ except ImportError:
34
+ if install_package("importlib-metadata"):
35
+ try:
36
+ from importlib_metadata import version, PackageNotFoundError
37
+ return version, PackageNotFoundError
38
+ except ImportError:
39
+ return None, None
40
+ return None, None
41
+
42
+ version, PackageNotFoundError = get_importlib_metadata()
43
+ def get_installed_version(package_name: str) -> str:
44
+ if version is None:return "unknown"
45
+ try:
46
+ return version(package_name)
47
+ except PackageNotFoundError:
48
+ return None
49
+ def get_latest_version(package_name: str) -> str:
50
+ url = f"https://pypi.org/pypi/{package_name}/json"
51
+ try:
52
+ resp = requests.get(url, timeout=5)
53
+ resp.raise_for_status()
54
+ data = resp.json()
55
+ return data["info"]["version"]
56
+ except Exception:return None
57
+
58
+
59
+ def show_last_six_words(text):
60
+ text = text.strip()
61
+ return text[-6:]
62
+ import requests
63
+ from pathlib import Path
64
+ from typing import Union, Optional, Dict, Any, Literal
65
+ import tempfile
66
+ import os
67
+ class Robot:
68
+ """
69
+ Main class to interact with Rubika Bot API.
70
+ Initialized with bot token.
71
+ """
72
+
73
+ 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):
74
+ """
75
+ راه‌اندازی اولیه ربات روبیکا و پیکربندی پارامترهای پایه.
76
+
77
+ Parameters:
78
+ token (str): توکن اصلی ربات برای احراز هویت با Rubika Bot API. این مقدار الزامی است.
79
+ session_name (str, optional): نام نشست دلخواه برای شناسایی یا جداسازی کلاینت‌ها. پیش‌فرض None.
80
+ auth (str, optional): مقدار احراز هویت اضافی برای اتصال به کلاینت سفارشی. پیش‌فرض None.
81
+ Key (str, optional): کلید اضافی برای رمزنگاری یا تأیید. در صورت نیاز استفاده می‌شود. پیش‌فرض None.
82
+ platform (str, optional): پلتفرم اجرایی مورد نظر (مثل "web" یا "android"). پیش‌فرض "web".
83
+ timeout (int, optional): مدت‌زمان تایم‌اوت برای درخواست‌های HTTP بر حسب ثانیه. پیش‌فرض 10 ثانیه.
84
+
85
+ توضیحات:
86
+ - این تابع یک شیء Session از requests می‌سازد برای استفاده در تمام درخواست‌ها.
87
+ - هندلرهای مختلف مانند پیام، دکمه، کوئری اینلاین و ... در این مرحله مقداردهی اولیه می‌شوند.
88
+ - لیست `self._callback_handlers` برای مدیریت چندین callback مختلف الزامی است.
89
+
90
+ مثال:
91
+ >>> bot = Robot(token="BOT_TOKEN", platform="android", timeout=15)
92
+ """
93
+ self.token = token
94
+ self._inline_query_handlers = []
95
+ self._inline_query_handlers: List[dict] = []
96
+ self.timeout = timeout
97
+ self.auth = auth
98
+ self.show_progress = show_progress
99
+ self.session_name = session_name
100
+ self.Key = Key
101
+
102
+ self.platform = platform
103
+ self.web_hook = web_hook
104
+ self.hook = web_hook
105
+ self._offset_id = None
106
+ self.session = requests.Session()
107
+ self.sessions: Dict[str, Dict[str, Any]] = {}
108
+ self._callback_handler = None
109
+ self._message_handler = None
110
+ self._inline_query_handler = None
111
+ self._message_handlers: List[dict] = []
112
+ self._callback_handlers = None
113
+ self._callback_handlers = []
114
+ self.geteToken()
115
+ if web_hook:
116
+ try:
117
+ json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
118
+ for endpoint_type in [
119
+ "ReceiveUpdate",
120
+ "ReceiveInlineMessage",
121
+ "ReceiveQuery",
122
+ "GetSelectionItem",
123
+ "SearchSelectionItems"
124
+ ]:
125
+ print(self.update_bot_endpoint(self.web_hook, endpoint_type))
126
+ self.web_hook = json_url
127
+ except Exception as e:
128
+ logger.error(f"Failed to set webhook from {web_hook}: {e}")
129
+ else:
130
+ self.web_hook = self.hook
131
+
132
+
133
+
134
+ logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
135
+ def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
136
+ url = f"{API_URL}/{self.token}/{method}"
137
+ try:
138
+ response = self.session.post(url, json=data, timeout=self.timeout)
139
+ response.raise_for_status()
140
+ try:
141
+ json_resp = response.json()
142
+ except ValueError:
143
+ logger.error(f"Invalid JSON response from {method}: {response.text}")
144
+ raise APIRequestError(f"Invalid JSON response: {response.text}")
145
+ if method != "getUpdates":logger.debug(f"API Response from {method}: {json_resp}")
146
+
147
+ return json_resp
148
+ except requests.RequestException as e:
149
+ logger.error(f"API request failed: {e}")
150
+ raise APIRequestError(f"API request failed: {e}") from e
151
+
152
+
153
+ def get_me(self) -> Dict[str, Any]:
154
+ """Get info about the bot itself."""
155
+ return self._post("getMe", {})
156
+ def geteToken(self):
157
+ """b"""
158
+ if self.get_me()['status'] != "OK":
159
+ raise InvalidTokenError("The provided bot token is invalid or expired.")
160
+ def on_message_private(
161
+ self,
162
+ chat_id: Optional[Union[str, List[str]]] = None,
163
+ commands: Optional[List[str]] = None,
164
+ filters: Optional[Callable[[Message], bool]] = None,
165
+ sender_id: Optional[Union[str, List[str]]] = None,
166
+ sender_type: Optional[str] = None,
167
+ allow_forwarded: bool = True,
168
+ allow_files: bool = True,
169
+ allow_stickers: bool = True,
170
+ allow_polls: bool = True,
171
+ allow_contacts: bool = True,
172
+ allow_locations: bool = True,
173
+ min_text_length: Optional[int] = None,
174
+ max_text_length: Optional[int] = None,
175
+ contains: Optional[str] = None,
176
+ startswith: Optional[str] = None,
177
+ endswith: Optional[str] = None,
178
+ case_sensitive: bool = False
179
+ ):
180
+ """
181
+ Sync decorator for handling only private messages with extended filters.
182
+ """
183
+
184
+ def decorator(func: Callable[[Any, Message], None]):
185
+ def wrapper(bot, message: Message):
186
+
187
+ if not message.is_private:
188
+ return
189
+
190
+
191
+ if chat_id:
192
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
193
+ return
194
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
195
+ return
196
+
197
+
198
+ if sender_id:
199
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
200
+ return
201
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
202
+ return
203
+
204
+
205
+ if sender_type and message.sender_type != sender_type:
206
+ return
207
+
208
+
209
+ if not allow_forwarded and message.forwarded_from:
210
+ return
211
+
212
+
213
+ if not allow_files and message.file:
214
+ return
215
+ if not allow_stickers and message.sticker:
216
+ return
217
+ if not allow_polls and message.poll:
218
+ return
219
+ if not allow_contacts and message.contact_message:
220
+ return
221
+ if not allow_locations and (message.location or message.live_location):
222
+ return
223
+
224
+
225
+ if message.text:
226
+ text = message.text if case_sensitive else message.text.lower()
227
+ if min_text_length and len(message.text) < min_text_length:
228
+ return
229
+ if max_text_length and len(message.text) > max_text_length:
230
+ return
231
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
232
+ return
233
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
234
+ return
235
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
236
+ return
237
+
238
+
239
+ if commands:
240
+ if not message.text:
241
+ return
242
+ parts = message.text.strip().split()
243
+ cmd = parts[0].lstrip("/")
244
+ if cmd not in commands:
245
+ return
246
+ message.args = parts[1:]
247
+
248
+
249
+ if filters and not filters(message):
250
+ return
251
+
252
+ return func(bot, message)
253
+
254
+ self._message_handlers.append({
255
+ "func": wrapper,
256
+ "filters": filters,
257
+ "commands": commands,
258
+ "chat_id": chat_id,
259
+ "private_only": True,
260
+ "sender_id": sender_id,
261
+ "sender_type": sender_type
262
+ })
263
+ return wrapper
264
+
265
+ return decorator
266
+ def on_message_channel(
267
+ self,
268
+ chat_id: Optional[Union[str, List[str]]] = None,
269
+ commands: Optional[List[str]] = None,
270
+ filters: Optional[Callable[[Message], bool]] = None,
271
+ sender_id: Optional[Union[str, List[str]]] = None,
272
+ sender_type: Optional[str] = None,
273
+ allow_forwarded: bool = True,
274
+ allow_files: bool = True,
275
+ allow_stickers: bool = True,
276
+ allow_polls: bool = True,
277
+ allow_contacts: bool = True,
278
+ allow_locations: bool = True,
279
+ min_text_length: Optional[int] = None,
280
+ max_text_length: Optional[int] = None,
281
+ contains: Optional[str] = None,
282
+ startswith: Optional[str] = None,
283
+ endswith: Optional[str] = None,
284
+ case_sensitive: bool = False
285
+ ):
286
+ """
287
+ Sync decorator for handling only private messages with extended filters.
288
+ """
289
+
290
+ def decorator(func: Callable[[Any, Message], None]):
291
+ def wrapper(bot, message: Message):
292
+
293
+ if not message.is_channel:
294
+ return
295
+
296
+
297
+ if chat_id:
298
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
299
+ return
300
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
301
+ return
302
+
303
+
304
+ if sender_id:
305
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
306
+ return
307
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
308
+ return
309
+
310
+
311
+ if sender_type and message.sender_type != sender_type:
312
+ return
313
+
314
+
315
+ if not allow_forwarded and message.forwarded_from:
316
+ return
317
+
318
+
319
+ if not allow_files and message.file:
320
+ return
321
+ if not allow_stickers and message.sticker:
322
+ return
323
+ if not allow_polls and message.poll:
324
+ return
325
+ if not allow_contacts and message.contact_message:
326
+ return
327
+ if not allow_locations and (message.location or message.live_location):
328
+ return
329
+
330
+
331
+ if message.text:
332
+ text = message.text if case_sensitive else message.text.lower()
333
+ if min_text_length and len(message.text) < min_text_length:
334
+ return
335
+ if max_text_length and len(message.text) > max_text_length:
336
+ return
337
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
338
+ return
339
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
340
+ return
341
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
342
+ return
343
+
344
+
345
+ if commands:
346
+ if not message.text:
347
+ return
348
+ parts = message.text.strip().split()
349
+ cmd = parts[0].lstrip("/")
350
+ if cmd not in commands:
351
+ return
352
+ message.args = parts[1:]
353
+
354
+
355
+ if filters and not filters(message):
356
+ return
357
+
358
+ return func(bot, message)
359
+
360
+ self._message_handlers.append({
361
+ "func": wrapper,
362
+ "filters": filters,
363
+ "commands": commands,
364
+ "chat_id": chat_id,
365
+ "private_only": True,
366
+ "sender_id": sender_id,
367
+ "sender_type": sender_type
368
+ })
369
+ return wrapper
370
+
371
+ return decorator
372
+
373
+ def on_message_group(
374
+ self,
375
+ chat_id: Optional[Union[str, List[str]]] = None,
376
+ commands: Optional[List[str]] = None,
377
+ filters: Optional[Callable[[Message], bool]] = None,
378
+ sender_id: Optional[Union[str, List[str]]] = None,
379
+ sender_type: Optional[str] = None,
380
+ allow_forwarded: bool = True,
381
+ allow_files: bool = True,
382
+ allow_stickers: bool = True,
383
+ allow_polls: bool = True,
384
+ allow_contacts: bool = True,
385
+ allow_locations: bool = True,
386
+ min_text_length: Optional[int] = None,
387
+ max_text_length: Optional[int] = None,
388
+ contains: Optional[str] = None,
389
+ startswith: Optional[str] = None,
390
+ endswith: Optional[str] = None,
391
+ case_sensitive: bool = False
392
+ ):
393
+ """
394
+ Sync decorator for handling only group messages with extended filters.
395
+ """
396
+
397
+ def decorator(func: Callable[[Any, Message], None]):
398
+ def wrapper(bot, message: Message):
399
+
400
+ if not message.is_group:
401
+ return
402
+
403
+
404
+ if chat_id:
405
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
406
+ return
407
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
408
+ return
409
+
410
+
411
+ if sender_id:
412
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
413
+ return
414
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
415
+ return
416
+
417
+
418
+ if sender_type and message.sender_type != sender_type:
419
+ return
420
+
421
+
422
+ if not allow_forwarded and message.forwarded_from:
423
+ return
424
+
425
+
426
+ if not allow_files and message.file:
427
+ return
428
+ if not allow_stickers and message.sticker:
429
+ return
430
+ if not allow_polls and message.poll:
431
+ return
432
+ if not allow_contacts and message.contact_message:
433
+ return
434
+ if not allow_locations and (message.location or message.live_location):
435
+ return
436
+
437
+
438
+ if message.text:
439
+ text = message.text if case_sensitive else message.text.lower()
440
+ if min_text_length and len(message.text) < min_text_length:
441
+ return
442
+ if max_text_length and len(message.text) > max_text_length:
443
+ return
444
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
445
+ return
446
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
447
+ return
448
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
449
+ return
450
+
451
+
452
+ if commands:
453
+ if not message.text:
454
+ return
455
+ parts = message.text.strip().split()
456
+ cmd = parts[0].lstrip("/")
457
+ if cmd not in commands:
458
+ return
459
+ message.args = parts[1:]
460
+
461
+
462
+ if filters and not filters(message):
463
+ return
464
+
465
+ return func(bot, message)
466
+
467
+ self._message_handlers.append({
468
+ "func": wrapper,
469
+ "filters": filters,
470
+ "commands": commands,
471
+ "chat_id": chat_id,
472
+ "group_only": True,
473
+ "sender_id": sender_id,
474
+ "sender_type": sender_type
475
+ })
476
+ return wrapper
477
+
478
+ return decorator
479
+
480
+ def on_message(
481
+ self,
482
+ filters: Optional[Callable[[Message], bool]] = None,
483
+ commands: Optional[List[str]] = None
484
+ ):
485
+ def decorator(func: Callable[[Any, Message], None]):
486
+ def wrapper(bot, message: Message):
487
+ if filters and not filters(message):
488
+ return
489
+ if commands:
490
+ if not getattr(message, "is_command", False):
491
+ return
492
+ cmd = message.text.split()[0].lstrip("/") if message.text else ""
493
+ if cmd not in commands:
494
+ return
495
+ return func(bot, message)
496
+ self._message_handlers.append({
497
+ "func": wrapper,
498
+ "filters": filters,
499
+ "commands": commands
500
+ })
501
+ return wrapper
502
+ return decorator
503
+ def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
504
+ def decorator(func: Callable[[Any, Message], None]):
505
+ def wrapper(bot, message: Message):
506
+ if not message.file:
507
+ return
508
+ if filters and not filters(message):
509
+ return
510
+ return func(bot, message)
511
+
512
+ self._message_handlers.append({
513
+ "func": wrapper,
514
+ "filters": filters,
515
+ "file_only": True,
516
+ "commands":commands
517
+ })
518
+ return wrapper
519
+ return decorator
520
+
521
+ def on_message_forwarded(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
522
+ def decorator(func: Callable[[Any, Message], None]):
523
+ def wrapper(bot, message: Message):
524
+ if not message.is_forwarded:
525
+ return
526
+ if filters and not filters(message):
527
+ return
528
+ return func(bot, message)
529
+
530
+ self._message_handlers.append({
531
+ "func": wrapper,
532
+ "filters": filters,
533
+ "forwarded_only": True,
534
+ "commands":commands
535
+ })
536
+ return wrapper
537
+ return decorator
538
+
539
+ def on_message_reply(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
540
+ def decorator(func: Callable[[Any, Message], None]):
541
+ def wrapper(bot, message: Message):
542
+ if not message.is_reply:
543
+ return
544
+ if filters and not filters(message):
545
+ return
546
+ return func(bot, message)
547
+
548
+ self._message_handlers.append({
549
+ "func": wrapper,
550
+ "filters": filters,
551
+ "reply_only": True,
552
+ "commands":commands
553
+ })
554
+ return wrapper
555
+ return decorator
556
+
557
+ def on_message_text(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
558
+ def decorator(func: Callable[[Any, Message], None]):
559
+ def wrapper(bot, message: Message):
560
+ if not message.text:
561
+ return
562
+ if filters and not filters(message):
563
+ return
564
+ return func(bot, message)
565
+
566
+ self._message_handlers.append({
567
+ "func": wrapper,
568
+ "filters": filters,
569
+ "text_only": True,
570
+ "commands":commands
571
+ })
572
+ return wrapper
573
+ return decorator
574
+
575
+ def on_message_media(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
576
+ def decorator(func: Callable[[Any, Message], None]):
577
+ def wrapper(bot, message: Message):
578
+ if not message.is_media:
579
+ return
580
+ if filters and not filters(message):
581
+ return
582
+ return func(bot, message)
583
+
584
+ self._message_handlers.append({
585
+ "func": wrapper,
586
+ "filters": filters,
587
+ "media_only": True,
588
+ "commands":commands
589
+ })
590
+ return wrapper
591
+ return decorator
592
+
593
+ def on_message_sticker(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
594
+ def decorator(func: Callable[[Any, Message], None]):
595
+ def wrapper(bot, message: Message):
596
+ if not message.sticker:
597
+ return
598
+ if filters and not filters(message):
599
+ return
600
+ return func(bot, message)
601
+
602
+ self._message_handlers.append({
603
+ "func": wrapper,
604
+ "filters": filters,
605
+ "sticker_only": True,
606
+ "commands":commands
607
+ })
608
+ return wrapper
609
+ return decorator
610
+
611
+ def on_message_contact(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
612
+ def decorator(func: Callable[[Any, Message], None]):
613
+ def wrapper(bot, message: Message):
614
+ if not message.is_contact:
615
+ return
616
+ if filters and not filters(message):
617
+ return
618
+ return func(bot, message)
619
+
620
+ self._message_handlers.append({
621
+ "func": wrapper,
622
+ "filters": filters,
623
+ "contact_only": True,
624
+ "commands":commands
625
+ })
626
+ return wrapper
627
+ return decorator
628
+
629
+ def on_message_location(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
630
+ def decorator(func: Callable[[Any, Message], None]):
631
+ def wrapper(bot, message: Message):
632
+ if not message.is_location:
633
+ return
634
+ if filters and not filters(message):
635
+ return
636
+ return func(bot, message)
637
+
638
+ self._message_handlers.append({
639
+ "func": wrapper,
640
+ "filters": filters,
641
+ "location_only": True,
642
+ "commands":commands
643
+ })
644
+ return wrapper
645
+ return decorator
646
+
647
+ def on_message_poll(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
648
+ def decorator(func: Callable[[Any, Message], None]):
649
+ def wrapper(bot, message: Message):
650
+ if not message.is_poll:
651
+ return
652
+ if filters and not filters(message):
653
+ return
654
+ return func(bot, message)
655
+
656
+ self._message_handlers.append({
657
+ "func": wrapper,
658
+ "filters": filters,
659
+ "poll_only": True,
660
+ "commands":commands
661
+ })
662
+ return wrapper
663
+ return decorator
664
+
665
+ def message_handler(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
666
+ def decorator(func: Callable[[Any, Message], None]):
667
+ self._message_handlers.append({
668
+ "func": func,
669
+ "filters": filters,
670
+ "commands": commands
671
+ })
672
+ return func
673
+ return decorator
674
+ def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
675
+ def decorator(func: Callable[[Any, Message], None]):
676
+ self._message_handlers.append({
677
+ "func": func,
678
+ "filters": filters,
679
+ "commands": commands
680
+ })
681
+ return func
682
+ return decorator
683
+
684
+ def on_callback(self, button_id: Optional[str] = None):
685
+ def decorator(func: Callable[[Any, Message], None]):
686
+ if not hasattr(self, "_callback_handlers"):
687
+ self._callback_handlers = []
688
+ self._callback_handlers.append({
689
+ "func": func,
690
+ "button_id": button_id
691
+ })
692
+ return func
693
+ return decorator
694
+ def callback_query(self, button_id: Optional[str] = None):
695
+ def decorator(func: Callable[[Any, Message], None]):
696
+ if not hasattr(self, "_callback_handlers"):
697
+ self._callback_handlers = []
698
+ self._callback_handlers.append({
699
+ "func": func,
700
+ "button_id": button_id
701
+ })
702
+ return func
703
+ return decorator
704
+ def callback_query_handler(self, button_id: Optional[str] = None):
705
+ def decorator(func: Callable[[Any, Message], None]):
706
+ if not hasattr(self, "_callback_handlers"):
707
+ self._callback_handlers = []
708
+ self._callback_handlers.append({
709
+ "func": func,
710
+ "button_id": button_id
711
+ })
712
+ return func
713
+ return decorator
714
+ def _handle_inline_query(self, inline_message: InlineMessage):
715
+ aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
716
+
717
+ for handler in self._inline_query_handlers:
718
+ if handler["button_id"] is None or handler["button_id"] == aux_button_id:
719
+ try:
720
+ handler["func"](self, inline_message)
721
+ except Exception as e:
722
+ print(f"Error in inline query handler: {e}")
723
+
724
+ def on_inline_query(self, button_id: Optional[str] = None):
725
+ def decorator(func: Callable[[Any, InlineMessage], None]):
726
+ self._inline_query_handlers.append({
727
+ "func": func,
728
+ "button_id": button_id
729
+ })
730
+ return func
731
+ return decorator
732
+
733
+
734
+ def _process_update(self, update: dict):
735
+ import threading
736
+
737
+ if update.get("type") == "ReceiveQuery":
738
+ msg = update.get("inline_message", {})
739
+ context = InlineMessage(bot=self, raw_data=msg)
740
+
741
+
742
+ if hasattr(self, "_callback_handlers"):
743
+ for handler in self._callback_handlers:
744
+ cb_id = getattr(context.aux_data, "button_id", None)
745
+ if not handler["button_id"] or handler["button_id"] == cb_id:
746
+ threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
747
+
748
+
749
+ threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
750
+ return
751
+
752
+ if update.get("type") == "NewMessage":
753
+ msg = update.get("new_message", {})
754
+ try:
755
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:
756
+ return
757
+ except Exception:
758
+ return
759
+
760
+ context = Message(bot=self,
761
+ chat_id=update.get("chat_id"),
762
+ message_id=msg.get("message_id"),
763
+ sender_id=msg.get("sender_id"),
764
+ text=msg.get("text"),
765
+ raw_data=msg)
766
+
767
+ if context.aux_data and self._callback_handlers:
768
+ for handler in self._callback_handlers:
769
+ if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
770
+ threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
771
+ return
772
+
773
+ if self._message_handlers:
774
+ for handler in self._message_handlers:
775
+ if handler["commands"]:
776
+ if not context.text or not context.text.startswith("/"):
777
+ continue
778
+ parts = context.text.split()
779
+ cmd = parts[0][1:]
780
+ if cmd not in handler["commands"]:
781
+ continue
782
+ context.args = parts[1:]
783
+
784
+ if handler["filters"] and not handler["filters"](context):
785
+ continue
786
+
787
+ threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
788
+ continue
789
+
790
+ def get_updates(
791
+ self,
792
+ offset_id: Optional[str] = None,
793
+ limit: Optional[int] = None
794
+ ) -> Dict[str, Any]:
795
+ """Get updates."""
796
+ data = {}
797
+ if offset_id:
798
+ data["offset_id"] = offset_id
799
+ if limit:
800
+ data["limit"] = limit
801
+ return self._post("getUpdates", data)
802
+ def update_webhook(
803
+ self,
804
+ offset_id: Optional[str] = None,
805
+ limit: Optional[int] = None
806
+ ) -> Dict[str, Any]:
807
+ data = {}
808
+ if offset_id:
809
+ data["offset_id"] = offset_id
810
+ if limit:
811
+ data["limit"] = limit
812
+ return list(requests.get(self.web_hook).json())
813
+ def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
814
+ now = time.time()
815
+
816
+ expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
817
+ for mid in expired:
818
+ del self._processed_message_ids[mid]
819
+
820
+ if message_id in self._processed_message_ids:
821
+ return True
822
+
823
+ self._processed_message_ids[message_id] = now
824
+ return False
825
+
826
+
827
+ import time
828
+
829
+
830
+ import datetime
831
+
832
+
833
+ def run(
834
+ self,
835
+ debug=False,
836
+ sleep_time=0.1,
837
+ webhook_timeout=20,
838
+ update_limit=100,
839
+ retry_delay=5,
840
+ stop_on_error=False,
841
+ max_errors=None,
842
+ max_runtime=None,
843
+ allowed_update_types=None,
844
+ ignore_duplicate_messages=True,
845
+ skip_inline_queries=False,
846
+ skip_channel_posts=False,
847
+ skip_service_messages=False,
848
+ skip_edited_messages=False,
849
+ skip_bot_messages=False,
850
+ log_file=None,
851
+ print_exceptions=True,
852
+ error_handler=None,
853
+ shutdown_hook=None,
854
+ log_to_console=True,
855
+ custom_update_fetcher=None,
856
+ custom_update_processor=None,
857
+ message_filter=None,
858
+ notify_on_error=False,
859
+ notification_handler=None,
860
+ ):
861
+ import time
862
+ from typing import Dict
863
+ if debug:
864
+ print("[DEBUG] Bot started running server...")
865
+
866
+ self._processed_message_ids: Dict[str, float] = {}
867
+ error_count = 0
868
+ start_time = time.time()
869
+
870
+ try:
871
+ while True:
872
+ try:
873
+
874
+ if max_runtime and (time.time() - start_time > max_runtime):
875
+ if debug:
876
+ print("[DEBUG] Max runtime reached, stopping...")
877
+ break
878
+
879
+
880
+ if self.web_hook:
881
+ updates = custom_update_fetcher() if custom_update_fetcher else self.update_webhook()
882
+ if isinstance(updates, list):
883
+ for item in updates:
884
+ data = item.get("data", {})
885
+ received_at_str = item.get("received_at")
886
+
887
+ if received_at_str:
888
+ try:
889
+ received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
890
+ if time.time() - received_at_ts > webhook_timeout:
891
+ continue
892
+ except (ValueError, TypeError):
893
+ pass
894
+
895
+ update = data.get("update") or (
896
+ {"type": "ReceiveQuery", "inline_message": data.get("inline_message")}
897
+ if "inline_message" in data else None
898
+ )
899
+ if not update:
900
+ continue
901
+
902
+
903
+ if skip_inline_queries and update.get("type") == "ReceiveQuery":
904
+ continue
905
+ if skip_channel_posts and update.get("type") == "ChannelPost":
906
+ continue
907
+ if skip_service_messages and update.get("type") == "ServiceMessage":
908
+ continue
909
+ if skip_edited_messages and update.get("type") == "EditedMessage":
910
+ continue
911
+ if skip_bot_messages and update.get("from", {}).get("is_bot"):
912
+ continue
913
+ if allowed_update_types and update.get("type") not in allowed_update_types:
914
+ continue
915
+
916
+ message_id = (
917
+ update.get("new_message", {}).get("message_id")
918
+ if update.get("type") == "NewMessage"
919
+ else update.get("inline_message", {}).get("message_id")
920
+ if update.get("type") == "ReceiveQuery"
921
+ else update.get("message_id")
922
+ )
923
+
924
+ if message_id is not None:
925
+ message_id = str(message_id)
926
+
927
+ if message_id and (not ignore_duplicate_messages or not self._is_duplicate(received_at_str)):
928
+ if message_filter and not message_filter(update):
929
+ continue
930
+ if custom_update_processor:
931
+ custom_update_processor(update)
932
+ else:
933
+ self._process_update(update)
934
+ if message_id:
935
+ self._processed_message_ids[message_id] = time.time()
936
+
937
+
938
+ else:
939
+ updates = custom_update_fetcher() if custom_update_fetcher else self.get_updates(offset_id=self._offset_id, limit=update_limit)
940
+ if updates and updates.get("data"):
941
+ for update in updates["data"].get("updates", []):
942
+ if allowed_update_types and update.get("type") not in allowed_update_types:
943
+ continue
944
+
945
+ message_id = (
946
+ update.get("new_message", {}).get("message_id")
947
+ if update.get("type") == "NewMessage"
948
+ else update.get("inline_message", {}).get("message_id")
949
+ if update.get("type") == "ReceiveQuery"
950
+ else update.get("message_id")
951
+ )
952
+
953
+ if message_id is not None:
954
+ message_id = str(message_id)
955
+
956
+ if message_id and (not ignore_duplicate_messages or not self._is_duplicate(message_id)):
957
+ if message_filter and not message_filter(update):
958
+ continue
959
+ if custom_update_processor:
960
+ custom_update_processor(update)
961
+ else:
962
+ self._process_update(update)
963
+ if message_id:
964
+ self._processed_message_ids[message_id] = time.time()
965
+
966
+ self._offset_id = updates["data"].get("next_offset_id", self._offset_id)
967
+
968
+ if sleep_time:
969
+ time.sleep(sleep_time)
970
+
971
+ except Exception as e:
972
+ error_count += 1
973
+ if log_to_console:
974
+ print(f"Error in run loop: {e}")
975
+ if log_file:
976
+ with open(log_file, "a", encoding="utf-8") as f:
977
+ f.write(f"{datetime.datetime.now()} - ERROR: {e}\n")
978
+ if print_exceptions:
979
+ import traceback
980
+ traceback.print_exc()
981
+ if error_handler:
982
+ error_handler(e)
983
+ if notify_on_error and notification_handler:
984
+ notification_handler(e)
985
+
986
+ if max_errors and error_count >= max_errors and stop_on_error:
987
+ break
988
+
989
+ time.sleep(retry_delay)
990
+
991
+ finally:
992
+ if shutdown_hook:
993
+ shutdown_hook()
994
+ if debug:
995
+ print("Bot stopped and session closed.")
996
+
997
+ def send_message(
998
+ self,
999
+ chat_id: str,
1000
+ text: str,
1001
+ chat_keypad: Optional[Dict[str, Any]] = None,
1002
+ inline_keypad: Optional[Dict[str, Any]] = None,
1003
+ disable_notification: bool = False,
1004
+ reply_to_message_id: Optional[str] = None,
1005
+ chat_keypad_type: Optional[Literal["New", "Removed"]] = None,
1006
+ delete_after = None,
1007
+ parse_mode = None
1008
+ ) -> Dict[str, Any]:
1009
+ """
1010
+ Send a text message to a chat.
1011
+ """
1012
+ payload = {
1013
+ "chat_id": chat_id,
1014
+ "text": text,
1015
+ "disable_notification": disable_notification
1016
+ }
1017
+ if chat_keypad:
1018
+ payload["chat_keypad"] = chat_keypad
1019
+ if inline_keypad:
1020
+ payload["inline_keypad"] = inline_keypad
1021
+ if reply_to_message_id:
1022
+ payload["reply_to_message_id"] = reply_to_message_id
1023
+ if chat_keypad_type:
1024
+ payload["chat_keypad_type"] = chat_keypad_type
1025
+
1026
+ return self._post("sendMessage", payload)
1027
+
1028
+ def _get_client(self):
1029
+ if self.session_name:
1030
+ return Client_get(self.session_name,self.auth,self.Key,self.platform)
1031
+ else :
1032
+ return Client_get(show_last_six_words(self.token),self.auth,self.Key,self.platform)
1033
+ from typing import Union
1034
+
1035
+ def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
1036
+ client = self._get_client()
1037
+
1038
+ if chat_id:
1039
+ chat_info = self.get_chat(chat_id).get('data', {}).get('chat', {})
1040
+ username = chat_info.get('username')
1041
+ user_id = chat_info.get('user_id')
1042
+
1043
+ if username:
1044
+ members = self.get_all_member(channel_guid, search_text=username).get('in_chat_members', [])
1045
+ return any(m.get('username') == username for m in members)
1046
+
1047
+ elif user_id:
1048
+ member_guids = client.get_all_members(channel_guid, just_get_guids=True)
1049
+ return user_id in member_guids
1050
+
1051
+ return False
1052
+
1053
+ return False
1054
+
1055
+ def get_url_file(self,file_id):
1056
+ data = self._post("getFile", {'file_id': file_id})
1057
+ return data.get("data").get("download_url")
1058
+
1059
+
1060
+ def get_all_member(
1061
+ self,
1062
+ channel_guid: str,
1063
+ search_text: str = None,
1064
+ start_id: str = None,
1065
+ just_get_guids: bool = False
1066
+ ):
1067
+ client = self._get_client()
1068
+ return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
1069
+
1070
+ def send_poll(
1071
+ self,
1072
+ chat_id: str,
1073
+ question: str,
1074
+ options: List[str]
1075
+ ) -> Dict[str, Any]:
1076
+ """
1077
+ Send a poll to a chat.
1078
+ """
1079
+ return self._post("sendPoll", {
1080
+ "chat_id": chat_id,
1081
+ "question": question,
1082
+ "options": options
1083
+ })
1084
+
1085
+ def send_location(
1086
+ self,
1087
+ chat_id: str,
1088
+ latitude: str,
1089
+ longitude: str,
1090
+ disable_notification: bool = False,
1091
+ inline_keypad: Optional[Dict[str, Any]] = None,
1092
+ reply_to_message_id: Optional[str] = None,
1093
+ chat_keypad_type: Optional[Literal["New", "Removed"]] = None
1094
+ ) -> Dict[str, Any]:
1095
+ """
1096
+ Send a location to a chat.
1097
+ """
1098
+ payload = {
1099
+ "chat_id": chat_id,
1100
+ "latitude": latitude,
1101
+ "longitude": longitude,
1102
+ "disable_notification": disable_notification,
1103
+ "inline_keypad": inline_keypad,
1104
+ "reply_to_message_id": reply_to_message_id,
1105
+ "chat_keypad_type": chat_keypad_type
1106
+ }
1107
+ payload = {k: v for k, v in payload.items() if v is not None}
1108
+ return self._post("sendLocation", payload)
1109
+
1110
+ def send_contact(
1111
+ self,
1112
+ chat_id: str,
1113
+ first_name: str,
1114
+ last_name: str,
1115
+ phone_number: str
1116
+ ) -> Dict[str, Any]:
1117
+ """
1118
+ Send a contact to a chat.
1119
+ """
1120
+ return self._post("sendContact", {
1121
+ "chat_id": chat_id,
1122
+ "first_name": first_name,
1123
+ "last_name": last_name,
1124
+ "phone_number": phone_number
1125
+ })
1126
+ def download(self,file_id: str, save_as: str = None, chunk_size: int = 1024 * 512, timeout_sec: int = 60, verbose: bool = False):
1127
+ """
1128
+ Download a file from server using its file_id with chunked transfer,
1129
+ progress bar, file extension detection, custom filename, and timeout.
1130
+
1131
+ If save_as is not provided, filename will be extracted from
1132
+ Content-Disposition header or Content-Type header extension.
1133
+
1134
+ Parameters:
1135
+ file_id (str): The file ID to fetch the download URL.
1136
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
1137
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
1138
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
1139
+ verbose (bool, optional): Show progress messages. Default True.
1140
+
1141
+ Returns:
1142
+ bool: True if success, raises exceptions otherwise.
1143
+ """
1144
+ try:
1145
+ url = self.get_url_file(file_id)
1146
+ if not url:
1147
+ raise ValueError("Download URL not found in response.")
1148
+ except Exception as e:
1149
+ raise ValueError(f"Failed to get download URL: {e}")
1150
+
1151
+ try:
1152
+ with requests.get(url, stream=True, timeout=timeout_sec) as resp:
1153
+ if resp.status_code != 200:
1154
+ raise requests.HTTPError(f"Failed to download file. Status code: {resp.status_code}")
1155
+
1156
+ if not save_as:
1157
+ content_disp = resp.headers.get("Content-Disposition", "")
1158
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
1159
+ if match:
1160
+ save_as = match.group(1)
1161
+ else:
1162
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
1163
+ extension = mimetypes.guess_extension(content_type) or ".bin"
1164
+ save_as = f"{file_id}{extension}"
1165
+
1166
+ total_size = int(resp.headers.get("Content-Length", 0))
1167
+ progress = tqdm(total=total_size, unit="B", unit_scale=True)
1168
+
1169
+ with open(save_as, "wb") as f:
1170
+ for chunk in resp.iter_content(chunk_size=chunk_size):
1171
+ if chunk:
1172
+ f.write(chunk)
1173
+ progress.update(len(chunk))
1174
+
1175
+ progress.close()
1176
+ if verbose:
1177
+ print(f"File saved as: {save_as}")
1178
+
1179
+ return True
1180
+
1181
+ except Exception as e:
1182
+ raise RuntimeError(f"Download failed: {e}")
1183
+ def get_chat(self, chat_id: str) -> Dict[str, Any]:
1184
+ """Get chat info."""
1185
+ return self._post("getChat", {"chat_id": chat_id})
1186
+
1187
+ def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
1188
+ is_temp_file = False
1189
+
1190
+ if isinstance(path, str) and path.startswith("http"):
1191
+ response = requests.get(path)
1192
+ if response.status_code != 200:
1193
+ raise Exception(f"Failed to download file from URL ({response.status_code})")
1194
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
1195
+ temp_file.write(response.content)
1196
+ temp_file.close()
1197
+ path = temp_file.name
1198
+ is_temp_file = True
1199
+
1200
+ file_size = os.path.getsize(path)
1201
+
1202
+ with open(path, 'rb') as f:
1203
+ progress_bar = None
1204
+
1205
+ if self.show_progress:
1206
+ progress_bar = tqdm(
1207
+ total=file_size,
1208
+ unit='B',
1209
+ unit_scale=True,
1210
+ unit_divisor=1024,
1211
+ desc=f'Uploading : {name}',
1212
+ bar_format='{l_bar}{bar:100}{r_bar}',
1213
+ colour='cyan'
1214
+ )
1215
+
1216
+ class FileWithProgress:
1217
+ def __init__(self, file, progress):
1218
+ self.file = file
1219
+ self.progress = progress
1220
+
1221
+ def read(self, size=-1):
1222
+ data = self.file.read(size)
1223
+ if self.progress:
1224
+ self.progress.update(len(data))
1225
+ return data
1226
+
1227
+ def __getattr__(self, attr):
1228
+ return getattr(self.file, attr)
1229
+
1230
+ file_with_progress = FileWithProgress(f, progress_bar)
1231
+
1232
+ files = {
1233
+ 'file': (name, file_with_progress, 'application/octet-stream')
1234
+ }
1235
+
1236
+ response = requests.post(upload_url, files=files)
1237
+
1238
+ if progress_bar:
1239
+ progress_bar.close()
1240
+
1241
+ if is_temp_file:
1242
+ os.remove(path)
1243
+
1244
+ if response.status_code != 200:
1245
+ raise Exception(f"Upload failed ({response.status_code}): {response.text}")
1246
+
1247
+ data = response.json()
1248
+ return data.get('data', {}).get('file_id')
1249
+
1250
+ def send_button_join(
1251
+ self,
1252
+ chat_id,
1253
+ title_button : Union[str, list],
1254
+ username : Union[str, list],
1255
+ text,
1256
+ reply_to_message_id=None,
1257
+ id="None"
1258
+ ):
1259
+ from .button import InlineBuilder
1260
+ builder = InlineBuilder()
1261
+
1262
+
1263
+ if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1264
+ for t, u in zip(title_button, username):
1265
+ builder = builder.row(
1266
+ InlineBuilder().button_join_channel(
1267
+ text=t,
1268
+ id=id,
1269
+ username=u
1270
+ )
1271
+ )
1272
+
1273
+
1274
+ elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
1275
+ for u in username:
1276
+ builder = builder.row(
1277
+ InlineBuilder().button_join_channel(
1278
+ text=title_button,
1279
+ id=id,
1280
+ username=u
1281
+ )
1282
+ )
1283
+
1284
+
1285
+ else:
1286
+ builder = builder.row(
1287
+ InlineBuilder().button_join_channel(
1288
+ text=title_button,
1289
+ id=id,
1290
+ username=username
1291
+ )
1292
+ )
1293
+
1294
+ return self.send_message(
1295
+ chat_id=chat_id,
1296
+ text=text,
1297
+ inline_keypad=builder.build(),
1298
+ reply_to_message_id=reply_to_message_id
1299
+ )
1300
+
1301
+
1302
+ def send_button_url(
1303
+ self,
1304
+ chat_id,
1305
+ title_button : Union[str, list],
1306
+ url : Union[str, list],
1307
+ text,
1308
+ reply_to_message_id=None,
1309
+ id="None"
1310
+ ):
1311
+ from .button import InlineBuilder
1312
+ builder = InlineBuilder()
1313
+
1314
+
1315
+ if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
1316
+ for t, u in zip(title_button, url):
1317
+ builder = builder.row(
1318
+ InlineBuilder().button_url_link(
1319
+ text=t,
1320
+ id=id,
1321
+ url=u
1322
+ )
1323
+ )
1324
+
1325
+
1326
+ elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
1327
+ for u in url:
1328
+ builder = builder.row(
1329
+ InlineBuilder().button_url_link(
1330
+ text=title_button,
1331
+ id=id,
1332
+ url=u
1333
+ )
1334
+ )
1335
+
1336
+
1337
+ else:
1338
+ builder = builder.row(
1339
+ InlineBuilder().button_url_link(
1340
+ text=title_button,
1341
+ id=id,
1342
+ url=url
1343
+ )
1344
+ )
1345
+
1346
+ return self.send_message(
1347
+ chat_id=chat_id,
1348
+ text=text,
1349
+ inline_keypad=builder.build(),
1350
+ reply_to_message_id=reply_to_message_id
1351
+ )
1352
+
1353
+
1354
+ def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
1355
+ allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
1356
+ if media_type not in allowed:
1357
+ raise ValueError(f"Invalid media type. Must be one of {allowed}")
1358
+ result = self._post("requestSendFile", {"type": media_type})
1359
+ return result.get("data", {}).get("upload_url")
1360
+ 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1361
+ payload = {
1362
+ "chat_id": chat_id,
1363
+ "file_id": file_id,
1364
+ "text": text,
1365
+ "disable_notification": disable_notification,
1366
+ "chat_keypad_type": chat_keypad_type,
1367
+ }
1368
+ if chat_keypad:
1369
+ payload["chat_keypad"] = chat_keypad
1370
+ if inline_keypad:
1371
+ payload["inline_keypad"] = inline_keypad
1372
+ if reply_to_message_id:
1373
+ payload["reply_to_message_id"] = str(reply_to_message_id)
1374
+
1375
+ resp = self._post("sendFile", payload)
1376
+ message_id_put = resp["data"]["message_id"]
1377
+ result = {
1378
+ "status": resp.get("status"),
1379
+ "status_det": resp.get("status_det"),
1380
+ "file_id": file_id,
1381
+ "text":text,
1382
+ "message_id": message_id_put,
1383
+ "send_to_chat_id": chat_id,
1384
+ "reply_to_message_id": reply_to_message_id,
1385
+ "disable_notification": disable_notification,
1386
+ "type_file": type_file,
1387
+ "raw_response": resp,
1388
+ "chat_keypad":chat_keypad,
1389
+ "inline_keypad":inline_keypad,
1390
+ "chat_keypad_type":chat_keypad_type
1391
+ }
1392
+ import json
1393
+ return json.dumps(result, ensure_ascii=False, indent=4)
1394
+ def send_file(
1395
+ self,
1396
+ chat_id: str,
1397
+ path: Optional[Union[str, Path]] = None,
1398
+ file_id: Optional[str] = None,
1399
+ caption: Optional[str] = None,
1400
+ file_name: Optional[str] = None,
1401
+ inline_keypad: Optional[Dict[str, Any]] = None,
1402
+ chat_keypad: Optional[Dict[str, Any]] = None,
1403
+ reply_to_message_id: Optional[str] = None,
1404
+ disable_notification: bool = False,
1405
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1406
+ ) -> Dict[str, Any]:
1407
+ if path:
1408
+ file_name = file_name or Path(path).name
1409
+ upload_url = self.get_upload_url("File")
1410
+ file_id = self.upload_media_file(upload_url, file_name, path)
1411
+ if not file_id:
1412
+ raise ValueError("Either path or file_id must be provided.")
1413
+ return self._send_uploaded_file(
1414
+ chat_id=chat_id,
1415
+ file_id=file_id,
1416
+ text=caption,
1417
+ inline_keypad=inline_keypad,
1418
+ chat_keypad=chat_keypad,
1419
+ reply_to_message_id=reply_to_message_id,
1420
+ disable_notification=disable_notification,
1421
+ chat_keypad_type=chat_keypad_type
1422
+ )
1423
+ def re_send_file(
1424
+ self,
1425
+ chat_id: str,
1426
+ path: Optional[Union[str, Path]] = None,
1427
+ file_id: Optional[str] = None,
1428
+ caption: Optional[str] = None,
1429
+ file_name: Optional[str] = None,
1430
+ inline_keypad: Optional[Dict[str, Any]] = None,
1431
+ chat_keypad: Optional[Dict[str, Any]] = None,
1432
+ reply_to_message_id: Optional[str] = None,
1433
+ disable_notification: bool = False,
1434
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1435
+ ) -> Dict[str, Any]:
1436
+ if path:
1437
+ file_name = file_name or Path(path).name
1438
+ upload_url = self.get_upload_url("File")
1439
+ file_id = self.upload_media_file(upload_url, file_name, path)
1440
+ if not file_id:
1441
+ raise ValueError("Either path or file_id must be provided.")
1442
+ return self._send_uploaded_file(
1443
+ chat_id=chat_id,
1444
+ file_id=file_id,
1445
+ text=caption,
1446
+ inline_keypad=inline_keypad,
1447
+ chat_keypad=chat_keypad,
1448
+ reply_to_message_id=reply_to_message_id,
1449
+ disable_notification=disable_notification,
1450
+ chat_keypad_type=chat_keypad_type
1451
+ )
1452
+ def send_document(
1453
+ self,
1454
+ chat_id: str,
1455
+ path: Optional[Union[str, Path]] = None,
1456
+ file_id: Optional[str] = None,
1457
+ text: Optional[str] = None,
1458
+ file_name: Optional[str] = None,
1459
+ inline_keypad: Optional[Dict[str, Any]] = None,
1460
+ chat_keypad: Optional[Dict[str, Any]] = None,
1461
+ reply_to_message_id: Optional[str] = None,
1462
+ disable_notification: bool = False,
1463
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1464
+ ) -> Dict[str, Any]:
1465
+ if path:
1466
+ file_name = file_name or Path(path).name
1467
+ upload_url = self.get_upload_url("File")
1468
+ file_id = self.upload_media_file(upload_url, file_name, path)
1469
+ if not file_id:
1470
+ raise ValueError("Either path or file_id must be provided.")
1471
+ return self._send_uploaded_file(
1472
+ chat_id=chat_id,
1473
+ file_id=file_id,
1474
+ text=text,
1475
+ inline_keypad=inline_keypad,
1476
+ chat_keypad=chat_keypad,
1477
+ reply_to_message_id=reply_to_message_id,
1478
+ disable_notification=disable_notification,
1479
+ chat_keypad_type=chat_keypad_type
1480
+ )
1481
+ def send_music(
1482
+ self,
1483
+ chat_id: str,
1484
+ path: Optional[Union[str, Path]] = None,
1485
+ file_id: Optional[str] = None,
1486
+ text: Optional[str] = None,
1487
+ file_name: Optional[str] = None,
1488
+ inline_keypad: Optional[Dict[str, Any]] = None,
1489
+ chat_keypad: Optional[Dict[str, Any]] = None,
1490
+ reply_to_message_id: Optional[str] = None,
1491
+ disable_notification: bool = False,
1492
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1493
+ ) -> Dict[str, Any]:
1494
+ if path:
1495
+ file_name = file_name or Path(path).name
1496
+ upload_url = self.get_upload_url("Music")
1497
+ file_id = self.upload_media_file(upload_url, file_name, path)
1498
+ if not file_id:
1499
+ raise ValueError("Either path or file_id must be provided.")
1500
+ return self._send_uploaded_file(
1501
+ chat_id=chat_id,
1502
+ file_id=file_id,
1503
+ text=text,
1504
+ inline_keypad=inline_keypad,
1505
+ chat_keypad=chat_keypad,
1506
+ reply_to_message_id=reply_to_message_id,
1507
+ disable_notification=disable_notification,
1508
+ chat_keypad_type=chat_keypad_type
1509
+ )
1510
+ def send_video(
1511
+ self,
1512
+ chat_id: str,
1513
+ path: Optional[Union[str, Path]] = None,
1514
+ file_id: Optional[str] = None,
1515
+ text: Optional[str] = None,
1516
+ file_name: Optional[str] = None,
1517
+ inline_keypad: Optional[Dict[str, Any]] = None,
1518
+ chat_keypad: Optional[Dict[str, Any]] = None,
1519
+ reply_to_message_id: Optional[str] = None,
1520
+ disable_notification: bool = False,
1521
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1522
+ ) -> Dict[str, Any]:
1523
+ if path:
1524
+ file_name = file_name or Path(path).name
1525
+ upload_url = self.get_upload_url("Video")
1526
+ file_id = self.upload_media_file(upload_url, file_name, path)
1527
+ if not file_id:
1528
+ raise ValueError("Either path or file_id must be provided.")
1529
+ return self._send_uploaded_file(
1530
+ chat_id=chat_id,
1531
+ file_id=file_id,
1532
+ text=text,
1533
+ inline_keypad=inline_keypad,
1534
+ chat_keypad=chat_keypad,
1535
+ reply_to_message_id=reply_to_message_id,
1536
+ disable_notification=disable_notification,
1537
+ chat_keypad_type=chat_keypad_type
1538
+ )
1539
+ def send_voice(
1540
+ self,
1541
+ chat_id: str,
1542
+ path: Optional[Union[str, Path]] = None,
1543
+ file_id: Optional[str] = None,
1544
+ text: Optional[str] = None,
1545
+ file_name: Optional[str] = None,
1546
+ inline_keypad: Optional[Dict[str, Any]] = None,
1547
+ chat_keypad: Optional[Dict[str, Any]] = None,
1548
+ reply_to_message_id: Optional[str] = None,
1549
+ disable_notification: bool = False,
1550
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1551
+ ) -> Dict[str, Any]:
1552
+ if path:
1553
+ file_name = file_name or Path(path).name
1554
+ upload_url = self.get_upload_url("Voice")
1555
+ file_id = self.upload_media_file(upload_url, file_name, path)
1556
+ if not file_id:
1557
+ raise ValueError("Either path or file_id must be provided.")
1558
+ return self._send_uploaded_file(
1559
+ chat_id=chat_id,
1560
+ file_id=file_id,
1561
+ text=text,
1562
+ inline_keypad=inline_keypad,
1563
+ chat_keypad=chat_keypad,
1564
+ reply_to_message_id=reply_to_message_id,
1565
+ disable_notification=disable_notification,
1566
+ chat_keypad_type=chat_keypad_type
1567
+ )
1568
+ def send_image(
1569
+ self,
1570
+ chat_id: str,
1571
+ path: Optional[Union[str, Path]] = None,
1572
+ file_id: Optional[str] = None,
1573
+ text: Optional[str] = None,
1574
+ file_name: Optional[str] = None,
1575
+ inline_keypad: Optional[Dict[str, Any]] = None,
1576
+ chat_keypad: Optional[Dict[str, Any]] = None,
1577
+ reply_to_message_id: Optional[str] = None,
1578
+ disable_notification: bool = False,
1579
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1580
+ ) -> Dict[str, Any]:
1581
+ if path:
1582
+ file_name = file_name or Path(path).name
1583
+ upload_url = self.get_upload_url("Image")
1584
+ file_id = self.upload_media_file(upload_url, file_name, path)
1585
+ if not file_id:
1586
+ raise ValueError("Either path or file_id must be provided.")
1587
+ return self._send_uploaded_file(
1588
+ chat_id=chat_id,
1589
+ file_id=file_id,
1590
+ text=text,
1591
+ inline_keypad=inline_keypad,
1592
+ chat_keypad=chat_keypad,
1593
+ reply_to_message_id=reply_to_message_id,
1594
+ disable_notification=disable_notification,
1595
+ chat_keypad_type=chat_keypad_type
1596
+ )
1597
+ def send_gif(
1598
+ self,
1599
+ chat_id: str,
1600
+ path: Optional[Union[str, Path]] = None,
1601
+ file_id: Optional[str] = None,
1602
+ text: Optional[str] = None,
1603
+ file_name: Optional[str] = None,
1604
+ inline_keypad: Optional[Dict[str, Any]] = None,
1605
+ chat_keypad: Optional[Dict[str, Any]] = None,
1606
+ reply_to_message_id: Optional[str] = None,
1607
+ disable_notification: bool = False,
1608
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
1609
+ ) -> Dict[str, Any]:
1610
+ if path:
1611
+ file_name = file_name or Path(path).name
1612
+ upload_url = self.get_upload_url("Gif")
1613
+ file_id = self.upload_media_file(upload_url, file_name, path)
1614
+ if not file_id:
1615
+ raise ValueError("Either path or file_id must be provided.")
1616
+ return self._send_uploaded_file(
1617
+ chat_id=chat_id,
1618
+ file_id=file_id,
1619
+ text=text,
1620
+ inline_keypad=inline_keypad,
1621
+ chat_keypad=chat_keypad,
1622
+ reply_to_message_id=reply_to_message_id,
1623
+ disable_notification=disable_notification,
1624
+ chat_keypad_type=chat_keypad_type
1625
+ )
1626
+
1627
+
1628
+
1629
+ def forward_message(
1630
+ self,
1631
+ from_chat_id: str,
1632
+ message_id: str,
1633
+ to_chat_id: str,
1634
+ disable_notification: bool = False
1635
+ ) -> Dict[str, Any]:
1636
+ """Forward a message from one chat to another."""
1637
+ return self._post("forwardMessage", {
1638
+ "from_chat_id": from_chat_id,
1639
+ "message_id": message_id,
1640
+ "to_chat_id": to_chat_id,
1641
+ "disable_notification": disable_notification
1642
+ })
1643
+
1644
+ def edit_message_text(
1645
+ self,
1646
+ chat_id: str,
1647
+ message_id: str,
1648
+ text: str
1649
+ ) -> Dict[str, Any]:
1650
+ """Edit text of an existing message."""
1651
+ return self._post("editMessageText", {
1652
+ "chat_id": chat_id,
1653
+ "message_id": message_id,
1654
+ "text": text
1655
+ })
1656
+
1657
+ def edit_inline_keypad(
1658
+ self,
1659
+ chat_id: str,
1660
+ message_id: str,
1661
+ inline_keypad: Dict[str, Any],
1662
+ text : str = None
1663
+ ) -> Dict[str, Any]:
1664
+ """Edit inline keypad of a message."""
1665
+ if text is not None:self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
1666
+ return self._post("editMessageKeypad", {
1667
+ "chat_id": chat_id,
1668
+ "message_id": message_id,
1669
+ "inline_keypad": inline_keypad
1670
+ })
1671
+
1672
+ def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
1673
+ """Delete a message from chat."""
1674
+ return self._post("deleteMessage", {
1675
+ "chat_id": chat_id,
1676
+ "message_id": message_id
1677
+ })
1678
+
1679
+ def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
1680
+ """Set bot commands."""
1681
+ return self._post("setCommands", {"bot_commands": bot_commands})
1682
+
1683
+ def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
1684
+ """Update bot endpoint (Webhook or Polling)."""
1685
+ return self._post("updateBotEndpoints", {
1686
+ "url": url,
1687
+ "type": type
1688
+ })
1689
+
1690
+ def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
1691
+ """Remove chat keypad."""
1692
+ return self._post("editChatKeypad", {
1693
+ "chat_id": chat_id,
1694
+ "chat_keypad_type": "Removed"
1695
+ })
1696
+
1697
+ def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
1698
+ """Edit or add new chat keypad."""
1699
+ return self._post("editChatKeypad", {
1700
+ "chat_id": chat_id,
1701
+ "chat_keypad_type": "New",
1702
+ "chat_keypad": chat_keypad
1703
+ })
1704
+ def get_name(self, chat_id: str) -> str:
1705
+ try:
1706
+ chat = self.get_chat(chat_id)
1707
+ chat_info = chat.get("data", {}).get("chat", {})
1708
+ first_name = chat_info.get("first_name", "")
1709
+ last_name = chat_info.get("last_name", "")
1710
+
1711
+ if first_name and last_name:
1712
+ return f"{first_name} {last_name}"
1713
+ elif first_name:
1714
+ return first_name
1715
+ elif last_name:
1716
+ return last_name
1717
+ else:
1718
+ return "Unknown"
1719
+ except Exception:
1720
+ return "Unknown"
1721
+ def get_username(self, chat_id: str) -> str:
1722
+ chat_info = self.get_chat(chat_id).get("data", {}).get("chat", {})
1723
+ return chat_info.get("username", "None")