Rubka 5.0.0__py3-none-any.whl → 6.4.2__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.
rubka/asynco.py CHANGED
@@ -12,7 +12,7 @@ except (ImportError, ModuleNotFoundError):
12
12
 
13
13
  from tqdm.asyncio import tqdm
14
14
  from urllib.parse import urlparse, parse_qs
15
-
15
+ class InvalidTokenError(Exception):pass
16
16
  import mimetypes
17
17
  from pathlib import Path
18
18
  import time
@@ -121,12 +121,12 @@ class Robot:
121
121
  self._callback_handler = None
122
122
  self._message_handler = None
123
123
  self._inline_query_handler = None
124
+
124
125
  self._callback_handlers: List[dict] = []
125
126
  self._processed_message_ids: Dict[str, float] = {}
126
- self._message_handlers: List[dict] = []
127
+ self._message_handlers: List[dict] = []
127
128
 
128
129
  logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
129
-
130
130
  async def _get_session(self) -> aiohttp.ClientSession:
131
131
  """Lazily creates and returns the aiohttp session."""
132
132
  if self._aiohttp_session is None or self._aiohttp_session.closed:
@@ -186,6 +186,327 @@ class Robot:
186
186
  async def get_me(self) -> Dict[str, Any]:
187
187
  """Get info about the bot itself."""
188
188
  return await self._post("getMe", {})
