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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. rubka/__init__.py +72 -3
  2. rubka/adaptorrubka/__init__.py +4 -0
  3. rubka/adaptorrubka/client/__init__.py +1 -0
  4. rubka/adaptorrubka/client/client.py +60 -0
  5. rubka/adaptorrubka/crypto/__init__.py +1 -0
  6. rubka/adaptorrubka/crypto/crypto.py +82 -0
  7. rubka/adaptorrubka/enums.py +36 -0
  8. rubka/adaptorrubka/exceptions.py +22 -0
  9. rubka/adaptorrubka/methods/__init__.py +1 -0
  10. rubka/adaptorrubka/methods/methods.py +90 -0
  11. rubka/adaptorrubka/network/__init__.py +3 -0
  12. rubka/adaptorrubka/network/helper.py +22 -0
  13. rubka/adaptorrubka/network/network.py +221 -0
  14. rubka/adaptorrubka/network/socket.py +31 -0
  15. rubka/adaptorrubka/sessions/__init__.py +1 -0
  16. rubka/adaptorrubka/sessions/sessions.py +72 -0
  17. rubka/adaptorrubka/types/__init__.py +1 -0
  18. rubka/adaptorrubka/types/socket/__init__.py +1 -0
  19. rubka/adaptorrubka/types/socket/message.py +187 -0
  20. rubka/adaptorrubka/utils/__init__.py +2 -0
  21. rubka/adaptorrubka/utils/configs.py +18 -0
  22. rubka/adaptorrubka/utils/utils.py +251 -0
  23. rubka/api.py +1450 -95
  24. rubka/asynco.py +2515 -0
  25. rubka/button.py +404 -0
  26. rubka/context.py +744 -34
  27. rubka/exceptions.py +35 -1
  28. rubka/filters.py +321 -0
  29. rubka/helpers.py +1461 -0
  30. rubka/keypad.py +255 -5
  31. rubka/metadata.py +117 -0
  32. rubka/rubino.py +1231 -0
  33. rubka/tv.py +145 -0
  34. rubka/update.py +1038 -0
  35. rubka-7.1.17.dist-info/METADATA +1048 -0
  36. rubka-7.1.17.dist-info/RECORD +45 -0
  37. rubka-7.1.17.dist-info/entry_points.txt +2 -0
  38. rubka-2.11.13.dist-info/METADATA +0 -315
  39. rubka-2.11.13.dist-info/RECORD +0 -15
  40. {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/WHEEL +0 -0
  41. {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/top_level.txt +0 -0
rubka/api.py CHANGED
@@ -1,13 +1,25 @@
1
1
  import requests
2
2
  from typing import List, Optional, Dict, Any, Literal
3
3
  from .exceptions import APIRequestError
4
+ from .adaptorrubka import Client as Client_get
4
5
  from .logger import logger
6
+ from . import filters
7
+ from . import helpers
5
8
  from typing import Callable
6
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
7
17
  API_URL = "https://botapi.rubika.ir/v3"
18
+ import mimetypes
19
+ import re
8
20
  import sys
9
21
  import subprocess
10
- import requests
22
+ class InvalidTokenError(Exception):pass
11
23
  def install_package(package_name):
12
24
  try:
13
25
  subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -42,41 +54,88 @@ def get_latest_version(package_name: str) -> str:
42
54
  data = resp.json()
43
55
  return data["info"]["version"]
44
56
  except Exception:return None
45
- def check_rubka_version():
46
- package_name = "rubka"
47
- installed_version = get_installed_version(package_name)
48
- if installed_version is None:return
49
- latest_version = get_latest_version(package_name)
50
- if latest_version is None:return
51
- if installed_version != latest_version:
52
- print(f"\n\n⚠️ WARNING: Your installed version of '{package_name}' is outdated!")
53
- print(f"Installed version: {installed_version}")
54
- print(f"Latest version: {latest_version}")
55
- print(f"Please update it using:\n\npip install --upgrade {package_name}\n")
56
-
57
- check_rubka_version()
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
58
67
  class Robot:
59
68
  """
60
69
  Main class to interact with Rubika Bot API.
61
70
  Initialized with bot token.
62
71
  """
63
72
 
64
- def __init__(self, token: str):
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
+ """
65
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
66
105
  self._offset_id = None
67
106
  self.session = requests.Session()
68
107
  self.sessions: Dict[str, Dict[str, Any]] = {}
69
108
  self._callback_handler = None
70
109
  self._message_handler = None
71
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
72
131
 
73
132
 
74
- logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
75
133
 
134
+ logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
76
135
  def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
77
136
  url = f"{API_URL}/{self.token}/{method}"
78
137
  try:
79
- response = self.session.post(url, json=data, timeout=10)
138
+ response = self.session.post(url, json=data, timeout=self.timeout)
80
139
  response.raise_for_status()
81
140
  try:
82
141
  json_resp = response.json()
@@ -94,100 +153,846 @@ class Robot:
94
153
  def get_me(self) -> Dict[str, Any]:
95
154
  """Get info about the bot itself."""
96
155
  return self._post("getMe", {})
97
- def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
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):
98
666
  def decorator(func: Callable[[Any, Message], None]):
99
- self._message_handler = {
667
+ self._message_handlers.append({
100
668
  "func": func,
101
669
  "filters": filters,
102
670
  "commands": commands
103
- }
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
+ })
104
681
  return func
105
682
  return decorator
106
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}")
107
723
 
108
- def on_inline_query(self):
724
+ def on_inline_query(self, button_id: Optional[str] = None):
109
725
  def decorator(func: Callable[[Any, InlineMessage], None]):
110
- self._inline_query_handler = func
726
+ self._inline_query_handlers.append({
727
+ "func": func,
728
+ "button_id": button_id
729
+ })
111
730
  return func
112
731
  return decorator
732
+
113
733
 
114
- def _process_update(self, update: Dict[str, Any]):
734
+ def _process_update(self, update: dict):
115
735
  import threading
116
- if update.get('type') == 'ReceiveQuery':
736
+
737
+ if update.get("type") == "ReceiveQuery":
117
738
  msg = update.get("inline_message", {})
118
- if self._inline_query_handler:
119
- context = InlineMessage(bot=self, raw_data=msg)
120
- threading.Thread(target=self._inline_query_handler, args=(self, context), daemon=True).start()
121
-
122
- if update.get('type') == 'NewMessage':
123
- msg = update.get('new_message', {})
124
- chat_id = update.get('chat_id')
125
- message_id = msg.get('message_id')
126
- sender_id = msg.get('sender_id')
127
- text = msg.get('text')
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", {})
128
754
  try:
129
- import time
130
755
  if msg.get("time") and (time.time() - float(msg["time"])) > 20:
131
756
  return
132
757
  except Exception:
133
758
  return
134
759
 
135
- if self._message_handler:
136
- handler = self._message_handler
137
- context = Message(bot=self, chat_id=chat_id, message_id=message_id, sender_id=sender_id, text=text, raw_data=msg)
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)
138
766
 
139
- if handler["commands"]:
140
- if not context.text or not context.text.startswith("/"):
141
- return
142
- parts = context.text.split()
143
- cmd = parts[0][1:]
144
- if cmd not in handler["commands"]:
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()
145
771
  return
146
- context.args = parts[1:]
147
772
 
148
- if handler["filters"]:
149
- if not handler["filters"](context):
150
- return
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:]
151
783
 
152
- threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
784
+ if handler["filters"] and not handler["filters"](context):
785
+ continue
153
786
 
154
- elif update.get('type') == 'ReceiveQuery':
155
- msg = update.get("inline_message", {})
156
- chat_id = msg.get("chat_id")
157
- message_id = msg.get("message_id")
158
- sender_id = msg.get("sender_id")
159
- text = msg.get("text")
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()
160
815
 
161
- if hasattr(self, "_callback_handler"):
162
- context = Message(bot=self, chat_id=chat_id, message_id=message_id, sender_id=sender_id, text=text, raw_data=msg)
163
- threading.Thread(target=self._callback_handler, args=(self, context), daemon=True).start()
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]
164
819
 
820
+ if message_id in self._processed_message_ids:
821
+ return True
165
822
 
823
+ self._processed_message_ids[message_id] = now
824
+ return False
166
825
 
167
- def run(self):
168
- print("Bot started running...")
169
- if self._offset_id is None:
170
- try:
171
- latest = self.get_updates(limit=100)
172
- if latest and latest.get("data") and latest["data"].get("updates"):
173
- updates = latest["data"]["updates"]
174
- last_update = updates[-1]
175
- self._offset_id = latest["data"].get("next_offset_id")
176
- print(f"Offset initialized to: {self._offset_id}")
177
- else:
178
- print("No updates found.")
179
- except Exception as e:
180
- print(f"Failed to fetch latest message: {e}")
181
826
 
182
- while True:
183
- try:
184
- updates = self.get_updates(offset_id=self._offset_id, limit=100)
185
- if updates and updates.get("data"):
186
- for update in updates["data"].get("updates", []):
187
- self._process_update(update)
188
- self._offset_id = updates["data"].get("next_offset_id", self._offset_id)
189
- except Exception as e:
190
- print(f"Error in run loop: {e}")
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.")
191
996
 
192
997
  def send_message(
193
998
  self,
@@ -197,7 +1002,9 @@ class Robot:
197
1002
  inline_keypad: Optional[Dict[str, Any]] = None,
198
1003
  disable_notification: bool = False,
199
1004
  reply_to_message_id: Optional[str] = None,
200
- chat_keypad_type: Optional[Literal["New", "Removed"]] = None
1005
+ chat_keypad_type: Optional[Literal["New", "Removed"]] = None,
1006
+ delete_after = None,
1007
+ parse_mode = None
201
1008
  ) -> Dict[str, Any]:
202
1009
  """
203
1010
  Send a text message to a chat.
@@ -218,6 +1025,48 @@ class Robot:
218
1025
 
219
1026
  return self._post("sendMessage", payload)
220
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
+
221
1070
  def send_poll(
222
1071
  self,
223
1072
  chat_id: str,
@@ -255,7 +1104,6 @@ class Robot:
255
1104
  "reply_to_message_id": reply_to_message_id,
256
1105
  "chat_keypad_type": chat_keypad_type
257
1106
  }
258
- # Remove None values
259
1107
  payload = {k: v for k, v in payload.items() if v is not None}
260
1108
  return self._post("sendLocation", payload)
261
1109
 
@@ -275,23 +1123,508 @@ class Robot:
275
1123
  "last_name": last_name,
276
1124
  "phone_number": phone_number
277
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))
278
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}")
279
1183
  def get_chat(self, chat_id: str) -> Dict[str, Any]:
