Rubka 5.0.0__tar.gz → 5.2.0__tar.gz
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-5.0.0 → rubka-5.2.0}/PKG-INFO +1 -1
- {rubka-5.0.0 → rubka-5.2.0}/Rubka.egg-info/PKG-INFO +1 -1
- {rubka-5.0.0 → rubka-5.2.0}/rubka/api.py +232 -65
- {rubka-5.0.0 → rubka-5.2.0}/rubka/asynco.py +596 -25
- {rubka-5.0.0 → rubka-5.2.0}/setup.py +1 -1
- {rubka-5.0.0 → rubka-5.2.0}/README.md +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/Rubka.egg-info/SOURCES.txt +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/Rubka.egg-info/dependency_links.txt +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/Rubka.egg-info/requires.txt +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/Rubka.egg-info/top_level.txt +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/client/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/client/client.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/crypto/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/crypto/crypto.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/enums.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/exceptions.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/methods/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/methods/methods.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/network/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/network/helper.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/network/network.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/network/socket.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/sessions/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/sessions/sessions.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/types/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/types/socket/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/types/socket/message.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/utils/__init__.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/utils/configs.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/adaptorrubka/utils/utils.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/button.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/config.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/context.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/decorators.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/exceptions.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/jobs.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/keyboards.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/keypad.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/logger.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/rubino.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/rubka/utils.py +0 -0
- {rubka-5.0.0 → rubka-5.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rubka
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: A Python library for interacting with Rubika Bot API.
|
|
5
5
|
Home-page: https://github.com/Mahdy-Ahmadi/Rubka
|
|
6
6
|
Download-URL: https://github.com/Mahdy-Ahmadi/rubka/blob/main/project_library.zip
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rubka
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: A Python library for interacting with Rubika Bot API.
|
|
5
5
|
Home-page: https://github.com/Mahdy-Ahmadi/Rubka
|
|
6
6
|
Download-URL: https://github.com/Mahdy-Ahmadi/rubka/blob/main/project_library.zip
|
|
@@ -17,6 +17,7 @@ import mimetypes
|
|
|
17
17
|
import re
|
|
18
18
|
import sys
|
|
19
19
|
import subprocess
|
|
20
|
+
class InvalidTokenError(Exception):pass
|
|
20
21
|
def install_package(package_name):
|
|
21
22
|
try:
|
|
22
23
|
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
@@ -109,6 +110,7 @@ class Robot:
|
|
|
109
110
|
self.show_progress = show_progress
|
|
110
111
|
self.session_name = session_name
|
|
111
112
|
self.Key = Key
|
|
113
|
+
|
|
112
114
|
self.platform = platform
|
|
113
115
|
self.web_hook = web_hook
|
|
114
116
|
self.hook = web_hook
|
|
@@ -121,6 +123,7 @@ class Robot:
|
|
|
121
123
|
self._message_handlers: List[dict] = []
|
|
122
124
|
self._callback_handlers = None
|
|
123
125
|
self._callback_handlers = []
|
|
126
|
+
self.geteToken()
|
|
124
127
|
if web_hook:
|
|
125
128
|
try:
|
|
126
129
|
json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
|
|
@@ -141,7 +144,6 @@ class Robot:
|
|
|
141
144
|
|
|
142
145
|
|
|
143
146
|
logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
|
|
144
|
-
|
|
145
147
|
def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
146
148
|
url = f"{API_URL}/{self.token}/{method}"
|
|
147
149
|
try:
|
|
@@ -163,6 +165,11 @@ class Robot:
|
|
|
163
165
|
def get_me(self) -> Dict[str, Any]:
|
|
164
166
|
"""Get info about the bot itself."""
|
|
165
167
|
return self._post("getMe", {})
|
|
168
|
+
def geteToken(self):
|
|
169
|
+
"""b"""
|
|
170
|
+
if self.get_me()['status'] != "OK":
|
|
171
|
+
raise InvalidTokenError("The provided bot token is invalid or expired.")
|
|
172
|
+
|
|
166
173
|
def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
167
174
|
def decorator(func: Callable[[Any, Message], None]):
|
|
168
175
|
self._message_handlers.append({
|
|
@@ -172,6 +179,15 @@ class Robot:
|
|
|
172
179
|
})
|
|
173
180
|
return func
|
|
174
181
|
return decorator
|
|
182
|
+
def message_handler(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
183
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
184
|
+
self._message_handlers.append({
|
|
185
|
+
"func": func,
|
|
186
|
+
"filters": filters,
|
|
187
|
+
"commands": commands
|
|
188
|
+
})
|
|
189
|
+
return func
|
|
190
|
+
return decorator
|
|
175
191
|
def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
176
192
|
def decorator(func: Callable[[Any, Message], None]):
|
|
177
193
|
self._message_handlers.append({
|
|
@@ -183,6 +199,26 @@ class Robot:
|
|
|
183
199
|
return decorator
|
|
184
200
|
|
|
185
201
|
def on_callback(self, button_id: Optional[str] = None):
|
|
202
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
203
|
+
if not hasattr(self, "_callback_handlers"):
|
|
204
|
+
self._callback_handlers = []
|
|
205
|
+
self._callback_handlers.append({
|
|
206
|
+
"func": func,
|
|
207
|
+
"button_id": button_id
|
|
208
|
+
})
|
|
209
|
+
return func
|
|
210
|
+
return decorator
|
|
211
|
+
def callback_query(self, button_id: Optional[str] = None):
|
|
212
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
213
|
+
if not hasattr(self, "_callback_handlers"):
|
|
214
|
+
self._callback_handlers = []
|
|
215
|
+
self._callback_handlers.append({
|
|
216
|
+
"func": func,
|
|
217
|
+
"button_id": button_id
|
|
218
|
+
})
|
|
219
|
+
return func
|
|
220
|
+
return decorator
|
|
221
|
+
def callback_query_handler(self, button_id: Optional[str] = None):
|
|
186
222
|
def decorator(func: Callable[[Any, Message], None]):
|
|
187
223
|
if not hasattr(self, "_callback_handlers"):
|
|
188
224
|
self._callback_handlers = []
|
|
@@ -218,6 +254,15 @@ class Robot:
|
|
|
218
254
|
if update.get("type") == "ReceiveQuery":
|
|
219
255
|
msg = update.get("inline_message", {})
|
|
220
256
|
context = InlineMessage(bot=self, raw_data=msg)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if hasattr(self, "_callback_handlers"):
|
|
260
|
+
for handler in self._callback_handlers:
|
|
261
|
+
cb_id = getattr(context.aux_data, "button_id", None)
|
|
262
|
+
if not handler["button_id"] or handler["button_id"] == cb_id:
|
|
263
|
+
threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
|
|
264
|
+
|
|
265
|
+
|
|
221
266
|
threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
|
|
222
267
|
return
|
|
223
268
|
|
|
@@ -299,83 +344,173 @@ class Robot:
|
|
|
299
344
|
import time
|
|
300
345
|
|
|
301
346
|
|
|
302
|
-
|
|
303
|
-
print("Bot started running...")
|
|
304
|
-
self._processed_message_ids: Dict[str, float] = {}
|
|
305
|
-
|
|
306
|
-
while True:
|
|
307
|
-
try:
|
|
308
|
-
if self.web_hook:
|
|
309
|
-
updates = self.update_webhook()
|
|
310
|
-
|
|
311
|
-
if isinstance(updates, list):
|
|
312
|
-
for item in updates:
|
|
313
|
-
data = item.get("data", {})
|
|
314
|
-
|
|
315
|
-
received_at_str = item.get("received_at")
|
|
316
|
-
if received_at_str:
|
|
317
|
-
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
318
|
-
if time.time() - received_at_ts > 20:
|
|
319
|
-
continue
|
|
320
|
-
|
|
321
|
-
update = None
|
|
322
|
-
if "update" in data:
|
|
323
|
-
update = data["update"]
|
|
324
|
-
elif "inline_message" in data:
|
|
325
|
-
update = {
|
|
326
|
-
"type": "ReceiveQuery",
|
|
327
|
-
"inline_message": data["inline_message"]
|
|
328
|
-
}
|
|
329
|
-
else:
|
|
330
|
-
continue
|
|
347
|
+
import datetime
|
|
331
348
|
|
|
332
|
-
message_id = None
|
|
333
|
-
if update.get("type") == "NewMessage":
|
|
334
|
-
message_id = update.get("new_message", {}).get("message_id")
|
|
335
|
-
elif update.get("type") == "ReceiveQuery":
|
|
336
|
-
message_id = update.get("inline_message", {}).get("message_id")
|
|
337
|
-
elif "message_id" in update:
|
|
338
|
-
message_id = update.get("message_id")
|
|
339
349
|
|
|
340
|
-
|
|
341
|
-
|
|
350
|
+
def run(
|
|
351
|
+
self,
|
|
352
|
+
debug=False,
|
|
353
|
+
sleep_time=0.1,
|
|
354
|
+
webhook_timeout=20,
|
|
355
|
+
update_limit=100,
|
|
356
|
+
retry_delay=5,
|
|
357
|
+
stop_on_error=False,
|
|
358
|
+
max_errors=None,
|
|
359
|
+
max_runtime=None,
|
|
360
|
+
allowed_update_types=None,
|
|
361
|
+
ignore_duplicate_messages=True,
|
|
362
|
+
skip_inline_queries=False,
|
|
363
|
+
skip_channel_posts=False,
|
|
364
|
+
skip_service_messages=False,
|
|
365
|
+
skip_edited_messages=False,
|
|
366
|
+
skip_bot_messages=False,
|
|
367
|
+
log_file=None,
|
|
368
|
+
print_exceptions=True,
|
|
369
|
+
error_handler=None,
|
|
370
|
+
shutdown_hook=None,
|
|
371
|
+
log_to_console=True,
|
|
372
|
+
custom_update_fetcher=None,
|
|
373
|
+
custom_update_processor=None,
|
|
374
|
+
message_filter=None,
|
|
375
|
+
notify_on_error=False,
|
|
376
|
+
notification_handler=None,
|
|
377
|
+
):
|
|
378
|
+
import time
|
|
379
|
+
from typing import Dict
|
|
380
|
+
if debug:
|
|
381
|
+
print("[DEBUG] Bot started running server...")
|
|
342
382
|
|
|
343
|
-
|
|
344
|
-
|
|
383
|
+
self._processed_message_ids: Dict[str, float] = {}
|
|
384
|
+
error_count = 0
|
|
385
|
+
start_time = time.time()
|
|
345
386
|
|
|
346
|
-
|
|
387
|
+
try:
|
|
388
|
+
while True:
|
|
389
|
+
try:
|
|
390
|
+
|
|
391
|
+
if max_runtime and (time.time() - start_time > max_runtime):
|
|
392
|
+
if debug:
|
|
393
|
+
print("[DEBUG] Max runtime reached, stopping...")
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if self.web_hook:
|
|
398
|
+
updates = custom_update_fetcher() if custom_update_fetcher else self.update_webhook()
|
|
399
|
+
if isinstance(updates, list):
|
|
400
|
+
for item in updates:
|
|
401
|
+
data = item.get("data", {})
|
|
402
|
+
received_at_str = item.get("received_at")
|
|
403
|
+
|
|
404
|
+
if received_at_str:
|
|
405
|
+
try:
|
|
406
|
+
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
407
|
+
if time.time() - received_at_ts > webhook_timeout:
|
|
408
|
+
continue
|
|
409
|
+
except (ValueError, TypeError):
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
update = data.get("update") or (
|
|
413
|
+
{"type": "ReceiveQuery", "inline_message": data.get("inline_message")}
|
|
414
|
+
if "inline_message" in data else None
|
|
415
|
+
)
|
|
416
|
+
if not update:
|
|
417
|
+
continue
|
|
347
418
|
|
|
348
|
-
|
|
349
|
-
|
|
419
|
+
|
|
420
|
+
if skip_inline_queries and update.get("type") == "ReceiveQuery":
|
|
421
|
+
continue
|
|
422
|
+
if skip_channel_posts and update.get("type") == "ChannelPost":
|
|
423
|
+
continue
|
|
424
|
+
if skip_service_messages and update.get("type") == "ServiceMessage":
|
|
425
|
+
continue
|
|
426
|
+
if skip_edited_messages and update.get("type") == "EditedMessage":
|
|
427
|
+
continue
|
|
428
|
+
if skip_bot_messages and update.get("from", {}).get("is_bot"):
|
|
429
|
+
continue
|
|
430
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
431
|
+
continue
|
|
350
432
|
|
|
351
|
-
|
|
352
|
-
|
|
433
|
+
message_id = (
|
|
434
|
+
update.get("new_message", {}).get("message_id")
|
|
435
|
+
if update.get("type") == "NewMessage"
|
|
436
|
+
else update.get("inline_message", {}).get("message_id")
|
|
437
|
+
if update.get("type") == "ReceiveQuery"
|
|
438
|
+
else update.get("message_id")
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if message_id is not None:
|
|
442
|
+
message_id = str(message_id)
|
|
443
|
+
|
|
444
|
+
if message_id and (not ignore_duplicate_messages or not self._is_duplicate(received_at_str)):
|
|
445
|
+
if message_filter and not message_filter(update):
|
|
446
|
+
continue
|
|
447
|
+
if custom_update_processor:
|
|
448
|
+
custom_update_processor(update)
|
|
449
|
+
else:
|
|
450
|
+
self._process_update(update)
|
|
451
|
+
if message_id:
|
|
452
|
+
self._processed_message_ids[message_id] = time.time()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
else:
|
|
456
|
+
updates = custom_update_fetcher() if custom_update_fetcher else self.get_updates(offset_id=self._offset_id, limit=update_limit)
|
|
457
|
+
if updates and updates.get("data"):
|
|
458
|
+
for update in updates["data"].get("updates", []):
|
|
459
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
460
|
+
continue
|
|
353
461
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
elif "message_id" in update:
|
|
362
|
-
message_id = update.get("message_id")
|
|
462
|
+
message_id = (
|
|
463
|
+
update.get("new_message", {}).get("message_id")
|
|
464
|
+
if update.get("type") == "NewMessage"
|
|
465
|
+
else update.get("inline_message", {}).get("message_id")
|
|
466
|
+
if update.get("type") == "ReceiveQuery"
|
|
467
|
+
else update.get("message_id")
|
|
468
|
+
)
|
|
363
469
|
|
|
364
|
-
|
|
365
|
-
|
|
470
|
+
if message_id is not None:
|
|
471
|
+
message_id = str(message_id)
|
|
366
472
|
|
|
367
|
-
|
|
368
|
-
|
|
473
|
+
if message_id and (not ignore_duplicate_messages or not self._is_duplicate(message_id)):
|
|
474
|
+
if message_filter and not message_filter(update):
|
|
475
|
+
continue
|
|
476
|
+
if custom_update_processor:
|
|
477
|
+
custom_update_processor(update)
|
|
478
|
+
else:
|
|
479
|
+
self._process_update(update)
|
|
480
|
+
if message_id:
|
|
481
|
+
self._processed_message_ids[message_id] = time.time()
|
|
369
482
|
|
|
370
|
-
self.
|
|
483
|
+
self._offset_id = updates["data"].get("next_offset_id", self._offset_id)
|
|
371
484
|
|
|
372
|
-
|
|
373
|
-
|
|
485
|
+
if sleep_time:
|
|
486
|
+
time.sleep(sleep_time)
|
|
374
487
|
|
|
375
|
-
|
|
488
|
+
except Exception as e:
|
|
489
|
+
error_count += 1
|
|
490
|
+
if log_to_console:
|
|
491
|
+
print(f"Error in run loop: {e}")
|
|
492
|
+
if log_file:
|
|
493
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
494
|
+
f.write(f"{datetime.datetime.now()} - ERROR: {e}\n")
|
|
495
|
+
if print_exceptions:
|
|
496
|
+
import traceback
|
|
497
|
+
traceback.print_exc()
|
|
498
|
+
if error_handler:
|
|
499
|
+
error_handler(e)
|
|
500
|
+
if notify_on_error and notification_handler:
|
|
501
|
+
notification_handler(e)
|
|
502
|
+
|
|
503
|
+
if max_errors and error_count >= max_errors and stop_on_error:
|
|
504
|
+
break
|
|
505
|
+
|
|
506
|
+
time.sleep(retry_delay)
|
|
507
|
+
|
|
508
|
+
finally:
|
|
509
|
+
if shutdown_hook:
|
|
510
|
+
shutdown_hook()
|
|
511
|
+
if debug:
|
|
512
|
+
print("Bot stopped and session closed.")
|
|
376
513
|
|
|
377
|
-
except Exception as e:
|
|
378
|
-
print(f"❌ Error in run loop: {e}")
|
|
379
514
|
def send_message(
|
|
380
515
|
self,
|
|
381
516
|
chat_id: str,
|
|
@@ -627,6 +762,38 @@ class Robot:
|
|
|
627
762
|
data = response.json()
|
|
628
763
|
return data.get('data', {}).get('file_id')
|
|
629
764
|
|
|
765
|
+
def send_button_join(self, chat_id, title_button, username, text, reply_to_message_id=None, id="None"):
|
|
766
|
+
from .button import InlineBuilder
|
|
767
|
+
return self.send_message(
|
|
768
|
+
chat_id=chat_id,
|
|
769
|
+
text=text,
|
|
770
|
+
inline_keypad=InlineBuilder()
|
|
771
|
+
.row(
|
|
772
|
+
InlineBuilder()
|
|
773
|
+
.button_join_channel(
|
|
774
|
+
text=title_button,
|
|
775
|
+
id=id,
|
|
776
|
+
username=username
|
|
777
|
+
)
|
|
778
|
+
).build(),
|
|
779
|
+
reply_to_message_id=reply_to_message_id
|
|
780
|
+
)
|
|
781
|
+
def send_button_url(self, chat_id, title_button, url, text, reply_to_message_id=None, id="None"):
|
|
782
|
+
from .button import InlineBuilder
|
|
783
|
+
return self.send_message(
|
|
784
|
+
chat_id=chat_id,
|
|
785
|
+
text=text,
|
|
786
|
+
inline_keypad=InlineBuilder()
|
|
787
|
+
.row(
|
|
788
|
+
InlineBuilder()
|
|
789
|
+
.button_url_link(
|
|
790
|
+
text=title_button,
|
|
791
|
+
id=id,
|
|
792
|
+
url=url
|
|
793
|
+
)
|
|
794
|
+
).build(),
|
|
795
|
+
reply_to_message_id=reply_to_message_id
|
|
796
|
+
)
|
|
630
797
|
|
|
631
798
|
def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
|
|
632
799
|
allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
|
|
@@ -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,7 +186,10 @@ 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
|
-
|
|
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.")
|
|
190
193
|
def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
191
194
|
def decorator(func: Callable[[Any, Message], None]):
|
|
192
195
|
self._message_handlers.append({
|
|
@@ -204,9 +207,29 @@ class Robot:
|
|
|
204
207
|
"commands": commands
|
|
205
208
|
})
|
|
206
209
|
return func
|
|
207
|
-
return decorator
|
|
210
|
+
return decorator
|
|
208
211
|
|
|
209
212
|
def on_callback(self, button_id: Optional[str] = None):
|
|
213
|
+
def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
|
|
214
|
+
if not hasattr(self, "_callback_handlers"):
|
|
215
|
+
self._callback_handlers = []
|
|
216
|
+
self._callback_handlers.append({
|
|
217
|
+
"func": func,
|
|
218
|
+
"button_id": button_id
|
|
219
|
+
})
|
|
220
|
+
return func
|
|
221
|
+
return decorator
|
|
222
|
+
def callback_query_handler(self, button_id: Optional[str] = None):
|
|
223
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
224
|
+
if not hasattr(self, "_callback_handlers"):
|
|
225
|
+
self._callback_handlers = []
|
|
226
|
+
self._callback_handlers.append({
|
|
227
|
+
"func": func,
|
|
228
|
+
"button_id": button_id
|
|
229
|
+
})
|
|
230
|
+
return func
|
|
231
|
+
return decorator
|
|
232
|
+
def callback_query(self, button_id: Optional[str] = None):
|
|
210
233
|
def decorator(func: Callable[[Any, Message], None]):
|
|
211
234
|
if not hasattr(self, "_callback_handlers"):
|
|
212
235
|
self._callback_handlers = []
|
|
@@ -272,6 +295,14 @@ class Robot:
|
|
|
272
295
|
if update.get("type") == "ReceiveQuery":
|
|
273
296
|
msg = update.get("inline_message", {})
|
|
274
297
|
context = InlineMessage(bot=self, raw_data=msg)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if hasattr(self, "_callback_handlers"):
|
|
301
|
+
for handler in self._callback_handlers:
|
|
302
|
+
if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
|
|
303
|
+
asyncio.create_task(handler["func"](self, context))
|
|
304
|
+
|
|
305
|
+
|
|
275
306
|
asyncio.create_task(self._handle_inline_query(context))
|
|
276
307
|
return
|
|
277
308
|
|
|
@@ -354,22 +385,389 @@ class Robot:
|
|
|
354
385
|
return False
|
|
355
386
|
|
|
356
387
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
async def run(
|
|
391
|
+
self,
|
|
392
|
+
debug: bool = False,
|
|
393
|
+
sleep_time: float = 0.2,
|
|
394
|
+
webhook_timeout: int = 20,
|
|
395
|
+
update_limit: int = 100,
|
|
396
|
+
retry_delay: float = 5.0,
|
|
397
|
+
stop_on_error: bool = False,
|
|
398
|
+
max_errors: int = 0,
|
|
399
|
+
auto_restart: bool = False,
|
|
400
|
+
max_runtime: float | None = None,
|
|
401
|
+
loop_forever: bool = True,
|
|
402
|
+
allowed_update_types: list[str] | None = None,
|
|
403
|
+
ignore_duplicate_messages: bool = True,
|
|
404
|
+
skip_inline_queries: bool = False,
|
|
405
|
+
skip_channel_posts: bool = False,
|
|
406
|
+
skip_service_messages: bool = False,
|
|
407
|
+
skip_edited_messages: bool = False,
|
|
408
|
+
skip_bot_messages: bool = False,
|
|
409
|
+
log_file: str | None = None,
|
|
410
|
+
log_level: str = "info",
|
|
411
|
+
print_exceptions: bool = True,
|
|
412
|
+
error_handler=None,
|
|
413
|
+
shutdown_hook=None,
|
|
414
|
+
save_unprocessed_updates: bool = False,
|
|
415
|
+
log_to_console: bool = True,
|
|
416
|
+
rate_limit: float | None = None,
|
|
417
|
+
max_message_size: int | None = None,
|
|
418
|
+
ignore_users: set[str] | None = None,
|
|
419
|
+
ignore_groups: set[str] | None = None,
|
|
420
|
+
require_auth_token: bool = False,
|
|
421
|
+
only_private_chats: bool = False,
|
|
422
|
+
only_groups: bool = False,
|
|
423
|
+
require_admin_rights: bool = False,
|
|
424
|
+
custom_update_fetcher=None,
|
|
425
|
+
custom_update_processor=None,
|
|
426
|
+
process_in_background: bool = False,
|
|
427
|
+
max_queue_size: int = 1000,
|
|
428
|
+
thread_workers: int = 3,
|
|
429
|
+
message_filter=None,
|
|
430
|
+
pause_on_idle: bool = False,
|
|
431
|
+
max_concurrent_tasks: int | None = None,
|
|
432
|
+
metrics_enabled: bool = False,
|
|
433
|
+
metrics_handler=None,
|
|
434
|
+
notify_on_error: bool = False,
|
|
435
|
+
notification_handler=None,
|
|
436
|
+
watchdog_timeout: float | None = None,
|
|
437
|
+
):
|
|
361
438
|
"""
|
|
439
|
+
Starts the bot's main execution loop with extensive configuration options.
|
|
440
|
+
|
|
441
|
+
This function handles:
|
|
442
|
+
- Update fetching and processing with optional filters for types and sources.
|
|
443
|
+
- Error handling, retry mechanisms, and automatic restart options.
|
|
444
|
+
- Logging to console and/or files with configurable log levels.
|
|
445
|
+
- Message filtering based on users, groups, chat types, admin rights, and more.
|
|
446
|
+
- Custom update fetchers and processors for advanced use cases.
|
|
447
|
+
- Background processing with threading and task concurrency controls.
|
|
448
|
+
- Metrics collection and error notifications.
|
|
449
|
+
- Optional runtime limits, sleep delays, rate limiting, and watchdog monitoring.
|
|
450
|
+
|
|
451
|
+
Parameters
|
|
452
|
+
----------
|
|
453
|
+
debug : bool
|
|
454
|
+
Enable debug mode for detailed logging and runtime checks.
|
|
455
|
+
sleep_time : float
|
|
456
|
+
Delay between update fetch cycles (seconds).
|
|
457
|
+
webhook_timeout : int
|
|
458
|
+
Timeout for webhook requests (seconds).
|
|
459
|
+
update_limit : int
|
|
460
|
+
Maximum updates to fetch per request.
|
|
461
|
+
retry_delay : float
|
|
462
|
+
Delay before retrying after failure (seconds).
|
|
463
|
+
stop_on_error : bool
|
|
464
|
+
Stop bot on unhandled errors.
|
|
465
|
+
max_errors : int
|
|
466
|
+
Maximum consecutive errors before stopping (0 = unlimited).
|
|
467
|
+
auto_restart : bool
|
|
468
|
+
Automatically restart the bot if it stops unexpectedly.
|
|
469
|
+
max_runtime : float | None
|
|
470
|
+
Maximum runtime in seconds before stopping.
|
|
471
|
+
loop_forever : bool
|
|
472
|
+
Keep the bot running continuously.
|
|
473
|
+
|
|
474
|
+
allowed_update_types : list[str] | None
|
|
475
|
+
Limit processing to specific update types.
|
|
476
|
+
ignore_duplicate_messages : bool
|
|
477
|
+
Skip identical messages.
|
|
478
|
+
skip_inline_queries : bool
|
|
479
|
+
Ignore inline query updates.
|
|
480
|
+
skip_channel_posts : bool
|
|
481
|
+
Ignore channel post updates.
|
|
482
|
+
skip_service_messages : bool
|
|
483
|
+
Ignore service messages.
|
|
484
|
+
skip_edited_messages : bool
|
|
485
|
+
Ignore edited messages.
|
|
486
|
+
skip_bot_messages : bool
|
|
487
|
+
Ignore messages from other bots.
|
|
488
|
+
|
|
489
|
+
log_file : str | None
|
|
490
|
+
File path for logging.
|
|
491
|
+
log_level : str
|
|
492
|
+
Logging level (debug, info, warning, error).
|
|
493
|
+
print_exceptions : bool
|
|
494
|
+
Print exceptions to console.
|
|
495
|
+
error_handler : callable | None
|
|
496
|
+
Custom function to handle errors.
|
|
497
|
+
shutdown_hook : callable | None
|
|
498
|
+
Function to execute on shutdown.
|
|
499
|
+
save_unprocessed_updates : bool
|
|
500
|
+
Save updates that failed processing.
|
|
501
|
+
log_to_console : bool
|
|
502
|
+
Enable/disable console logging.
|
|
503
|
+
|
|
504
|
+
rate_limit : float | None
|
|
505
|
+
Minimum delay between processing updates from the same user/group.
|
|
506
|
+
max_message_size : int | None
|
|
507
|
+
Maximum allowed message size.
|
|
508
|
+
ignore_users : set[str] | None
|
|
509
|
+
User IDs to ignore.
|
|
510
|
+
ignore_groups : set[str] | None
|
|
511
|
+
Group IDs to ignore.
|
|
512
|
+
require_auth_token : bool
|
|
513
|
+
Require users to provide authentication token.
|
|
514
|
+
only_private_chats : bool
|
|
515
|
+
Process only private chats.
|
|
516
|
+
only_groups : bool
|
|
517
|
+
Process only group chats.
|
|
518
|
+
require_admin_rights : bool
|
|
519
|
+
Process only if sender is admin.
|
|
520
|
+
|
|
521
|
+
custom_update_fetcher : callable | None
|
|
522
|
+
Custom update fetching function.
|
|
523
|
+
custom_update_processor : callable | None
|
|
524
|
+
Custom update processing function.
|
|
525
|
+
process_in_background : bool
|
|
526
|
+
Run processing in background threads.
|
|
527
|
+
max_queue_size : int
|
|
528
|
+
Maximum updates in processing queue.
|
|
529
|
+
thread_workers : int
|
|
530
|
+
Number of background worker threads.
|
|
531
|
+
message_filter : callable | None
|
|
532
|
+
Function to filter messages.
|
|
533
|
+
pause_on_idle : bool
|
|
534
|
+
Pause processing if idle.
|
|
535
|
+
max_concurrent_tasks : int | None
|
|
536
|
+
Maximum concurrent processing tasks.
|
|
537
|
+
|
|
538
|
+
metrics_enabled : bool
|
|
539
|
+
Enable metrics collection.
|
|
540
|
+
metrics_handler : callable | None
|
|
541
|
+
Function to handle metrics.
|
|
542
|
+
notify_on_error : bool
|
|
543
|
+
Send notifications on errors.
|
|
544
|
+
notification_handler : callable | None
|
|
545
|
+
Function to send error notifications.
|
|
546
|
+
watchdog_timeout : float | None
|
|
547
|
+
Maximum idle time before triggering watchdog restart.
|
|
548
|
+
"""
|
|
549
|
+
import asyncio, time, datetime, traceback
|
|
550
|
+
from collections import deque
|
|
551
|
+
def _log(msg: str, level: str = "info"):
|
|
552
|
+
level_order = {"debug": 10, "info": 20, "warning": 30, "error": 40}
|
|
553
|
+
if level not in level_order:
|
|
554
|
+
level = "info"
|
|
555
|
+
if level_order[level] < level_order.get(log_level, 20):
|
|
556
|
+
return
|
|
557
|
+
line = f"[{level.upper()}] {datetime.datetime.now().isoformat()} - {msg}"
|
|
558
|
+
if log_to_console:
|
|
559
|
+
print(msg)
|
|
560
|
+
if log_file:
|
|
561
|
+
try:
|
|
562
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
563
|
+
f.write(line + "\n")
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
def _get_sender_and_chat(update: dict):
|
|
568
|
+
|
|
569
|
+
sender = None
|
|
570
|
+
chat = None
|
|
571
|
+
t = update.get("type")
|
|
572
|
+
if t == "NewMessage":
|
|
573
|
+
nm = update.get("new_message", {})
|
|
574
|
+
sender = nm.get("author_object_guid") or nm.get("author_guid") or nm.get("from_id")
|
|
575
|
+
chat = nm.get("object_guid") or nm.get("chat_id")
|
|
576
|
+
elif t == "ReceiveQuery":
|
|
577
|
+
im = update.get("inline_message", {})
|
|
578
|
+
sender = im.get("author_object_guid") or im.get("author_guid")
|
|
579
|
+
chat = im.get("object_guid") or im.get("chat_id")
|
|
580
|
+
else:
|
|
581
|
+
sender = update.get("author_guid") or update.get("from_id")
|
|
582
|
+
chat = update.get("object_guid") or update.get("chat_id")
|
|
583
|
+
return str(sender) if sender is not None else None, str(chat) if chat is not None else None
|
|
584
|
+
|
|
585
|
+
def _is_group_chat(chat_guid: str | None) -> bool | None:
|
|
586
|
+
|
|
587
|
+
if chat_guid is None:
|
|
588
|
+
return None
|
|
589
|
+
if hasattr(self, "_is_group_chat") and callable(getattr(self, "_is_group_chat")):
|
|
590
|
+
try:
|
|
591
|
+
return bool(self._is_group_chat(chat_guid))
|
|
592
|
+
except Exception:
|
|
593
|
+
return None
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
async def _maybe_notify(err: Exception, context: dict):
|
|
597
|
+
if notify_on_error and notification_handler:
|
|
598
|
+
try:
|
|
599
|
+
if asyncio.iscoroutinefunction(notification_handler):
|
|
600
|
+
await notification_handler(err, context)
|
|
601
|
+
else:
|
|
602
|
+
|
|
603
|
+
notification_handler(err, context)
|
|
604
|
+
except Exception:
|
|
605
|
+
pass
|
|
606
|
+
|
|
607
|
+
async def _handle_error(err: Exception, context: dict):
|
|
608
|
+
if print_exceptions:
|
|
609
|
+
_log("Exception occurred:\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__)), "error")
|
|
610
|
+
else:
|
|
611
|
+
_log(f"Exception occurred: {err}", "error")
|
|
612
|
+
await _maybe_notify(err, context)
|
|
613
|
+
if error_handler:
|
|
614
|
+
try:
|
|
615
|
+
if asyncio.iscoroutinefunction(error_handler):
|
|
616
|
+
await error_handler(err, context)
|
|
617
|
+
else:
|
|
618
|
+
error_handler(err, context)
|
|
619
|
+
except Exception as e2:
|
|
620
|
+
_log(f"Error in error_handler: {e2}", "error")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
rate_window = deque()
|
|
624
|
+
def _rate_ok():
|
|
625
|
+
if rate_limit is None or rate_limit <= 0:
|
|
626
|
+
return True
|
|
627
|
+
now = time.time()
|
|
628
|
+
|
|
629
|
+
while rate_window and now - rate_window[0] > 1.0:
|
|
630
|
+
rate_window.popleft()
|
|
631
|
+
if len(rate_window) < int(rate_limit):
|
|
632
|
+
rate_window.append(now)
|
|
633
|
+
return True
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
queue = asyncio.Queue(maxsize=max_queue_size) if process_in_background else None
|
|
638
|
+
active_workers = []
|
|
639
|
+
|
|
640
|
+
sem = asyncio.Semaphore(max_concurrent_tasks) if max_concurrent_tasks and max_concurrent_tasks > 0 else None
|
|
641
|
+
|
|
642
|
+
async def _process(update: dict):
|
|
643
|
+
|
|
644
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
t = update.get("type")
|
|
649
|
+
if skip_inline_queries and t == "ReceiveQuery":
|
|
650
|
+
return False
|
|
651
|
+
if skip_service_messages and t == "ServiceMessage":
|
|
652
|
+
return False
|
|
653
|
+
if skip_edited_messages and t == "EditMessage":
|
|
654
|
+
return False
|
|
655
|
+
if skip_channel_posts and t == "ChannelPost":
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
sender, chat = _get_sender_and_chat(update)
|
|
660
|
+
if ignore_users and sender and sender in ignore_users:
|
|
661
|
+
return False
|
|
662
|
+
if ignore_groups and chat and chat in ignore_groups:
|
|
663
|
+
return False
|
|
664
|
+
if require_auth_token and not getattr(self, "_has_auth_token", False):
|
|
665
|
+
return False
|
|
666
|
+
if only_private_chats:
|
|
667
|
+
is_group = _is_group_chat(chat)
|
|
668
|
+
if is_group is True:
|
|
669
|
+
return False
|
|
670
|
+
if only_groups:
|
|
671
|
+
is_group = _is_group_chat(chat)
|
|
672
|
+
if is_group is False:
|
|
673
|
+
return False
|
|
674
|
+
if skip_bot_messages and getattr(self, "_is_bot_guid", None) and sender == self._is_bot_guid:
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
if max_message_size is not None and max_message_size > 0:
|
|
678
|
+
|
|
679
|
+
content = None
|
|
680
|
+
if t == "NewMessage":
|
|
681
|
+
content = (update.get("new_message") or {}).get("text")
|
|
682
|
+
elif t == "ReceiveQuery":
|
|
683
|
+
content = (update.get("inline_message") or {}).get("text")
|
|
684
|
+
elif "text" in update:
|
|
685
|
+
content = update.get("text")
|
|
686
|
+
if content and isinstance(content, str) and len(content) > max_message_size:
|
|
687
|
+
return False
|
|
688
|
+
|
|
689
|
+
if message_filter:
|
|
690
|
+
try:
|
|
691
|
+
if not message_filter(update):
|
|
692
|
+
return False
|
|
693
|
+
except Exception:
|
|
694
|
+
|
|
695
|
+
pass
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
if not _rate_ok():
|
|
699
|
+
return False
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
if custom_update_processor:
|
|
703
|
+
if asyncio.iscoroutinefunction(custom_update_processor):
|
|
704
|
+
await custom_update_processor(update)
|
|
705
|
+
else:
|
|
706
|
+
|
|
707
|
+
await asyncio.get_running_loop().run_in_executor(None, custom_update_processor, update)
|
|
708
|
+
else:
|
|
709
|
+
|
|
710
|
+
await self._process_update(update)
|
|
711
|
+
return True
|
|
712
|
+
|
|
713
|
+
async def _worker():
|
|
714
|
+
while True:
|
|
715
|
+
update = await queue.get()
|
|
716
|
+
try:
|
|
717
|
+
if sem:
|
|
718
|
+
async with sem:
|
|
719
|
+
await _process(update)
|
|
720
|
+
else:
|
|
721
|
+
await _process(update)
|
|
722
|
+
except Exception as e:
|
|
723
|
+
await _handle_error(e, {"stage": "worker_process", "update": update})
|
|
724
|
+
finally:
|
|
725
|
+
queue.task_done()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
start_ts = time.time()
|
|
729
|
+
error_count = 0
|
|
730
|
+
last_loop_tick = time.time()
|
|
731
|
+
processed_count = 0
|
|
732
|
+
skipped_count = 0
|
|
733
|
+
enqueued_count = 0
|
|
734
|
+
unprocessed_storage = []
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
if process_in_background:
|
|
738
|
+
n_workers = max(1, int(thread_workers))
|
|
739
|
+
for _ in range(n_workers):
|
|
740
|
+
active_workers.append(asyncio.create_task(_worker()))
|
|
741
|
+
|
|
742
|
+
|
|
362
743
|
await check_rubka_version()
|
|
363
744
|
await self._initialize_webhook()
|
|
364
|
-
|
|
745
|
+
await self.geteToken()
|
|
746
|
+
_log("Bot started running...", "info")
|
|
365
747
|
|
|
366
748
|
try:
|
|
367
749
|
while True:
|
|
368
750
|
try:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
751
|
+
|
|
752
|
+
if max_runtime is not None and (time.time() - start_ts) >= max_runtime:
|
|
753
|
+
_log("Max runtime reached. Stopping loop.", "warning")
|
|
754
|
+
break
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
now = time.time()
|
|
758
|
+
if watchdog_timeout and (now - last_loop_tick) > watchdog_timeout:
|
|
759
|
+
_log(f"Watchdog triggered (> {watchdog_timeout}s)", "warning")
|
|
760
|
+
if auto_restart:
|
|
761
|
+
break
|
|
762
|
+
last_loop_tick = now
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
received_updates = None
|
|
766
|
+
if custom_update_fetcher:
|
|
767
|
+
received_updates = await custom_update_fetcher()
|
|
768
|
+
elif self.web_hook:
|
|
372
769
|
webhook_data = await self.update_webhook()
|
|
770
|
+
received_updates = []
|
|
373
771
|
if isinstance(webhook_data, list):
|
|
374
772
|
for item in webhook_data:
|
|
375
773
|
data = item.get("data", {})
|
|
@@ -378,10 +776,12 @@ class Robot:
|
|
|
378
776
|
if received_at_str:
|
|
379
777
|
try:
|
|
380
778
|
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
381
|
-
if time.time() - received_at_ts >
|
|
779
|
+
if time.time() - received_at_ts > webhook_timeout:
|
|
780
|
+
if debug:
|
|
781
|
+
_log(f"Skipped old webhook update ({received_at_str})", "debug")
|
|
382
782
|
continue
|
|
383
783
|
except (ValueError, TypeError):
|
|
384
|
-
pass
|
|
784
|
+
pass
|
|
385
785
|
|
|
386
786
|
update = None
|
|
387
787
|
if "update" in data:
|
|
@@ -391,6 +791,7 @@ class Robot:
|
|
|
391
791
|
else:
|
|
392
792
|
continue
|
|
393
793
|
|
|
794
|
+
|
|
394
795
|
message_id = None
|
|
395
796
|
if update.get("type") == "NewMessage":
|
|
396
797
|
message_id = update.get("new_message", {}).get("message_id")
|
|
@@ -398,17 +799,21 @@ class Robot:
|
|
|
398
799
|
message_id = update.get("inline_message", {}).get("message_id")
|
|
399
800
|
elif "message_id" in update:
|
|
400
801
|
message_id = update.get("message_id")
|
|
802
|
+
|
|
401
803
|
|
|
402
|
-
|
|
403
|
-
|
|
804
|
+
dup_ok = True
|
|
805
|
+
if ignore_duplicate_messages:
|
|
806
|
+
key = str(received_at_str) if received_at_str else str(message_id)
|
|
807
|
+
dup_ok = (not self._is_duplicate(str(key))) if key else True
|
|
404
808
|
|
|
809
|
+
if message_id and dup_ok:
|
|
810
|
+
received_updates.append(update)
|
|
405
811
|
else:
|
|
406
|
-
|
|
407
|
-
|
|
812
|
+
get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=update_limit)
|
|
813
|
+
received_updates = []
|
|
408
814
|
if get_updates_response and get_updates_response.get("data"):
|
|
409
815
|
updates = get_updates_response["data"].get("updates", [])
|
|
410
816
|
self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
|
|
411
|
-
|
|
412
817
|
for update in updates:
|
|
413
818
|
message_id = None
|
|
414
819
|
if update.get("type") == "NewMessage":
|
|
@@ -417,18 +822,138 @@ class Robot:
|
|
|
417
822
|
message_id = update.get("inline_message", {}).get("message_id")
|
|
418
823
|
elif "message_id" in update:
|
|
419
824
|
message_id = update.get("message_id")
|
|
825
|
+
|
|
826
|
+
dup_ok = True
|
|
827
|
+
if ignore_duplicate_messages:
|
|
828
|
+
dup_ok = (not self._is_duplicate(str(message_id))) if message_id else True
|
|
829
|
+
if message_id and dup_ok:
|
|
830
|
+
received_updates.append(update)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
if not received_updates:
|
|
834
|
+
if pause_on_idle and sleep_time == 0:
|
|
835
|
+
await asyncio.sleep(0.05)
|
|
836
|
+
else:
|
|
837
|
+
await asyncio.sleep(sleep_time)
|
|
838
|
+
if not loop_forever and max_runtime is None:
|
|
839
|
+
break
|
|
840
|
+
continue
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
for update in received_updates:
|
|
844
|
+
if require_admin_rights:
|
|
845
|
+
|
|
846
|
+
sender, _ = _get_sender_and_chat(update)
|
|
847
|
+
if hasattr(self, "is_admin") and callable(getattr(self, "is_admin")):
|
|
848
|
+
try:
|
|
849
|
+
if not await self.is_admin(sender) if asyncio.iscoroutinefunction(self.is_admin) else not self.is_admin(sender):
|
|
850
|
+
skipped_count += 1
|
|
851
|
+
continue
|
|
852
|
+
except Exception:
|
|
853
|
+
pass
|
|
854
|
+
|
|
855
|
+
if process_in_background:
|
|
856
|
+
try:
|
|
857
|
+
queue.put_nowait(update)
|
|
858
|
+
enqueued_count += 1
|
|
859
|
+
except asyncio.QueueFull:
|
|
420
860
|
|
|
421
|
-
|
|
422
|
-
|
|
861
|
+
skipped_count += 1
|
|
862
|
+
if save_unprocessed_updates:
|
|
863
|
+
unprocessed_storage.append(update)
|
|
864
|
+
else:
|
|
865
|
+
try:
|
|
866
|
+
if sem:
|
|
867
|
+
async with sem:
|
|
868
|
+
ok = await _process(update)
|
|
869
|
+
else:
|
|
870
|
+
ok = await _process(update)
|
|
871
|
+
processed_count += 1 if ok else 0
|
|
872
|
+
skipped_count += 0 if ok else 1
|
|
873
|
+
except Exception as e:
|
|
874
|
+
await _handle_error(e, {"stage": "inline_process", "update": update})
|
|
875
|
+
error_count += 1
|
|
876
|
+
if save_unprocessed_updates:
|
|
877
|
+
unprocessed_storage.append(update)
|
|
878
|
+
if stop_on_error or (max_errors and error_count >= max_errors):
|
|
879
|
+
raise
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
if process_in_background and queue.qsize() > 0:
|
|
883
|
+
await asyncio.sleep(0)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
if debug:
|
|
887
|
+
_log(f"Loop stats — processed: {processed_count}, enqueued: {enqueued_count}, skipped: {skipped_count}, queue: {queue.qsize() if queue else 0}", "debug")
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
await asyncio.sleep(sleep_time)
|
|
423
891
|
|
|
424
|
-
await asyncio.sleep(0)
|
|
425
892
|
except Exception as e:
|
|
426
|
-
|
|
427
|
-
|
|
893
|
+
await _handle_error(e, {"stage": "run_loop"})
|
|
894
|
+
error_count += 1
|
|
895
|
+
if stop_on_error or (max_errors and error_count >= max_errors):
|
|
896
|
+
break
|
|
897
|
+
await asyncio.sleep(retry_delay)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
if not loop_forever and max_runtime is None:
|
|
901
|
+
break
|
|
902
|
+
|
|
428
903
|
finally:
|
|
904
|
+
|
|
905
|
+
if process_in_background and queue:
|
|
906
|
+
try:
|
|
907
|
+
await queue.join()
|
|
908
|
+
except Exception:
|
|
909
|
+
pass
|
|
910
|
+
for w in active_workers:
|
|
911
|
+
w.cancel()
|
|
912
|
+
|
|
913
|
+
for w in active_workers:
|
|
914
|
+
try:
|
|
915
|
+
await w
|
|
916
|
+
except Exception:
|
|
917
|
+
pass
|
|
918
|
+
|
|
919
|
+
|
|
429
920
|
if self._aiohttp_session:
|
|
430
921
|
await self._aiohttp_session.close()
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
stats = {
|
|
925
|
+
"processed": processed_count,
|
|
926
|
+
"skipped": skipped_count,
|
|
927
|
+
"enqueued": enqueued_count,
|
|
928
|
+
"errors": error_count,
|
|
929
|
+
"uptime_sec": round(time.time() - start_ts, 3),
|
|
930
|
+
}
|
|
931
|
+
if metrics_enabled and metrics_handler:
|
|
932
|
+
try:
|
|
933
|
+
if asyncio.iscoroutinefunction(metrics_handler):
|
|
934
|
+
await metrics_handler(stats)
|
|
935
|
+
else:
|
|
936
|
+
metrics_handler(stats)
|
|
937
|
+
except Exception:
|
|
938
|
+
pass
|
|
939
|
+
|
|
940
|
+
if shutdown_hook:
|
|
941
|
+
try:
|
|
942
|
+
if asyncio.iscoroutinefunction(shutdown_hook):
|
|
943
|
+
await shutdown_hook(stats)
|
|
944
|
+
else:
|
|
945
|
+
shutdown_hook(stats)
|
|
946
|
+
except Exception:
|
|
947
|
+
pass
|
|
948
|
+
|
|
431
949
|
print("Bot stopped and session closed.")
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
if auto_restart:
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
_log("Auto-restart requested. You can call run(...) again as needed.", "warning")
|
|
956
|
+
|
|
432
957
|
|
|
433
958
|
async def send_message(
|
|
434
959
|
self,
|
|
@@ -488,6 +1013,52 @@ class Robot:
|
|
|
488
1013
|
member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
|
|
489
1014
|
return user_id in member_guids
|
|
490
1015
|
return False
|
|
1016
|
+
async def send_button_join(
|
|
1017
|
+
self,
|
|
1018
|
+
chat_id,
|
|
1019
|
+
title_button,
|
|
1020
|
+
username,
|
|
1021
|
+
text,
|
|
1022
|
+
reply_to_message_id=None,
|
|
1023
|
+
id="None"):
|
|
1024
|
+
from .button import InlineBuilder
|
|
1025
|
+
return await self.send_message(
|
|
1026
|
+
chat_id=chat_id,
|
|
1027
|
+
text=text,
|
|
1028
|
+
inline_keypad=InlineBuilder()
|
|
1029
|
+
.row(
|
|
1030
|
+
InlineBuilder()
|
|
1031
|
+
.button_join_channel(
|
|
1032
|
+
text=title_button,
|
|
1033
|
+
id=id,
|
|
1034
|
+
username=username
|
|
1035
|
+
)
|
|
1036
|
+
).build(),
|
|
1037
|
+
reply_to_message_id=reply_to_message_id
|
|
1038
|
+
)
|
|
1039
|
+
async def send_button_link(
|
|
1040
|
+
self,
|
|
1041
|
+
chat_id,
|
|
1042
|
+
title_button,
|
|
1043
|
+
url,
|
|
1044
|
+
text,
|
|
1045
|
+
reply_to_message_id=None,
|
|
1046
|
+
id="None"):
|
|
1047
|
+
from .button import InlineBuilder
|
|
1048
|
+
return await self.send_message(
|
|
1049
|
+
chat_id=chat_id,
|
|
1050
|
+
text=text,
|
|
1051
|
+
inline_keypad=InlineBuilder()
|
|
1052
|
+
.row(
|
|
1053
|
+
InlineBuilder()
|
|
1054
|
+
.button_url_link(
|
|
1055
|
+
text=title_button,
|
|
1056
|
+
id=id,
|
|
1057
|
+
url=url
|
|
1058
|
+
)
|
|
1059
|
+
).build(),
|
|
1060
|
+
reply_to_message_id=reply_to_message_id
|
|
1061
|
+
)
|
|
491
1062
|
|
|
492
1063
|
def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
|
|
493
1064
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|