189
+ async def geteToken(self):
190
+ """Check if the bot token is valid."""
191
+ if (await self.get_me())['status'] != "OK":
192
+ raise InvalidTokenError("The provided bot token is invalid or expired.")
193
+ from typing import Callable, Any, Optional, List
194
+
195
+ def on_message_private(
196
+ self,
197
+ chat_id: Optional[Union[str, List[str]]] = None,
198
+ commands: Optional[List[str]] = None,
199
+ filters: Optional[Callable[[Message], bool]] = None,
200
+ sender_id: Optional[Union[str, List[str]]] = None,
201
+ sender_type: Optional[str] = None,
202
+ allow_forwarded: bool = True,
203
+ allow_files: bool = True,
204
+ allow_stickers: bool = True,
205
+ allow_polls: bool = True,
206
+ allow_contacts: bool = True,
207
+ allow_locations: bool = True,
208
+ min_text_length: Optional[int] = None,
209
+ max_text_length: Optional[int] = None,
210
+ contains: Optional[str] = None,
211
+ startswith: Optional[str] = None,
212
+ endswith: Optional[str] = None,
213
+ case_sensitive: bool = False
214
+ ):
215
+ """
216
+ Advanced decorator for handling only private messages with extended filters.
217
+ """
218
+
219
+ def decorator(func: Callable[[Any, Message], None]):
220
+ async def wrapper(bot, message: Message):
221
+
222
+ if not message.is_private:
223
+ return
224
+
225
+
226
+ if chat_id:
227
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
228
+ return
229
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
230
+ return
231
+
232
+
233
+ if sender_id:
234
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
235
+ return
236
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
237
+ return
238
+
239
+
240
+ if sender_type and message.sender_type != sender_type:
241
+ return
242
+
243
+
244
+ if not allow_forwarded and message.forwarded_from:
245
+ return
246
+
247
+
248
+ if not allow_files and message.file:
249
+ return
250
+ if not allow_stickers and message.sticker:
251
+ return
252
+ if not allow_polls and message.poll:
253
+ return
254
+ if not allow_contacts and message.contact_message:
255
+ return
256
+ if not allow_locations and (message.location or message.live_location):
257
+ return
258
+
259
+
260
+ if message.text:
261
+ text = message.text if case_sensitive else message.text.lower()
262
+ if min_text_length and len(message.text) < min_text_length:
263
+ return
264
+ if max_text_length and len(message.text) > max_text_length:
265
+ return
266
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
267
+ return
268
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
269
+ return
270
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
271
+ return
272
+
273
+
274
+ if commands:
275
+ if not message.text:
276
+ return
277
+ parts = message.text.strip().split()
278
+ cmd = parts[0].lstrip("/")
279
+ if cmd not in commands:
280
+ return
281
+ message.args = parts[1:]
282
+
283
+
284
+ if filters and not filters(message):
285
+ return
286
+
287
+ return await func(bot, message)
288
+
289
+ self._message_handlers.append({
290
+ "func": wrapper,
291
+ "filters": filters,
292
+ "commands": commands,
293
+ "chat_id": chat_id,
294
+ "private_only": True,
295
+ "sender_id": sender_id,
296
+ "sender_type": sender_type
297
+ })
298
+ return wrapper
299
+ return decorator
300
+ def on_message_channel(
301
+ self,
302
+ chat_id: Optional[Union[str, List[str]]] = None,
303
+ commands: Optional[List[str]] = None,
304
+ filters: Optional[Callable[[Message], bool]] = None,
305
+ sender_id: Optional[Union[str, List[str]]] = None,
306
+ sender_type: Optional[str] = None,
307
+ allow_forwarded: bool = True,
308
+ allow_files: bool = True,
309
+ allow_stickers: bool = True,
310
+ allow_polls: bool = True,
311
+ allow_contacts: bool = True,
312
+ allow_locations: bool = True,
313
+ min_text_length: Optional[int] = None,
314
+ max_text_length: Optional[int] = None,
315
+ contains: Optional[str] = None,
316
+ startswith: Optional[str] = None,
317
+ endswith: Optional[str] = None,
318
+ case_sensitive: bool = False
319
+ ):
320
+ """
321
+ Advanced decorator for handling only channel messages with extended filters.
322
+ """
323
+
324
+ def decorator(func: Callable[[Any, Message], None]):
325
+ async def wrapper(bot, message: Message):
326
+
327
+ if not message.is_channel:
328
+ return
329
+
330
+
331
+ if chat_id:
332
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
333
+ return
334
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
335
+ return
336
+
337
+
338
+ if sender_id:
339
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
340
+ return
341
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
342
+ return
343
+
344
+
345
+ if sender_type and message.sender_type != sender_type:
346
+ return
347
+
348
+
349
+ if not allow_forwarded and message.forwarded_from:
350
+ return
351
+
352
+
353
+ if not allow_files and message.file:
354
+ return
355
+ if not allow_stickers and message.sticker:
356
+ return
357
+ if not allow_polls and message.poll:
358
+ return
359
+ if not allow_contacts and message.contact_message:
360
+ return
361
+ if not allow_locations and (message.location or message.live_location):
362
+ return
363
+
364
+
365
+ if message.text:
366
+ text = message.text if case_sensitive else message.text.lower()
367
+ if min_text_length and len(message.text) < min_text_length:
368
+ return
369
+ if max_text_length and len(message.text) > max_text_length:
370
+ return
371
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
372
+ return
373
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
374
+ return
375
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
376
+ return
377
+
378
+
379
+ if commands:
380
+ if not message.text:
381
+ return
382
+ parts = message.text.strip().split()
383
+ cmd = parts[0].lstrip("/")
384
+ if cmd not in commands:
385
+ return
386
+ message.args = parts[1:]
387
+
388
+
389
+ if filters and not filters(message):
390
+ return
391
+
392
+ return await func(bot, message)
393
+
394
+ self._message_handlers.append({
395
+ "func": wrapper,
396
+ "filters": filters,
397
+ "commands": commands,
398
+ "chat_id": chat_id,
399
+ "group_only": True,
400
+ "sender_id": sender_id,
401
+ "sender_type": sender_type
402
+ })
403
+ return wrapper
404
+ return decorator
405
+ def on_message_group(
406
+ self,
407
+ chat_id: Optional[Union[str, List[str]]] = None,
408
+ commands: Optional[List[str]] = None,
409
+ filters: Optional[Callable[[Message], bool]] = None,
410
+ sender_id: Optional[Union[str, List[str]]] = None,
411
+ sender_type: Optional[str] = None,
412
+ allow_forwarded: bool = True,
413
+ allow_files: bool = True,
414
+ allow_stickers: bool = True,
415
+ allow_polls: bool = True,
416
+ allow_contacts: bool = True,
417
+ allow_locations: bool = True,
418
+ min_text_length: Optional[int] = None,
419
+ max_text_length: Optional[int] = None,
420
+ contains: Optional[str] = None,
421
+ startswith: Optional[str] = None,
422
+ endswith: Optional[str] = None,
423
+ case_sensitive: bool = False
424
+ ):
425
+ """
426
+ Advanced decorator for handling only group messages with extended filters.
427
+ """
428
+
429
+ def decorator(func: Callable[[Any, Message], None]):
430
+ async def wrapper(bot, message: Message):
431
+
432
+ if not message.is_group:
433
+ return
434
+
435
+
436
+ if chat_id:
437
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
438
+ return
439
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
440
+ return
441
+
442
+
443
+ if sender_id:
444
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
445
+ return
446
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
447
+ return
448
+
449
+
450
+ if sender_type and message.sender_type != sender_type:
451
+ return
452
+
453
+
454
+ if not allow_forwarded and message.forwarded_from:
455
+ return
456
+
457
+
458
+ if not allow_files and message.file:
459
+ return
460
+ if not allow_stickers and message.sticker:
461
+ return
462
+ if not allow_polls and message.poll:
463
+ return
464
+ if not allow_contacts and message.contact_message:
465
+ return
466
+ if not allow_locations and (message.location or message.live_location):
467
+ return
468
+
469
+
470
+ if message.text:
471
+ text = message.text if case_sensitive else message.text.lower()
472
+ if min_text_length and len(message.text) < min_text_length:
473
+ return
474
+ if max_text_length and len(message.text) > max_text_length:
475
+ return
476
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
477
+ return
478
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
479
+ return
480
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
481
+ return
482
+
483
+
484
+ if commands:
485
+ if not message.text:
486
+ return
487
+ parts = message.text.strip().split()
488
+ cmd = parts[0].lstrip("/")
489
+ if cmd not in commands:
490
+ return
491
+ message.args = parts[1:]
492
+
493
+
494
+ if filters and not filters(message):
495
+ return
496
+
497
+ return await func(bot, message)
498
+
499
+ self._message_handlers.append({
500
+ "func": wrapper,
501
+ "filters": filters,
502
+ "commands": commands,
503
+ "chat_id": chat_id,
504
+ "group_only": True,
505
+ "sender_id": sender_id,
506
+ "sender_type": sender_type
507
+ })
508
+ return wrapper
509
+ return decorator
189
510
 