280
1184
  """Get chat info."""
281
1185
  return self._post("getChat", {"chat_id": chat_id})
282
1186
 
283
- def get_updates(
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(
284
1395
  self,
285
- offset_id: Optional[str] = None,
286
- limit: Optional[int] = None
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"
287
1406
  ) -> Dict[str, Any]:
288
- """Get updates."""
289
- data = {}
290
- if offset_id:
291
- data["offset_id"] = offset_id
292
- if limit:
293
- data["limit"] = limit
294
- return self._post("getUpdates", data)
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
+
295
1628
 
296
1629
  def forward_message(
297
1630
  self,
@@ -325,10 +1658,12 @@ class Robot:
325
1658
  self,
326
1659
  chat_id: str,
327
1660
  message_id: str,
328
- inline_keypad: Dict[str, Any]
1661
+ inline_keypad: Dict[str, Any],
1662
+ text : str = None
329
1663
  ) -> Dict[str, Any]:
330
1664
  """Edit inline keypad of a message."""
331
- return self._post("editInlineKeypad", {
1665
+ if text is not None:self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
1666
+ return self._post("editMessageKeypad", {
332
1667
  "chat_id": chat_id,
333
1668
  "message_id": message_id,
334
1669
  "inline_keypad": inline_keypad
@@ -366,3 +1701,23 @@ class Robot:
366
1701
  "chat_keypad_type": "New",
367
1702
  "chat_keypad": chat_keypad
368
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")