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/api.py +622 -65
- rubka/asynco.py +964 -24
- rubka/button.py +32 -1
- rubka/context.py +4 -0
- rubka/update.py +489 -0
- {rubka-5.0.0.dist-info → rubka-6.4.2.dist-info}/METADATA +1 -1
- {rubka-5.0.0.dist-info → rubka-6.4.2.dist-info}/RECORD +9 -8
- {rubka-5.0.0.dist-info → rubka-6.4.2.dist-info}/WHEEL +0 -0
- {rubka-5.0.0.dist-info → rubka-6.4.2.dist-info}/top_level.txt +0 -0
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
1073
|
+
await self.geteToken()
|
|
1074
|
+
_log("Bot started running...", "info")
|
|
365
1075
|
|
|
366
1076
|
try:
|
|
367
1077
|
while True:
|
|
368
1078
|
try:
|
|
369
|
-
|
|
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 >
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
|