190
511
  def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
191
512
  def decorator(func: Callable[[Any, Message], None]):
@@ -204,9 +525,39 @@ class Robot:
204
525
  "commands": commands
205
526
  })
206
527
  return func
207
- return decorator
528
+ return decorator
208
529
 
209
530
  def on_callback(self, button_id: Optional[str] = None):
531
+ def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
532
+ if not hasattr(self, "_callback_handlers"):
533
+ self._callback_handlers = []
534
+ self._callback_handlers.append({
535
+ "func": func,
536
+ "button_id": button_id
537
+ })
538
+ return func
539
+ return decorator
540
+ def on_callback_query(self, button_id: Optional[str] = None):
541
+ def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
542
+ if not hasattr(self, "_callback_handlers"):
543
+ self._callback_handlers = []
544
+ self._callback_handlers.append({
545
+ "func": func,
546
+ "button_id": button_id
547
+ })
548
+ return func
549
+ return decorator
550
+ def callback_query_handler(self, button_id: Optional[str] = None):
551
+ def decorator(func: Callable[[Any, Message], None]):
552
+ if not hasattr(self, "_callback_handlers"):
553
+ self._callback_handlers = []
554
+ self._callback_handlers.append({
555
+ "func": func,
556
+ "button_id": button_id
557
+ })
558
+ return func
559
+ return decorator
560
+ def callback_query(self, button_id: Optional[str] = None):
210
561
  def decorator(func: Callable[[Any, Message], None]):
211
562
  if not hasattr(self, "_callback_handlers"):
212
563
  self._callback_handlers = []
@@ -272,6 +623,14 @@ class Robot:
272
623
  if update.get("type") == "ReceiveQuery":
273
624
  msg = update.get("inline_message", {})
274
625
  context = InlineMessage(bot=self, raw_data=msg)
626
+
627
+
628
+ if hasattr(self, "_callback_handlers"):
629
+ for handler in self._callback_handlers:
630
+ if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
631
+ asyncio.create_task(handler["func"](self, context))
632
+
633
+
275
634
  asyncio.create_task(self._handle_inline_query(context))
276
635
  return
277
636
 
@@ -354,22 +713,389 @@ class Robot:
354
713
  return False
355
714
 
356
715
 
357
- async def run(self):
358
- """
359
- Starts the bot.
360
- This method is now corrected to handle webhook updates similarly to the original synchronous code.
716
+
717
+
718
+ async def run(
719
+ self,
720
+ debug: bool = False,
721
+ sleep_time: float = 0.2,
722
+ webhook_timeout: int = 20,
723
+ update_limit: int = 100,
724
+ retry_delay: float = 5.0,
725
+ stop_on_error: bool = False,
726
+ max_errors: int = 0,
727
+ auto_restart: bool = False,
728
+ max_runtime: float | None = None,
729
+ loop_forever: bool = True,
730
+ allowed_update_types: list[str] | None = None,
731
+ ignore_duplicate_messages: bool = True,
732
+ skip_inline_queries: bool = False,
733
+ skip_channel_posts: bool = False,
734
+ skip_service_messages: bool = False,
735
+ skip_edited_messages: bool = False,
736
+ skip_bot_messages: bool = False,
737
+ log_file: str | None = None,
738
+ log_level: str = "info",
739
+ print_exceptions: bool = True,
740
+ error_handler=None,
741
+ shutdown_hook=None,
742
+ save_unprocessed_updates: bool = False,
743
+ log_to_console: bool = True,
744
+ rate_limit: float | None = None,
745
+ max_message_size: int | None = None,
746
+ ignore_users: set[str] | None = None,
747
+ ignore_groups: set[str] | None = None,
748
+ require_auth_token: bool = False,
749
+ only_private_chats: bool = False,
750
+ only_groups: bool = False,
751
+ require_admin_rights: bool = False,
752
+ custom_update_fetcher=None,
753
+ custom_update_processor=None,
754
+ process_in_background: bool = False,
755
+ max_queue_size: int = 1000,
756
+ thread_workers: int = 3,
757
+ message_filter=None,
758
+ pause_on_idle: bool = False,
759
+ max_concurrent_tasks: int | None = None,
760
+ metrics_enabled: bool = False,
761
+ metrics_handler=None,
762
+ notify_on_error: bool = False,
763
+ notification_handler=None,
764
+ watchdog_timeout: float | None = None,
765
+ ):
361
766
  """
