Rubka 4.6.2__py3-none-any.whl → 5.2.0__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 +241 -65
- rubka/asynco.py +604 -24
- rubka/button.py +109 -0
- {rubka-4.6.2.dist-info → rubka-5.2.0.dist-info}/METADATA +1 -1
- {rubka-4.6.2.dist-info → rubka-5.2.0.dist-info}/RECORD +7 -7
- {rubka-4.6.2.dist-info → rubka-5.2.0.dist-info}/WHEEL +0 -0
- {rubka-4.6.2.dist-info → rubka-5.2.0.dist-info}/top_level.txt +0 -0
rubka/api.py
CHANGED
|
@@ -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,8 +179,46 @@ 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
|
|
191
|
+
def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
192
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
193
|
+
self._message_handlers.append({
|
|
194
|
+
"func": func,
|
|
195
|
+
"filters": filters,
|
|
196
|
+
"commands": commands
|
|
197
|
+
})
|
|
198
|
+
return func
|
|
199
|
+
return decorator
|
|
175
200
|
|
|
176
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):
|
|
177
222
|
def decorator(func: Callable[[Any, Message], None]):
|
|
178
223
|
if not hasattr(self, "_callback_handlers"):
|
|
179
224
|
self._callback_handlers = []
|
|
@@ -209,6 +254,15 @@ class Robot:
|
|
|
209
254
|
if update.get("type") == "ReceiveQuery":
|
|
210
255
|
msg = update.get("inline_message", {})
|
|
211
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
|
+
|
|
212
266
|
threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
|
|
213
267
|
return
|
|
214
268
|
|
|
@@ -290,83 +344,173 @@ class Robot:
|
|
|
290
344
|
import time
|
|
291
345
|
|
|
292
346
|
|
|
293
|
-
|
|
294
|
-
print("Bot started running...")
|
|
295
|
-
self._processed_message_ids: Dict[str, float] = {}
|
|
296
|
-
|
|
297
|
-
while True:
|
|
298
|
-
try:
|
|
299
|
-
if self.web_hook:
|
|
300
|
-
updates = self.update_webhook()
|
|
301
|
-
|
|
302
|
-
if isinstance(updates, list):
|
|
303
|
-
for item in updates:
|
|
304
|
-
data = item.get("data", {})
|
|
305
|
-
|
|
306
|
-
received_at_str = item.get("received_at")
|
|
307
|
-
if received_at_str:
|
|
308
|
-
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
309
|
-
if time.time() - received_at_ts > 20:
|
|
310
|
-
continue
|
|
311
|
-
|
|
312
|
-
update = None
|
|
313
|
-
if "update" in data:
|
|
314
|
-
update = data["update"]
|
|
315
|
-
elif "inline_message" in data:
|
|
316
|
-
update = {
|
|
317
|
-
"type": "ReceiveQuery",
|
|
318
|
-
"inline_message": data["inline_message"]
|
|
319
|
-
}
|
|
320
|
-
else:
|
|
321
|
-
continue
|
|
347
|
+
import datetime
|
|
322
348
|
|
|
323
|
-
message_id = None
|
|
324
|
-
if update.get("type") == "NewMessage":
|
|
325
|
-
message_id = update.get("new_message", {}).get("message_id")
|
|
326
|
-
elif update.get("type") == "ReceiveQuery":
|
|
327
|
-
message_id = update.get("inline_message", {}).get("message_id")
|
|
328
|
-
elif "message_id" in update:
|
|
329
|
-
message_id = update.get("message_id")
|
|
330
349
|
|
|
331
|
-
|
|
332
|
-
|
|
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...")
|
|
333
382
|
|
|
334
|
-
|
|
335
|
-
|
|
383
|
+
self._processed_message_ids: Dict[str, float] = {}
|
|
384
|
+
error_count = 0
|
|
385
|
+
start_time = time.time()
|
|
336
386
|
|
|
337
|
-
|
|
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
|
|
338
418
|
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
341
432
|
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
344
461
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
elif "message_id" in update:
|
|
353
|
-
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
|
+
)
|
|
354
469
|
|
|
355
|
-
|
|
356
|
-
|
|
470
|
+
if message_id is not None:
|
|
471
|
+
message_id = str(message_id)
|
|
357
472
|
|
|
358
|
-
|
|
359
|
-
|
|
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()
|
|
360
482
|
|
|
361
|
-
self.
|
|
483
|
+
self._offset_id = updates["data"].get("next_offset_id", self._offset_id)
|
|
362
484
|
|
|
363
|
-
|
|
364
|
-
|
|
485
|
+
if sleep_time:
|
|
486
|
+
time.sleep(sleep_time)
|
|
365
487
|
|
|
366
|
-
|
|
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.")
|
|
367
513
|
|
|
368
|
-
except Exception as e:
|
|
369
|
-
print(f"❌ Error in run loop: {e}")
|
|
370
514
|
def send_message(
|
|
371
515
|
self,
|
|
372
516
|
chat_id: str,
|
|
@@ -618,6 +762,38 @@ class Robot:
|
|
|
618
762
|
data = response.json()
|
|
619
763
|
return data.get('data', {}).get('file_id')
|
|
620
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
|
+
)
|
|
621
797
|
|
|
622
798
|
def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
|
|
623
799
|
allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
|
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,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({
|
|
@@ -196,8 +199,37 @@ class Robot:
|
|
|
196
199
|
})
|
|
197
200
|
return func
|
|
198
201
|
return decorator
|
|
202
|
+
def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
203
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
204
|
+
self._message_handlers.append({
|
|
205
|
+
"func": func,
|
|
206
|
+
"filters": filters,
|
|
207
|
+
"commands": commands
|
|
208
|
+
})
|
|
209
|
+
return func
|
|
210
|
+
return decorator
|
|
199
211
|
|
|
200
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):
|
|
201
233
|
def decorator(func: Callable[[Any, Message], None]):
|
|
202
234
|
if not hasattr(self, "_callback_handlers"):
|
|
203
235
|
self._callback_handlers = []
|
|
@@ -263,6 +295,14 @@ class Robot:
|
|
|
263
295
|
if update.get("type") == "ReceiveQuery":
|
|
264
296
|
msg = update.get("inline_message", {})
|
|
265
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
|
+
|
|
266
306
|
asyncio.create_task(self._handle_inline_query(context))
|
|
267
307
|
return
|
|
268
308
|
|
|
@@ -345,22 +385,389 @@ class Robot:
|
|
|
345
385
|
return False
|
|
346
386
|
|
|
347
387
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
):
|
|
352
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
|
+
|
|
353
743
|
await check_rubka_version()
|
|
354
744
|
await self._initialize_webhook()
|
|
355
|
-
|
|
745
|
+
await self.geteToken()
|
|
746
|
+
_log("Bot started running...", "info")
|
|
356
747
|
|
|
357
748
|
try:
|
|
358
749
|
while True:
|
|
359
750
|
try:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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:
|
|
363
769
|
webhook_data = await self.update_webhook()
|
|
770
|
+
received_updates = []
|
|
364
771
|
if isinstance(webhook_data, list):
|
|
365
772
|
for item in webhook_data:
|
|
366
773
|
data = item.get("data", {})
|
|
@@ -369,10 +776,12 @@ class Robot:
|
|
|
369
776
|
if received_at_str:
|
|
370
777
|
try:
|
|
371
778
|
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
372
|
-
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")
|
|
373
782
|
continue
|
|
374
783
|
except (ValueError, TypeError):
|
|
375
|
-
pass
|
|
784
|
+
pass
|
|
376
785
|
|
|
377
786
|
update = None
|
|
378
787
|
if "update" in data:
|
|
@@ -382,6 +791,7 @@ class Robot:
|
|
|
382
791
|
else:
|
|
383
792
|
continue
|
|
384
793
|
|
|
794
|
+
|
|
385
795
|
message_id = None
|
|
386
796
|
if update.get("type") == "NewMessage":
|
|
387
797
|
message_id = update.get("new_message", {}).get("message_id")
|
|
@@ -389,17 +799,21 @@ class Robot:
|
|
|
389
799
|
message_id = update.get("inline_message", {}).get("message_id")
|
|
390
800
|
elif "message_id" in update:
|
|
391
801
|
message_id = update.get("message_id")
|
|
802
|
+
|
|
392
803
|
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
395
808
|
|
|
809
|
+
if message_id and dup_ok:
|
|
810
|
+
received_updates.append(update)
|
|
396
811
|
else:
|
|
397
|
-
|
|
398
|
-
|
|
812
|
+
get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=update_limit)
|
|
813
|
+
received_updates = []
|
|
399
814
|
if get_updates_response and get_updates_response.get("data"):
|
|
400
815
|
updates = get_updates_response["data"].get("updates", [])
|
|
401
816
|
self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
|
|
402
|
-
|
|
403
817
|
for update in updates:
|
|
404
818
|
message_id = None
|
|
405
819
|
if update.get("type") == "NewMessage":
|
|
@@ -408,18 +822,138 @@ class Robot:
|
|
|
408
822
|
message_id = update.get("inline_message", {}).get("message_id")
|
|
409
823
|
elif "message_id" in update:
|
|
410
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:
|
|
411
860
|
|
|
412
|
-
|
|
413
|
-
|
|
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)
|
|
414
891
|
|
|
415
|
-
await asyncio.sleep(0)
|
|
416
892
|
except Exception as e:
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
|
|
419
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
|
+
|
|
420
920
|
if self._aiohttp_session:
|
|
421
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
|
+
|
|
422
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
|
+
|
|
423
957
|
|
|
424
958
|
async def send_message(
|
|
425
959
|
self,
|
|
@@ -479,6 +1013,52 @@ class Robot:
|
|
|
479
1013
|
member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
|
|
480
1014
|
return user_id in member_guids
|
|
481
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
|
+
)
|
|
482
1062
|
|
|
483
1063
|
def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
|
|
484
1064
|
|
rubka/button.py
CHANGED
|
@@ -16,6 +16,115 @@ class InlineBuilder:
|
|
|
16
16
|
raise ValueError("حداقل یک دکمه باید به row داده شود")
|
|
17
17
|
self.rows.append({"buttons": list(buttons)})
|
|
18
18
|
return self
|
|
19
|
+
def button_open_chat(self, id: str, text: str, object_guid: str, object_type: str ="User") -> Dict:
|
|
20
|
+
return {
|
|
21
|
+
"id": id,
|
|
22
|
+
"type": 'Link',
|
|
23
|
+
"button_text": text,
|
|
24
|
+
"button_link": {
|
|
25
|
+
"type": 'openchat',
|
|
26
|
+
"open_chat_data": {
|
|
27
|
+
"object_guid": object_guid,
|
|
28
|
+
"object_type": object_type
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
def button_join_channel(self, id: str, text: str, username: str, ask_join: bool = False) -> Dict:
|
|
33
|
+
"""
|
|
34
|
+
Creates an inline button that prompts the user to join a Rubika channel.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
id (str): Unique identifier for the button (used for event handling).
|
|
38
|
+
text (str): The text displayed on the button.
|
|
39
|
+
username (str): The channel username (can be with or without '@').
|
|
40
|
+
ask_join (bool, optional): If True, the user will be prompted with a
|
|
41
|
+
confirmation dialog before joining.
|
|
42
|
+
Defaults to False.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: A dictionary representing the inline button, which can be passed
|
|
46
|
+
to inline keyboard builder methods.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
from rubka.button import InlineBuilder
|
|
51
|
+
|
|
52
|
+
buttons = (
|
|
53
|
+
InlineBuilder()
|
|
54
|
+
.row(
|
|
55
|
+
InlineBuilder().button_join_channel(
|
|
56
|
+
id="join_btn",
|
|
57
|
+
text="Join our channel 📢",
|
|
58
|
+
username="rubka_library",
|
|
59
|
+
ask_join=True
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
.build()
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
await message.reply_inline(
|
|
66
|
+
text="Please join our channel before using the bot.",
|
|
67
|
+
inline_keypad=buttons
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
"""
|
|
71
|
+
return {
|
|
72
|
+
"id": id,
|
|
73
|
+
"type": 'Link',
|
|
74
|
+
"button_text": text,
|
|
75
|
+
"button_link": {
|
|
76
|
+
"type": 'joinchannel',
|
|
77
|
+
"joinchannel_data": {
|
|
78
|
+
"username": username.replace("@", ""),
|
|
79
|
+
"ask_join": ask_join
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def button_url_link(self, id: str, text: str, url: str) -> Dict:
|
|
85
|
+
"""
|
|
86
|
+
Creates an inline button that opens a given URL when clicked.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
id (str): Unique identifier for the button (used for event handling if needed).
|
|
90
|
+
text (str): The text displayed on the button.
|
|
91
|
+
url (str): The destination URL that will be opened when the button is clicked.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: A dictionary representing the inline button, which can be passed
|
|
95
|
+
to inline keyboard builder methods.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
```python
|
|
99
|
+
from rubka.button import InlineBuilder
|
|
100
|
+
|
|
101
|
+
buttons = (
|
|
102
|
+
InlineBuilder()
|
|
103
|
+
.row(
|
|
104
|
+
InlineBuilder().button_url_link(
|
|
105
|
+
id="website_btn",
|
|
106
|
+
text="Visit our website 🌐",
|
|
107
|
+
url="https://api-free.ir"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
.build()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
await message.reply_inline(
|
|
114
|
+
text="Click the button below to visit our website.",
|
|
115
|
+
inline_keypad=buttons
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
"""
|
|
119
|
+
return {
|
|
120
|
+
"id": id,
|
|
121
|
+
"type": 'Link',
|
|
122
|
+
"button_text": text,
|
|
123
|
+
"button_link": {
|
|
124
|
+
"type": 'url',
|
|
125
|
+
"link_url": url
|
|
126
|
+
}
|
|
127
|
+
}
|
|
19
128
|
|
|
20
129
|
def button_simple(self, id: str, text: str) -> Dict:
|
|
21
130
|
return {"id": id, "type": "Simple", "button_text": text}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rubka
|
|
3
|
-
Version:
|
|
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,7 +1,7 @@
|
|
|
1
1
|
rubka/__init__.py,sha256=TR1DABU5Maz2eO62ZEFiwOqNU0dH6l6HZfqRUxeo4eY,194
|
|
2
|
-
rubka/api.py,sha256
|
|
3
|
-
rubka/asynco.py,sha256=
|
|
4
|
-
rubka/button.py,sha256=
|
|
2
|
+
rubka/api.py,sha256=PGTQoKqKz8DdOtHjrEi0nDkI-jsXitf5Dw0GZkpIpQ8,47144
|
|
3
|
+
rubka/asynco.py,sha256=slWprOtMDcWo95hZrWN7hO5oQAN2dAOvgSNQqcTStl0,60562
|
|
4
|
+
rubka/button.py,sha256=VLYEBczH4gevqw2cBO06X9QXZivdQ19vvDdVp99s_TY,12229
|
|
5
5
|
rubka/config.py,sha256=Bck59xkOiqioLv0GkQ1qPGnBXVctz1hKk6LT4h2EPx0,78
|
|
6
6
|
rubka/context.py,sha256=-98bKHFF3u8ZRqULpYZJVtNhaycsKUY2ZvXJHlWTX-s,18122
|
|
7
7
|
rubka/decorators.py,sha256=hGwUoE4q2ImrunJIGJ_kzGYYxQf1ueE0isadqraKEts,1157
|
|
@@ -33,7 +33,7 @@ rubka/adaptorrubka/types/socket/message.py,sha256=0WgLMZh4eow8Zn7AiSX4C3GZjQTkIg
|
|
|
33
33
|
rubka/adaptorrubka/utils/__init__.py,sha256=OgCFkXdNFh379quNwIVOAWY2NP5cIOxU5gDRRALTk4o,54
|
|
34
34
|
rubka/adaptorrubka/utils/configs.py,sha256=nMUEOJh1NqDJsf9W9PurkN_DLYjO6kKPMm923i4Jj_A,492
|
|
35
35
|
rubka/adaptorrubka/utils/utils.py,sha256=5-LioLNYX_TIbQGDeT50j7Sg9nAWH2LJUUs-iEXpsUY,8816
|
|
36
|
-
rubka-
|
|
37
|
-
rubka-
|
|
38
|
-
rubka-
|
|
39
|
-
rubka-
|
|
36
|
+
rubka-5.2.0.dist-info/METADATA,sha256=LSyZAZRQr0ogt2NA9WTNs6F_dJMfjecZT5PwN9osnKo,33335
|
|
37
|
+
rubka-5.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
rubka-5.2.0.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
|
|
39
|
+
rubka-5.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|