767
+ Starts the bot's main execution loop with extensive configuration options.
768
+
769
+ This function handles:
770
+ - Update fetching and processing with optional filters for types and sources.
771
+ - Error handling, retry mechanisms, and automatic restart options.
772
+ - Logging to console and/or files with configurable log levels.
773
+ - Message filtering based on users, groups, chat types, admin rights, and more.
774
+ - Custom update fetchers and processors for advanced use cases.
775
+ - Background processing with threading and task concurrency controls.
776
+ - Metrics collection and error notifications.
777
+ - Optional runtime limits, sleep delays, rate limiting, and watchdog monitoring.
778
+
779
+ Parameters
780
+ ----------
781
+ debug : bool
782
+ Enable debug mode for detailed logging and runtime checks.
783
+ sleep_time : float
784
+ Delay between update fetch cycles (seconds).
785
+ webhook_timeout : int
786
+ Timeout for webhook requests (seconds).
787
+ update_limit : int
788
+ Maximum updates to fetch per request.
789
+ retry_delay : float
790
+ Delay before retrying after failure (seconds).
791
+ stop_on_error : bool
792
+ Stop bot on unhandled errors.
793
+ max_errors : int
794
+ Maximum consecutive errors before stopping (0 = unlimited).
795
+ auto_restart : bool
796
+ Automatically restart the bot if it stops unexpectedly.
797
+ max_runtime : float | None
798
+ Maximum runtime in seconds before stopping.
799
+ loop_forever : bool
800
+ Keep the bot running continuously.
801
+
802
+ allowed_update_types : list[str] | None
803
+ Limit processing to specific update types.
804
+ ignore_duplicate_messages : bool
805
+ Skip identical messages.
806
+ skip_inline_queries : bool
807
+ Ignore inline query updates.
808
+ skip_channel_posts : bool
809
+ Ignore channel post updates.
810
+ skip_service_messages : bool
811
+ Ignore service messages.
812
+ skip_edited_messages : bool
813
+ Ignore edited messages.
814
+ skip_bot_messages : bool
815
+ Ignore messages from other bots.
816
+
817
+ log_file : str | None
818
+ File path for logging.
819
+ log_level : str
820
+ Logging level (debug, info, warning, error).
821
+ print_exceptions : bool
822
+ Print exceptions to console.
823
+ error_handler : callable | None
824
+ Custom function to handle errors.
825
+ shutdown_hook : callable | None
826
+ Function to execute on shutdown.
827
+ save_unprocessed_updates : bool
828
+ Save updates that failed processing.
829
+ log_to_console : bool
830
+ Enable/disable console logging.
831
+
832
+ rate_limit : float | None
833
+ Minimum delay between processing updates from the same user/group.
834
+ max_message_size : int | None
835
+ Maximum allowed message size.
836
+ ignore_users : set[str] | None
837
+ User IDs to ignore.
838
+ ignore_groups : set[str] | None
839
+ Group IDs to ignore.
840
+ require_auth_token : bool
841
+ Require users to provide authentication token.
842
+ only_private_chats : bool
843
+ Process only private chats.
844
+ only_groups : bool
845
+ Process only group chats.
846
+ require_admin_rights : bool
847
+ Process only if sender is admin.
848
+
849
+ custom_update_fetcher : callable | None
850
+ Custom update fetching function.
851
+ custom_update_processor : callable | None
852
+ Custom update processing function.
853
+ process_in_background : bool
854
+ Run processing in background threads.
855
+ max_queue_size : int
856
+ Maximum updates in processing queue.
857
+ thread_workers : int
858
+ Number of background worker threads.
859
+ message_filter : callable | None
860
+ Function to filter messages.
861
+ pause_on_idle : bool
862
+ Pause processing if idle.
863
+ max_concurrent_tasks : int | None
864
+ Maximum concurrent processing tasks.
865
+
866
+ metrics_enabled : bool
867
+ Enable metrics collection.
868
+ metrics_handler : callable | None
869
+ Function to handle metrics.
870
+ notify_on_error : bool
871
+ Send notifications on errors.
872
+ notification_handler : callable | None
873
+ Function to send error notifications.
874
+ watchdog_timeout : float | None
875
+ Maximum idle time before triggering watchdog restart.
876
+ """
877
+ import asyncio, time, datetime, traceback
878
+ from collections import deque
879
+ def _log(msg: str, level: str = "info"):
880
+ level_order = {"debug": 10, "info": 20, "warning": 30, "error": 40}
881
+ if level not in level_order:
882
+ level = "info"
883
+ if level_order[level] < level_order.get(log_level, 20):
884
+ return
885
+ line = f"[{level.upper()}] {datetime.datetime.now().isoformat()} - {msg}"
886
+ if log_to_console:
887
+ print(msg)
888
+ if log_file:
889
+ try:
890
+ with open(log_file, "a", encoding="utf-8") as f:
891
+ f.write(line + "\n")
892
+ except Exception:
893
+ pass
894
+
895
+ def _get_sender_and_chat(update: dict):
896
+
897
+ sender = None
898
+ chat = None
899
+ t = update.get("type")
900
+ if t == "NewMessage":
901
+ nm = update.get("new_message", {})
902
+ sender = nm.get("author_object_guid") or nm.get("author_guid") or nm.get("from_id")
903
+ chat = nm.get("object_guid") or nm.get("chat_id")
904
+ elif t == "ReceiveQuery":
905
+ im = update.get("inline_message", {})
906
+ sender = im.get("author_object_guid") or im.get("author_guid")
907
+ chat = im.get("object_guid") or im.get("chat_id")
908
+ else:
909
+ sender = update.get("author_guid") or update.get("from_id")
910
+ chat = update.get("object_guid") or update.get("chat_id")
911
+ return str(sender) if sender is not None else None, str(chat) if chat is not None else None
912
+
913
+ def _is_group_chat(chat_guid: str | None) -> bool | None:
914
+
915
+ if chat_guid is None:
916
+ return None
917
+ if hasattr(self, "_is_group_chat") and callable(getattr(self, "_is_group_chat")):
918
+ try:
919
+ return bool(self._is_group_chat(chat_guid))
920
+ except Exception:
921
+ return None
922
+ return None
923
+
924
+ async def _maybe_notify(err: Exception, context: dict):
925
+ if notify_on_error and notification_handler:
926
+ try:
927
+ if asyncio.iscoroutinefunction(notification_handler):
928
+ await notification_handler(err, context)
929
+ else:
930
+
931
+ notification_handler(err, context)
932
+ except Exception:
933
+ pass
934
+
935
+ async def _handle_error(err: Exception, context: dict):
936
+ if print_exceptions:
937
+ _log("Exception occurred:\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__)), "error")
938
+ else:
939
+ _log(f"Exception occurred: {err}", "error")
940
+ await _maybe_notify(err, context)
941
+ if error_handler:
942
+ try:
943
+ if asyncio.iscoroutinefunction(error_handler):
944
+ await error_handler(err, context)
945
+ else:
946
+ error_handler(err, context)
947
+ except Exception as e2:
948
+ _log(f"Error in error_handler: {e2}", "error")
949
+
950
+
951
+ rate_window = deque()
952
+ def _rate_ok():
953
+ if rate_limit is None or rate_limit <= 0:
954
+ return True
955
+ now = time.time()
956
+
957
+ while rate_window and now - rate_window[0] > 1.0:
958
+ rate_window.popleft()
959
+ if len(rate_window) < int(rate_limit):
960
+ rate_window.append(now)
961
+ return True
962
+ return False
963
+
964
+
965
+ queue = asyncio.Queue(maxsize=max_queue_size) if process_in_background else None
966
+ active_workers = []
967
+
968
+ sem = asyncio.Semaphore(max_concurrent_tasks) if max_concurrent_tasks and max_concurrent_tasks > 0 else None
969
+
970
+ async def _process(update: dict):
971
+
972
+ if allowed_update_types and update.get("type") not in allowed_update_types:
973
+ return False
974
+
975
+
976
+ t = update.get("type")
977
+ if skip_inline_queries and t == "ReceiveQuery":
978
+ return False
979
+ if skip_service_messages and t == "ServiceMessage":
980
+ return False
981
+ if skip_edited_messages and t == "EditMessage":
982
+ return False
983
+ if skip_channel_posts and t == "ChannelPost":
984
+ return False
985
+
986
+
987
+ sender, chat = _get_sender_and_chat(update)
988
+ if ignore_users and sender and sender in ignore_users:
989
+ return False
990
+ if ignore_groups and chat and chat in ignore_groups:
991
+ return False
992
+ if require_auth_token and not getattr(self, "_has_auth_token", False):
993
+ return False
994
+ if only_private_chats:
995
+ is_group = _is_group_chat(chat)
996
+ if is_group is True:
997
+ return False
998
+ if only_groups:
999
+ is_group = _is_group_chat(chat)
1000
+ if is_group is False:
1001
+ return False
1002
+ if skip_bot_messages and getattr(self, "_is_bot_guid", None) and sender == self._is_bot_guid:
1003
+ return False
1004
+
1005
+ if max_message_size is not None and max_message_size > 0:
1006
+
1007
+ content = None
1008
+ if t == "NewMessage":
1009
+ content = (update.get("new_message") or {}).get("text")
1010
+ elif t == "ReceiveQuery":
1011
+ content = (update.get("inline_message") or {}).get("text")
1012
+ elif "text" in update:
1013
+ content = update.get("text")
1014
+ if content and isinstance(content, str) and len(content) > max_message_size:
1015
+ return False
1016
+
1017
+ if message_filter:
1018
+ try:
1019
+ if not message_filter(update):
1020
+ return False
1021
+ except Exception:
1022
+
1023
+ pass
1024
+
1025
+
1026
+ if not _rate_ok():
1027
+ return False
1028
+
1029
+
1030
+ if custom_update_processor:
1031
+ if asyncio.iscoroutinefunction(custom_update_processor):
1032
+ await custom_update_processor(update)
1033
+ else:
1034
+
1035
+ await asyncio.get_running_loop().run_in_executor(None, custom_update_processor, update)
1036
+ else:
1037
+
1038
+ await self._process_update(update)
1039
+ return True
1040
+
1041
+ async def _worker():
1042
+ while True:
1043
+ update = await queue.get()
1044
+ try:
1045
+ if sem:
1046
+ async with sem:
1047
+ await _process(update)
1048
+ else:
1049
+ await _process(update)
1050
+ except Exception as e:
1051
+ await _handle_error(e, {"stage": "worker_process", "update": update})
1052
+ finally:
1053
+ queue.task_done()
1054
+
1055
+
1056
+ start_ts = time.time()
1057
+ error_count = 0
1058
+ last_loop_tick = time.time()
1059
+ processed_count = 0
1060
+ skipped_count = 0
1061
+ enqueued_count = 0
1062
+ unprocessed_storage = []
1063
+
1064
+
1065
+ if process_in_background:
1066
+ n_workers = max(1, int(thread_workers))
1067
+ for _ in range(n_workers):
1068
+ active_workers.append(asyncio.create_task(_worker()))
1069
+
1070
+
362
1071
  await check_rubka_version()
363
1072
  await self._initialize_webhook()
364
- print("Bot started running...")
1073
+ await self.geteToken()
1074
+ _log("Bot started running...", "info")
365
1075
 
366
1076
  try:
367
1077
  while True:
368
1078
  try:
369
- if self.web_hook:
370
-
371
-
1079
+
1080
+ if max_runtime is not None and (time.time() - start_ts) >= max_runtime:
1081
+ _log("Max runtime reached. Stopping loop.", "warning")
1082
+ break
1083
+
1084
+
1085
+ now = time.time()
1086
+ if watchdog_timeout and (now - last_loop_tick) > watchdog_timeout:
1087
+ _log(f"Watchdog triggered (> {watchdog_timeout}s)", "warning")
1088
+ if auto_restart:
1089
+ break
1090
+ last_loop_tick = now
1091
+
1092
+
1093
+ received_updates = None
1094
+ if custom_update_fetcher:
1095
+ received_updates = await custom_update_fetcher()
1096
+ elif self.web_hook:
372
1097
  webhook_data = await self.update_webhook()
1098
+ received_updates = []
373
1099
  if isinstance(webhook_data, list):
374
1100
  for item in webhook_data:
375
1101
  data = item.get("data", {})
@@ -378,10 +1104,12 @@ class Robot:
378
1104
  if received_at_str:
379
1105
  try:
380
1106
  received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
381
- if time.time() - received_at_ts > 20:
1107
+ if time.time() - received_at_ts > webhook_timeout:
1108
+ if debug:
1109
+ _log(f"Skipped old webhook update ({received_at_str})", "debug")
382
1110
  continue
383
1111
  except (ValueError, TypeError):
384
- pass
1112
+ pass
385
1113
 
386
1114
  update = None
387
1115
  if "update" in data:
@@ -391,6 +1119,7 @@ class Robot:
391
1119
  else:
392
1120
  continue
393
1121
 
1122
+
394
1123
  message_id = None
395
1124
  if update.get("type") == "NewMessage":
396
1125
  message_id = update.get("new_message", {}).get("message_id")
@@ -398,17 +1127,21 @@ class Robot:
398
1127
  message_id = update.get("inline_message", {}).get("message_id")
399
1128
  elif "message_id" in update:
400
1129
  message_id = update.get("message_id")
1130
+
401
1131
 
402
- if message_id and not self._is_duplicate(str(received_at_str)):
403
- await self._process_update(update)
1132
+ dup_ok = True
1133
+ if ignore_duplicate_messages:
1134
+ key = str(received_at_str) if received_at_str else str(message_id)
1135
+ dup_ok = (not self._is_duplicate(str(key))) if key else True
404
1136
 
1137
+ if message_id and dup_ok:
1138
+ received_updates.append(update)
405
1139
  else:
406
-
407
- get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=100)
1140
+ get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=update_limit)
1141
+ received_updates = []
408
1142
  if get_updates_response and get_updates_response.get("data"):
409
1143
  updates = get_updates_response["data"].get("updates", [])
410
1144
  self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
411
-
412
1145
  for update in updates:
413
1146
  message_id = None
414
1147
  if update.get("type") == "NewMessage":
@@ -417,18 +1150,138 @@ class Robot:
417
1150
  message_id = update.get("inline_message", {}).get("message_id")
418
1151
  elif "message_id" in update:
419
1152
  message_id = update.get("message_id")
1153
+
1154
+ dup_ok = True
1155
+ if ignore_duplicate_messages:
1156
+ dup_ok = (not self._is_duplicate(str(message_id))) if message_id else True
1157
+ if message_id and dup_ok:
1158
+ received_updates.append(update)
1159
+
1160
+
1161
+ if not received_updates:
1162
+ if pause_on_idle and sleep_time == 0:
1163
+ await asyncio.sleep(0.05)
1164
+ else:
1165
+ await asyncio.sleep(sleep_time)
1166
+ if not loop_forever and max_runtime is None:
1167
+ break
1168
+ continue
1169
+
1170
+
1171
+ for update in received_updates:
1172
+ if require_admin_rights:
1173
+
1174
+ sender, _ = _get_sender_and_chat(update)
1175
+ if hasattr(self, "is_admin") and callable(getattr(self, "is_admin")):
1176
+ try:
1177
+ if not await self.is_admin(sender) if asyncio.iscoroutinefunction(self.is_admin) else not self.is_admin(sender):
1178
+ skipped_count += 1
1179
+ continue
1180
+ except Exception:
1181
+ pass
1182
+
1183
+ if process_in_background:
1184
+ try:
1185
+ queue.put_nowait(update)
1186
+ enqueued_count += 1
1187
+ except asyncio.QueueFull:
420
1188
 
421
- if message_id and not self._is_duplicate(str(message_id)):
422
- await self._process_update(update)
1189
+ skipped_count += 1
1190
+ if save_unprocessed_updates:
1191
+ unprocessed_storage.append(update)
1192
+ else:
1193
+ try:
1194
+ if sem:
1195
+ async with sem:
1196
+ ok = await _process(update)
1197
+ else:
1198
+ ok = await _process(update)
1199
+ processed_count += 1 if ok else 0
1200
+ skipped_count += 0 if ok else 1
1201
+ except Exception as e:
1202
+ await _handle_error(e, {"stage": "inline_process", "update": update})
1203
+ error_count += 1
1204
+ if save_unprocessed_updates:
1205
+ unprocessed_storage.append(update)
1206
+ if stop_on_error or (max_errors and error_count >= max_errors):
1207
+ raise
1208
+
1209
+
1210
+ if process_in_background and queue.qsize() > 0:
1211
+ await asyncio.sleep(0)
1212
+
1213
+
1214
+ if debug:
1215
+ _log(f"Loop stats — processed: {processed_count}, enqueued: {enqueued_count}, skipped: {skipped_count}, queue: {queue.qsize() if queue else 0}", "debug")
1216
+
1217
+
1218
+ await asyncio.sleep(sleep_time)
423
1219
 
424
- await asyncio.sleep(0)
425
1220
  except Exception as e:
426
- print(f"❌ Error in run loop: {e}")
427
- await asyncio.sleep(5)
1221
+ await _handle_error(e, {"stage": "run_loop"})
1222
+ error_count += 1
1223
+ if stop_on_error or (max_errors and error_count >= max_errors):
1224
+ break
1225
+ await asyncio.sleep(retry_delay)
1226
+
1227
+
1228
+ if not loop_forever and max_runtime is None:
1229
+ break
1230
+
428
1231
  finally:
1232
+
1233
+ if process_in_background and queue:
1234
+ try:
1235
+ await queue.join()
1236
+ except Exception:
1237
+ pass
1238
+ for w in active_workers:
1239
+ w.cancel()
1240
+
1241
+ for w in active_workers:
1242
+ try:
1243
+ await w
1244
+ except Exception:
1245
+ pass
1246
+
1247
+
429
1248
  if self._aiohttp_session:
430
1249
  await self._aiohttp_session.close()
1250
+
1251
+
1252
+ stats = {
1253
+ "processed": processed_count,
1254
+ "skipped": skipped_count,
1255
+ "enqueued": enqueued_count,
1256
+ "errors": error_count,
1257
+ "uptime_sec": round(time.time() - start_ts, 3),
1258
+ }
1259
+ if metrics_enabled and metrics_handler:
1260
+ try:
1261
+ if asyncio.iscoroutinefunction(metrics_handler):
1262
+ await metrics_handler(stats)
1263
+ else:
1264
+ metrics_handler(stats)
1265
+ except Exception:
1266
+ pass
1267
+
1268
+ if shutdown_hook:
1269
+ try:
1270
+ if asyncio.iscoroutinefunction(shutdown_hook):
1271
+ await shutdown_hook(stats)
1272
+ else:
1273
+ shutdown_hook(stats)
1274
+ except Exception:
1275
+ pass
1276
+
431
1277
  print("Bot stopped and session closed.")
1278
+
1279
+
1280
+ if auto_restart:
1281
+
1282
+
1283
+ _log("Auto-restart requested. You can call run(...) again as needed.", "warning")
1284
+
432
1285
 
433
1286
  async def send_message(
434
1287
  self,
@@ -488,6 +1341,93 @@ class Robot:
488
1341
  member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
489
1342
  return user_id in member_guids
490
1343
  return False
1344
+ async def send_button_join(
1345
+ self,
1346
+ chat_id,
1347
+ title_button : Union[str, list],
1348
+ username : Union[str, list],
1349
+ text,
1350
+ reply_to_message_id=None,
1351
+ id="None"):
1352
+ from .button import InlineBuilder
1353
+ builder = InlineBuilder()
1354
+
1355
+ if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1356
+ for t, u in zip(title_button, username):
1357
+ builder = builder.row(
1358
+ InlineBuilder().button_join_channel(
1359
+ text=t,
1360
+ id=id,
1361
+ username=u
1362
+ )
1363
+ )
1364
+ elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
1365
+ for u in username:
1366
+ builder = builder.row(
1367
+ InlineBuilder().button_join_channel(
1368
+ text=title_button,
1369
+ id=id,
1370
+ username=u
1371
+ )
1372
+ )
1373
+ else:
1374
+ builder = builder.row(
1375
+ InlineBuilder().button_join_channel(
1376
+ text=title_button,
1377
+ id=id,
1378
+ username=username
1379
+ )
1380
+ )
1381
+ return await self.send_message(
1382
+ chat_id=chat_id,
1383
+ text=text,
1384
+ inline_keypad=builder.build(),
1385
+ reply_to_message_id=reply_to_message_id
1386
+ )
1387
+ async def send_button_link(
1388
+ self,
1389
+ chat_id,
1390
+ title_button: Union[str, list],
1391
+ url: Union[str, list],
1392
+ text,
1393
+ reply_to_message_id=None,
1394
+ id="None"
1395
+ ):
1396
+ from .button import InlineBuilder
1397
+ builder = InlineBuilder()
1398
+ if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
1399
+ for t, u in zip(title_button, url):
1400
+ builder = builder.row(
1401
+ InlineBuilder().button_url_link(
1402
+ text=t,
1403
+ id=id,
1404
+ url=u
1405
+ )
1406
+ )
1407
+ elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
1408
+ for u in url:
1409
+ builder = builder.row(
1410
+ InlineBuilder().button_url_link(
1411
+ text=title_button,
1412
+ id=id,
1413
+ url=u
1414
+ )
1415
+ )
1416
+ else:
1417
+ builder = builder.row(
1418
+ InlineBuilder().button_url_link(
1419
+ text=title_button,
1420
+ id=id,
1421
+ url=url
1422
+ )
1423
+ )
1424
+
1425
+ return await self.send_message(
1426
+ chat_id=chat_id,
1427
+ text=text,
1428
+ inline_keypad=builder.build(),
1429
+ reply_to_message_id=reply_to_message_id
1430
+ )
491
1431
 
492
1432
  def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
493
1433