Rubka 6.5.2__tar.gz → 6.6.2__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.

Potentially problematic release.


This version of Rubka might be problematic. Click here for more details.

Files changed (46) hide show
  1. {rubka-6.5.2 → rubka-6.6.2}/PKG-INFO +1 -1
  2. {rubka-6.5.2 → rubka-6.6.2}/Rubka.egg-info/PKG-INFO +1 -1
  3. {rubka-6.5.2 → rubka-6.6.2}/rubka/api.py +3 -1
  4. {rubka-6.5.2 → rubka-6.6.2}/rubka/asynco.py +54 -204
  5. {rubka-6.5.2 → rubka-6.6.2}/rubka/filters.py +34 -46
  6. {rubka-6.5.2 → rubka-6.6.2}/setup.py +1 -1
  7. {rubka-6.5.2 → rubka-6.6.2}/README.md +0 -0
  8. {rubka-6.5.2 → rubka-6.6.2}/Rubka.egg-info/SOURCES.txt +0 -0
  9. {rubka-6.5.2 → rubka-6.6.2}/Rubka.egg-info/dependency_links.txt +0 -0
  10. {rubka-6.5.2 → rubka-6.6.2}/Rubka.egg-info/requires.txt +0 -0
  11. {rubka-6.5.2 → rubka-6.6.2}/Rubka.egg-info/top_level.txt +0 -0
  12. {rubka-6.5.2 → rubka-6.6.2}/rubka/__init__.py +0 -0
  13. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/__init__.py +0 -0
  14. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/client/__init__.py +0 -0
  15. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/client/client.py +0 -0
  16. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/crypto/__init__.py +0 -0
  17. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/crypto/crypto.py +0 -0
  18. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/enums.py +0 -0
  19. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/exceptions.py +0 -0
  20. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/methods/__init__.py +0 -0
  21. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/methods/methods.py +0 -0
  22. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/network/__init__.py +0 -0
  23. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/network/helper.py +0 -0
  24. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/network/network.py +0 -0
  25. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/network/socket.py +0 -0
  26. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/sessions/__init__.py +0 -0
  27. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/sessions/sessions.py +0 -0
  28. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/types/__init__.py +0 -0
  29. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/types/socket/__init__.py +0 -0
  30. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/types/socket/message.py +0 -0
  31. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/utils/__init__.py +0 -0
  32. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/utils/configs.py +0 -0
  33. {rubka-6.5.2 → rubka-6.6.2}/rubka/adaptorrubka/utils/utils.py +0 -0
  34. {rubka-6.5.2 → rubka-6.6.2}/rubka/button.py +0 -0
  35. {rubka-6.5.2 → rubka-6.6.2}/rubka/config.py +0 -0
  36. {rubka-6.5.2 → rubka-6.6.2}/rubka/context.py +0 -0
  37. {rubka-6.5.2 → rubka-6.6.2}/rubka/decorators.py +0 -0
  38. {rubka-6.5.2 → rubka-6.6.2}/rubka/exceptions.py +0 -0
  39. {rubka-6.5.2 → rubka-6.6.2}/rubka/jobs.py +0 -0
  40. {rubka-6.5.2 → rubka-6.6.2}/rubka/keyboards.py +0 -0
  41. {rubka-6.5.2 → rubka-6.6.2}/rubka/keypad.py +0 -0
  42. {rubka-6.5.2 → rubka-6.6.2}/rubka/logger.py +0 -0
  43. {rubka-6.5.2 → rubka-6.6.2}/rubka/rubino.py +0 -0
  44. {rubka-6.5.2 → rubka-6.6.2}/rubka/update.py +0 -0
  45. {rubka-6.5.2 → rubka-6.6.2}/rubka/utils.py +0 -0
  46. {rubka-6.5.2 → rubka-6.6.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rubka
3
- Version: 6.5.2
3
+ Version: 6.6.2
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: 6.5.2
3
+ Version: 6.6.2
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
@@ -1669,9 +1669,11 @@ class Robot:
1669
1669
  self,
1670
1670
  chat_id: str,
1671
1671
  message_id: str,
1672
- inline_keypad: Dict[str, Any]
1672
+ inline_keypad: Dict[str, Any],
1673
+ text : str = None
1673
1674
  ) -> Dict[str, Any]:
1674
1675
  """Edit inline keypad of a message."""
1676
+ if text is not None:self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
1675
1677
  return self._post("editMessageKeypad", {
1676
1678
  "chat_id": chat_id,
1677
1679
  "message_id": message_id,
@@ -1,31 +1,19 @@
1
- import asyncio
2
- import aiohttp
3
- import aiofiles
1
+ import asyncio,aiohttp,aiofiles,time,datetime,json,tempfile,os,sys,subprocess,mimetypes
4
2
  from typing import List, Optional, Dict, Any, Literal, Callable, Union
5
3
  from .exceptions import APIRequestError
6
4
  from .adaptorrubka import Client as Client_get
7
5
  from .logger import logger
8
6
  from . import filters
9
- try:
10
- from .context import Message, InlineMessage
11
- except (ImportError, ModuleNotFoundError):
12
- from context import Message, InlineMessage
7
+ try:from .context import Message, InlineMessage
8
+ except (ImportError, ModuleNotFoundError):from context import Message, InlineMessage
13
9
  class FeatureNotAvailableError(Exception):
14
10
  pass
15
11
 
16
12
  from tqdm.asyncio import tqdm
17
13
  from urllib.parse import urlparse, parse_qs
18
14
  class InvalidTokenError(Exception):pass
19
- import mimetypes
20
15
  from pathlib import Path
21
- import time
22
- import datetime,json
23
- import tempfile
24
16
  from tqdm import tqdm
25
- import os
26
- import sys
27
- import subprocess
28
-
29
17
  API_URL = "https://botapi.rubika.ir/v3"
30
18
 
31
19
  def install_package(package_name: str) -> bool:
@@ -35,31 +23,7 @@ def install_package(package_name: str) -> bool:
35
23
  return True
36
24
  except Exception:
37
25
  return False
38
- class MessageResponse:
39
- def __init__(self, resp: dict, chat_id: str, file_id: str,
40
- type_file: str, reply_to_message_id: str,
41
- disable_notification: bool, text: str = None,
42
- chat_keypad=None, inline_keypad=None, chat_keypad_type="None"):
43
-
44
- self.status = resp.get("status")
45
- self.status_det = resp.get("status_det")
46
- self.file_id = file_id
47
- self.message_id = resp["data"].get("message_id")
48
- self.send_to_chat_id = chat_id
49
- self.reply_to_message_id = reply_to_message_id
50
- self.disable_notification = disable_notification
51
- self.type_file = type_file
52
- self.text = text
53
- self.chat_keypad = chat_keypad
54
- self.inline_keypad = inline_keypad
55
- self.chat_keypad_type = chat_keypad_type
56
- self.raw_response = resp
57
-
58
- def to_dict(self):
59
- return self.__dict__
60
-
61
- def to_json(self):
62
- return json.dumps(self.__dict__, ensure_ascii=False, indent=4)
26
+
63
27
  def get_importlib_metadata():
64
28
  """Dynamically imports and returns metadata functions from importlib."""
65
29
  try:
@@ -124,12 +88,40 @@ def show_last_six_words(text: str) -> str:
124
88
  """Returns the last 6 characters of a stripped string."""
125
89
  text = text.strip()
126
90
  return text[-6:]
127
-
91
+ class AttrDict(dict):
92
+ def __getattr__(self, item):
93
+ value = self.get(item)
94
+ if isinstance(value, dict):
95
+ return AttrDict(value)
96
+ return value
128
97
 
129
98
  class Robot:
130
99
  """
131
- Main async class to interact with Rubika Bot API.
132
- Initialized with a bot token.
100
+ Main asynchronous class to interact with the Rubika Bot API.
101
+
102
+ This class handles sending and receiving messages, inline queries, callbacks,
103
+ and manages sessions and API interactions. It is initialized with a bot token
104
+ and provides multiple optional parameters for configuration.
105
+
106
+ Attributes:
107
+ token (str): Bot token used for authentication with Rubika Bot API.
108
+ session_name (str | None): Optional session name for storing session data.
109
+ auth (str | None): Optional authentication string for advanced features.
110
+ Key (str | None): Optional key for additional authorization if required.
111
+ platform (str): Platform type, default is 'web'.
112
+ web_hook (str | None): Optional webhook URL for receiving updates.
113
+ timeout (int): Timeout for API requests in seconds (default 10).
114
+ show_progress (bool): Whether to show progress for long operations (default False).
115
+ Example:
116
+ ```python
117
+ import asyncio
118
+ from rubka.asynco import Robot,filters,Message
119
+ bot = Robot(token="YOUR_BOT_TOKEN")
120
+ @bot.on_message(filters.is_command.start)
121
+ async def start_command(bot: Robot, message: Message):
122
+ await message.reply("Hello!")
123
+ asyncio.run(bot.run())
124
+
133
125
  """
134
126
 
135
127
  def __init__(self, token: str, session_name: str = None, auth: str = None, Key: str = None, platform: str = "web", web_hook: str = None, timeout: int = 10, show_progress: bool = False):
@@ -173,7 +165,6 @@ class Robot:
173
165
  print(data)
174
166
  json_url = data.get('url', self.web_hook)
175
167
  print(self.web_hook)
176
-
177
168
  for endpoint_type in [
178
169
  "ReceiveUpdate",
179
170
  "ReceiveInlineMessage",
@@ -187,8 +178,6 @@ class Robot:
187
178
  except Exception as e:
188
179
  logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
189
180
  self.web_hook = None
190
-
191
-
192
181
  async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
193
182
  url = f"{API_URL}/{self.token}/{method}"
194
183
  session = await self._get_session()
@@ -201,11 +190,9 @@ class Robot:
201
190
  text_resp = await response.text()
202
191
  logger.error(f"Invalid JSON response from {method}: {text_resp}")
203
192
  raise APIRequestError(f"Invalid JSON response: {text_resp}")
204
-
205
193
  if method != "getUpdates":
206
194
  logger.debug(f"API Response from {method}: {json_resp}")
207
-
208
- return json_resp
195
+ return AttrDict(json_resp)
209
196
  except aiohttp.ClientError as e:
210
197
  logger.error(f"API request failed: {e}")
211
198
  raise APIRequestError(f"API request failed: {e}") from e
@@ -275,8 +262,6 @@ class Robot:
275
262
  return
276
263
  if not allow_locations and (message.location or message.live_location):
277
264
  return
278
-
279
-
280
265
  if message.text:
281
266
  text = message.text if case_sensitive else message.text.lower()
282
267
  if min_text_length and len(message.text) < min_text_length:
@@ -289,8 +274,6 @@ class Robot:
289
274
  return
290
275
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
291
276
  return
292
-
293
-
294
277
  if commands:
295
278
  if not message.text:
296
279
  return
@@ -299,13 +282,9 @@ class Robot:
299
282
  if cmd not in commands:
300
283
  return
301
284
  message.args = parts[1:]
302
-
303
-
304
285
  if filters and not filters(message):
305
286
  return
306
-
307
287
  return await func(bot, message)
308
-
309
288
  self._message_handlers.append({
310
289
  "func": wrapper,
311
290
  "filters": filters,
@@ -346,30 +325,20 @@ class Robot:
346
325
 
347
326
  if not message.is_channel:
348
327
  return
349
-
350
-
351
328
  if chat_id:
352
329
  if isinstance(chat_id, str) and message.chat_id != chat_id:
353
330
  return
354
331
  if isinstance(chat_id, list) and message.chat_id not in chat_id:
355
332
  return
356
-
357
-
358
333
  if sender_id:
359
334
  if isinstance(sender_id, str) and message.sender_id != sender_id:
360
335
  return
361
336
  if isinstance(sender_id, list) and message.sender_id not in sender_id:
362
337
  return
363
-
364
-
365
338
  if sender_type and message.sender_type != sender_type:
366
339
  return
367
-
368
-
369
340
  if not allow_forwarded and message.forwarded_from:
370
341
  return
371
-
372
-
373
342
  if not allow_files and message.file:
374
343
  return
375
344
  if not allow_stickers and message.sticker:
@@ -380,8 +349,6 @@ class Robot:
380
349
  return
381
350
  if not allow_locations and (message.location or message.live_location):
382
351
  return
383
-
384
-
385
352
  if message.text:
386
353
  text = message.text if case_sensitive else message.text.lower()
387
354
  if min_text_length and len(message.text) < min_text_length:
@@ -394,8 +361,6 @@ class Robot:
394
361
  return
395
362
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
396
363
  return
397
-
398
-
399
364
  if commands:
400
365
  if not message.text:
401
366
  return
@@ -403,14 +368,10 @@ class Robot:
403
368
  cmd = parts[0].lstrip("/")
404
369
  if cmd not in commands:
405
370
  return
406
- message.args = parts[1:]
407
-
408
-
371
+ message.args = parts[1:]
409
372
  if filters and not filters(message):
410
373
  return
411
-
412
374
  return await func(bot, message)
413
-
414
375
  self._message_handlers.append({
415
376
  "func": wrapper,
416
377
  "filters": filters,
@@ -448,33 +409,22 @@ class Robot:
448
409
 
449
410
  def decorator(func: Callable[[Any, Message], None]):
450
411
  async def wrapper(bot, message: Message):
451
-
452
412
  if not message.is_group:
453
413
  return
454
-
455
-
456
414
  if chat_id:
457
415
  if isinstance(chat_id, str) and message.chat_id != chat_id:
458
416
  return
459
417
  if isinstance(chat_id, list) and message.chat_id not in chat_id:
460
418
  return
461
-
462
-
463
419
  if sender_id:
464
420
  if isinstance(sender_id, str) and message.sender_id != sender_id:
465
421
  return
466
422
  if isinstance(sender_id, list) and message.sender_id not in sender_id:
467
423
  return
468
-
469
-
470
424
  if sender_type and message.sender_type != sender_type:
471
425
  return
472
-
473
-
474
426
  if not allow_forwarded and message.forwarded_from:
475
427
  return
476
-
477
-
478
428
  if not allow_files and message.file:
479
429
  return
480
430
  if not allow_stickers and message.sticker:
@@ -485,8 +435,6 @@ class Robot:
485
435
  return
486
436
  if not allow_locations and (message.location or message.live_location):
487
437
  return
488
-
489
-
490
438
  if message.text:
491
439
  text = message.text if case_sensitive else message.text.lower()
492
440
  if min_text_length and len(message.text) < min_text_length:
@@ -499,8 +447,6 @@ class Robot:
499
447
  return
500
448
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
501
449
  return
502
-
503
-
504
450
  if commands:
505
451
  if not message.text:
506
452
  return
@@ -509,13 +455,9 @@ class Robot:
509
455
  if cmd not in commands:
510
456
  return
511
457
  message.args = parts[1:]
512
-
513
-
514
458
  if filters and not filters(message):
515
459
  return
516
-
517
460
  return await func(bot, message)
518
-
519
461
  self._message_handlers.append({
520
462
  "func": wrapper,
521
463
  "filters": filters,
@@ -738,13 +680,12 @@ class Robot:
738
680
 
739
681
  async def _handle_inline_query(self, inline_message: InlineMessage):
740
682
  aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
741
-
742
683
  for handler in self._inline_query_handlers:
743
684
  if handler["button_id"] is None or handler["button_id"] == aux_button_id:
744
685
  try:
745
686
  await handler["func"](self, inline_message)
746
687
  except Exception as e:
747
- print(f"Error in inline query handler: {e}")
688
+ raise Exception(f"Error in inline query handler: {e}")
748
689
 
749
690
  def on_inline_query(self, button_id: Optional[str] = None):
750
691
  def decorator(func: Callable[[Any, InlineMessage], None]):
@@ -757,33 +698,19 @@ class Robot:
757
698
  def on_inline_query_prefix(self, prefix: str, button_id: Optional[str] = None):
758
699
  if not prefix.startswith('/'):
759
700
  prefix = '/' + prefix
760
-
761
701
  def decorator(func: Callable[[Any, InlineMessage], None]):
762
-
763
702
  async def handler_wrapper(bot_instance, inline_message: InlineMessage):
764
-
765
703
  if not inline_message.raw_data or 'text' not in inline_message.raw_data:
766
704
  return
767
-
768
705
  query_text = inline_message.raw_data['text']
769
-
770
-
771
706
  if query_text.startswith(prefix):
772
-
773
-
774
-
775
707
  try:
776
708
  await func(bot_instance, inline_message)
777
709
  except Exception as e:
778
- print(f"Error in inline query prefix handler '{prefix}': {e}")
779
-
780
-
710
+ raise Exception(f"Error in inline query prefix handler '{prefix}': {e}")
781
711
  self._inline_query_handlers.append({
782
712
  "func": handler_wrapper,
783
- "button_id": button_id
784
-
785
-
786
-
713
+ "button_id": button_id
787
714
  })
788
715
  return func
789
716
  return decorator
@@ -791,40 +718,29 @@ class Robot:
791
718
  if update.get("type") == "ReceiveQuery":
792
719
  msg = update.get("inline_message", {})
793
720
  context = InlineMessage(bot=self, raw_data=msg)
794
-
795
-
796
721
  if hasattr(self, "_callback_handlers"):
797
722
  for handler in self._callback_handlers:
798
723
  if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
799
724
  asyncio.create_task(handler["func"](self, context))
800
-
801
-
802
725
  asyncio.create_task(self._handle_inline_query(context))
803
726
  return
804
727
 
805
728
  if update.get("type") == "NewMessage":
806
729
  msg = update.get("new_message", {})
807
730
  try:
808
- if msg.get("time") and (time.time() - float(msg["time"])) > 20:
809
- return
810
- except (ValueError, TypeError):
811
- return
812
-
731
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:return
732
+ except (ValueError, TypeError):return
813
733
  context = Message(bot=self,
814
734
  chat_id=update.get("chat_id"),
815
735
  message_id=msg.get("message_id"),
816
736
  sender_id=msg.get("sender_id"),
817
737
  text=msg.get("text"),
818
738
  raw_data=msg)
819
-
820
-
821
739
  if context.aux_data and self._callback_handlers:
822
740
  for handler in self._callback_handlers:
823
741
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
824
742
  asyncio.create_task(handler["func"](self, context))
825
743
  return
826
-
827
-
828
744
  if self._message_handlers:
829
745
  for handler_info in self._message_handlers:
830
746
 
@@ -836,18 +752,12 @@ class Robot:
836
752
  if cmd not in handler_info["commands"]:
837
753
  continue
838
754
  context.args = parts[1:]
839
-
840
-
841
755
  if handler_info["filters"]:
842
756
  if not handler_info["filters"](context):
843
757
  continue
844
-
845
-
846
758
  if not handler_info["commands"] and not handler_info["filters"]:
847
759
  asyncio.create_task(handler_info["func"](self, context))
848
760
  continue
849
-
850
-
851
761
  if handler_info["commands"] or handler_info["filters"]:
852
762
  asyncio.create_task(handler_info["func"](self, context))#jaq
853
763
  continue
@@ -864,25 +774,18 @@ class Robot:
864
774
  if offset_id: params['offset_id'] = offset_id
865
775
  if limit: params['limit'] = limit
866
776
  async with session.get(self.web_hook, params=params) as response:
867
- response.raise_for_status()
868
-
777
+ response.raise_for_status()
869
778
  return await response.json()
870
779
 
871
780
  def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
872
781
  now = time.time()
873
782
  expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
874
- for mid in expired:
875
- del self._processed_message_ids[mid]
876
-
877
- if message_id in self._processed_message_ids:
878
- return True
879
-
783
+ for mid in expired:del self._processed_message_ids[mid]
784
+ if message_id in self._processed_message_ids:return True
880
785
  self._processed_message_ids[message_id] = now
881
786
  return False
882
787
 
883
788
 
884
-
885
-
886
789
  async def run(
887
790
  self,
888
791
  debug: bool = False,
@@ -1449,8 +1352,7 @@ class Robot:
1449
1352
 
1450
1353
 
1451
1354
  _log("Auto-restart requested. You can call run(...) again as needed.", "warning")
1452
-
1453
-
1355
+
1454
1356
  async def send_message(
1455
1357
  self,
1456
1358
  chat_id: str,
@@ -1460,27 +1362,21 @@ class Robot:
1460
1362
  disable_notification: bool = False,
1461
1363
  reply_to_message_id: Optional[str] = None,
1462
1364
  chat_keypad_type: Optional[Literal["New", "Removed"]] = None
1463
- ) -> Dict[str, Any]:
1365
+ ) -> Dict[str, Any]:
1464
1366
  payload = {
1465
1367
  "chat_id": chat_id,
1466
1368
  "text": text,
1467
1369
  "disable_notification": disable_notification,
1468
1370
  }
1469
-
1470
1371
  if chat_keypad:
1471
1372
  payload["chat_keypad"] = chat_keypad
1472
1373
  payload["chat_keypad_type"] = chat_keypad_type or "New"
1473
-
1474
1374
  if inline_keypad:
1475
1375
  payload["inline_keypad"] = inline_keypad
1476
-
1477
1376
  if reply_to_message_id:
1478
1377
  payload["reply_to_message_id"] = reply_to_message_id
1479
-
1480
1378
  return await self._post("sendMessage", payload)
1481
1379
 
1482
-
1483
-
1484
1380
  async def get_url_file(self,file_id):
1485
1381
  data = await self._post("getFile", {'file_id': file_id})
1486
1382
  return data.get("data").get("download_url")
@@ -1493,14 +1389,11 @@ class Robot:
1493
1389
 
1494
1390
  async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
1495
1391
  client = self._get_client()
1496
-
1497
1392
  if chat_id:
1498
1393
  chat_info_data = await self.get_chat(chat_id)
1499
1394
  chat_info = chat_info_data.get('data', {}).get('chat', {})
1500
1395
  username = chat_info.get('username')
1501
1396
  user_id = chat_info.get('user_id')
1502
-
1503
-
1504
1397
  if username:
1505
1398
  result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
1506
1399
  members = result.get('in_chat_members', [])
@@ -1519,7 +1412,6 @@ class Robot:
1519
1412
  id="None"):
1520
1413
  from .button import InlineBuilder
1521
1414
  builder = InlineBuilder()
1522
-
1523
1415
  if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1524
1416
  for t, u in zip(title_button, username):
1525
1417
  builder = builder.row(
@@ -1589,55 +1481,41 @@ class Robot:
1589
1481
  url=url
1590
1482
  )
1591
1483
  )
1592
-
1593
1484
  return await self.send_message(
1594
1485
  chat_id=chat_id,
1595
1486
  text=text,
1596
1487
  inline_keypad=builder.build(),
1597
1488
  reply_to_message_id=reply_to_message_id
1598
1489
  )
1599
-
1600
1490
  def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
1601
-
1602
1491
  client = self._get_client()
1603
1492
  return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
1604
-
1605
1493
  async def send_poll(self, chat_id: str, question: str, options: List[str]) -> Dict[str, Any]:
1606
1494
  return await self._post("sendPoll", {"chat_id": chat_id, "question": question, "options": options})
1607
-
1608
1495
  async def send_location(self, chat_id: str, latitude: str, longitude: str, disable_notification: bool = False, inline_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Removed"]] = None) -> Dict[str, Any]:
1609
1496
  payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
1610
1497
  if inline_keypad: payload["inline_keypad"] = inline_keypad
1611
1498
  if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
1612
1499
  if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
1613
1500
  return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
1614
-
1615
1501
  async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str) -> Dict[str, Any]:
1616
1502
  return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number})
1617
-
1618
1503
  async def get_chat(self, chat_id: str) -> Dict[str, Any]:
1619
1504
  return await self._post("getChat", {"chat_id": chat_id})
1620
-
1621
1505
  async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
1622
1506
  is_temp_file = False
1623
1507
  session = await self._get_session()
1624
-
1625
1508
  if isinstance(path, str) and path.startswith("http"):
1626
1509
  async with session.get(path) as response:
1627
1510
  if response.status != 200:
1628
1511
  raise Exception(f"Failed to download file from URL ({response.status})")
1629
-
1630
1512
  content = await response.read()
1631
-
1632
1513
  with tempfile.NamedTemporaryFile(delete=False) as temp_file:
1633
1514
  temp_file.write(content)
1634
1515
  path = temp_file.name
1635
1516
  is_temp_file = True
1636
-
1637
1517
  file_size = os.path.getsize(path)
1638
-
1639
1518
  progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024, desc=f'Uploading : {name}', bar_format='{l_bar}{bar:100}{r_bar}', colour='cyan', disable=not self.show_progress)
1640
-
1641
1519
  async def file_progress_generator(file_path, chunk_size=8192):
1642
1520
  async with aiofiles.open(file_path, 'rb') as f:
1643
1521
  while True:
@@ -1646,7 +1524,6 @@ class Robot:
1646
1524
  break
1647
1525
  progress_bar.update(len(chunk))
1648
1526
  yield chunk
1649
-
1650
1527
  data = aiohttp.FormData()
1651
1528
  data.add_field('file', file_progress_generator(path), filename=name, content_type='application/octet-stream')
1652
1529
  try:
@@ -1661,12 +1538,9 @@ class Robot:
1661
1538
  return json_data.get('data', {}).get('file_id')
1662
1539
  except :
1663
1540
  raise FeatureNotAvailableError(f"files is not currently supported by the server.")
1664
-
1665
-
1666
1541
  def get_extension(content_type: str) -> str:
1667
1542
  ext = mimetypes.guess_extension(content_type)
1668
1543
  return ext if ext else ''
1669
-
1670
1544
  async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
1671
1545
  """
1672
1546
  Download a file from server using its file_id with chunked transfer,
@@ -1685,16 +1559,11 @@ class Robot:
1685
1559
  Returns:
1686
1560
  bool: True if success, raises exceptions otherwise.
1687
1561
  """
1688
-
1689
1562
  try:
1690
1563
  url = await self.get_url_file(file_id)
1691
- if not url:
1692
- raise ValueError("Download URL not found in response.")
1693
- except Exception as e:
1694
- raise ValueError(f"Failed to get download URL: {e}")
1695
-
1564
+ if not url:raise ValueError("Download URL not found in response.")
1565
+ except Exception as e:raise ValueError(f"Failed to get download URL: {e}")
1696
1566
  timeout = aiohttp.ClientTimeout(total=timeout_sec)
1697
-
1698
1567
  try:
1699
1568
  async with aiohttp.ClientSession(timeout=timeout) as session:
1700
1569
  async with session.get(url) as resp:
@@ -1706,21 +1575,17 @@ class Robot:
1706
1575
  message="Failed to download file.",
1707
1576
  headers=resp.headers
1708
1577
  )
1709
-
1710
1578
  if not save_as:
1711
1579
  content_disp = resp.headers.get("Content-Disposition", "")
1712
1580
  import re
1713
1581
  match = re.search(r'filename="?([^\";]+)"?', content_disp)
1714
- if match:
1715
- save_as = match.group(1)
1582
+ if match:save_as = match.group(1)
1716
1583
  else:
1717
1584
  content_type = resp.headers.get("Content-Type", "").split(";")[0]
1718
1585
  extension = mimetypes.guess_extension(content_type) or ".bin"
1719
1586
  save_as = f"{file_id}{extension}"
1720
-
1721
1587
  total_size = int(resp.headers.get("Content-Length", 0))
1722
1588
  progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
1723
-
1724
1589
  async with aiofiles.open(save_as, "wb") as f:
1725
1590
  async for chunk in resp.content.iter_chunked(chunk_size):
1726
1591
  await f.write(chunk)
@@ -1783,7 +1648,7 @@ class Robot:
1783
1648
  "inline_keypad":inline_keypad,
1784
1649
  "chat_keypad_type":chat_keypad_type
1785
1650
  }
1786
- return result
1651
+ return AttrDict(result)
1787
1652
 
1788
1653
  async def _send_file_generic(self, media_type, chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type):
1789
1654
  if path:
@@ -1802,43 +1667,31 @@ class Robot:
1802
1667
  return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1803
1668
  async def send_music(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = "music", inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
1804
1669
  return await self._send_file_generic("File", chat_id, path, file_id, text, f"{file_name}.ogg", inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1805
-
1806
1670
  async def send_video(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
1807
1671
  return await self._send_file_generic("Video", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1808
-
1809
1672
  async def send_voice(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
1810
1673
  return await self._send_file_generic("voice", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1811
-
1812
1674
  async def send_image(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
1813
1675
  return await self._send_file_generic("Image", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1814
-
1815
1676
  async def send_gif(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
1816
1677
  return await self._send_file_generic("Gif", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1817
-
1818
1678
  async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
1819
1679
  return await self._post("forwardMessage", {"from_chat_id": from_chat_id, "message_id": message_id, "to_chat_id": to_chat_id, "disable_notification": disable_notification})
1820
-
1821
1680
  async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Dict[str, Any]:
1822
1681
  return await self._post("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text})
1823
-
1824
- async def edit_inline_keypad(self, chat_id: str, message_id: str, inline_keypad: Dict[str, Any]) -> Dict[str, Any]:
1825
- return await self._post("editMessageKeypad", {"chat_id": chat_id, "message_id": message_id, "inline_keypad": inline_keypad})
1826
-
1682
+ async def edit_inline_keypad(self,chat_id: str,message_id: str,inline_keypad: Dict[str, Any],text: str = None) -> Dict[str, Any]:
1683
+ if text is not None:await self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
1684
+ return await self._post("editMessageKeypad", {"chat_id": chat_id,"message_id": message_id,"inline_keypad": inline_keypad})
1827
1685
  async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
1828
1686
  return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
1829
-
1830
1687
  async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
1831
1688
  return await self._post("setCommands", {"bot_commands": bot_commands})
1832
-
1833
1689
  async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
1834
1690
  return await self._post("updateBotEndpoints", {"url": url, "type": type})
1835
-
1836
1691
  async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
1837
1692
  return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Removed"})
1838
-
1839
1693
  async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
1840
1694
  return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
1841
-
1842
1695
  async def get_name(self, chat_id: str) -> str:
1843
1696
  try:
1844
1697
  chat = await self.get_chat(chat_id)
@@ -1854,9 +1707,6 @@ class Robot:
1854
1707
  return title if title else "null"
1855
1708
  else:return "null"
1856
1709
  except Exception:return "null"
1857
-
1858
-
1859
-
1860
1710
  async def get_username(self, chat_id: str) -> str:
1861
1711
  chat_info = await self.get_chat(chat_id)
1862
1712
  return chat_info.get("data", {}).get("chat", {}).get("username", "None")
@@ -1,6 +1,11 @@
1
1
  from typing import Callable
2
2
  import re
3
-
3
+ class TextFilter:
4
+ def __call__(self, keyword=None):
5
+ if keyword is None:
6
+ return Filter(lambda m: getattr(m, "is_text", False))
7
+ else:
8
+ return Filter(lambda m: getattr(m, "is_text", False) and keyword in getattr(m, "text", ""))
4
9
  class Filter:
5
10
  def __init__(self, func: Callable):
6
11
  self.func = func
@@ -49,8 +54,32 @@ class Filter:
49
54
 
50
55
  def __truediv__(self, other):
51
56
  return Filter(lambda m: self(m) / (other(m) if callable(other) else other))
52
-
53
- is_text = Filter(lambda m: getattr(m, "is_text", False))
57
+ class IsCommand(Filter):
58
+ def __init__(self, commands=None):
59
+ if commands is None:
60
+ func = lambda m: getattr(m, "is_command", False)
61
+ else:
62
+ if isinstance(commands, str):
63
+ commands = [commands]
64
+ func = lambda m: getattr(m, "is_command", False) and getattr(m, "text", "").lstrip("/").split()[0] in commands
65
+
66
+ super().__init__(func)
67
+
68
+ def __getattr__(self, name: str):
69
+ return IsCommand([name])
70
+
71
+
72
+ class IsText:
73
+ def __call__(self, text=None):
74
+ if text is None:
75
+ func = lambda m: m.is_text is True and m.is_file is False
76
+ else:
77
+ if isinstance(text, str):
78
+ text = [text]
79
+ func = lambda m: m.is_text is True and m.is_file is False and m.text in text
80
+
81
+ return Filter(func)
82
+ #is_text = Filter(lambda m: getattr(m, "is_text", False))
54
83
  is_file = Filter(lambda m: getattr(m, "file", None) is not None)
55
84
  is_sticker = Filter(lambda m: getattr(m, "sticker", None) is not None)
56
85
  is_contact = Filter(lambda m: getattr(m, "contact_message", None) is not None)
@@ -59,7 +88,8 @@ is_location = Filter(lambda m: getattr(m, "location", None) is not None)
59
88
  is_live_location = Filter(lambda m: getattr(m, "live_location", None) is not None)
60
89
  has_any_media = Filter(lambda m: getattr(m, "has_any_media", False))
61
90
  has_media = Filter(lambda m: getattr(m, "has_media", False))
62
- is_command = Filter(lambda m: getattr(m, "is_command", False))
91
+ is_text = IsText()
92
+ is_command = IsCommand()
63
93
  is_user = Filter(lambda m: getattr(m, "is_user", False))
64
94
  is_private = Filter(lambda m: getattr(m, "is_private", False))
65
95
  is_group = Filter(lambda m: getattr(m, "is_group", False))
@@ -67,13 +97,8 @@ is_channel = Filter(lambda m: getattr(m, "is_channel", False))
67
97
  is_reply = Filter(lambda m: getattr(m, "is_reply", False))
68
98
  is_forwarded = Filter(lambda m: getattr(m, "is_forwarded", False))
69
99
  is_edited = Filter(lambda m: getattr(m, "is_edited", False))
70
-
71
-
72
-
73
-
74
100
  def text(keyword: str):
75
101
  return Filter(lambda m: getattr(m, "text", "") and keyword in m.text)
76
-
77
102
  def text_length(min_len: int = 0, max_len: int = None):
78
103
  def _filter(m):
79
104
  t = getattr(m, "text", "")
@@ -82,26 +107,19 @@ def text_length(min_len: int = 0, max_len: int = None):
82
107
  if max_len is not None and len(t) > max_len: return False
83
108
  return True
84
109
  return Filter(_filter)
85
-
86
110
  def text_regex(pattern: str):
87
111
  regex = re.compile(pattern)
88
112
  return Filter(lambda m: getattr(m, "text", "") and regex.search(m.text))
89
-
90
113
  def text_startswith(prefix: str):
91
114
  return Filter(lambda m: getattr(m, "text", "").startswith(prefix) if getattr(m, "text", None) else False)
92
-
93
115
  def text_endswith(suffix: str):
94
116
  return Filter(lambda m: getattr(m, "text", "").endswith(suffix) if getattr(m, "text", None) else False)
95
-
96
117
  def text_upper():
97
118
  return Filter(lambda m: getattr(m, "text", "").isupper() if getattr(m, "text", None) else False)
98
-
99
119
  def text_lower():
100
120
  return Filter(lambda m: getattr(m, "text", "").islower() if getattr(m, "text", None) else False)
101
-
102
121
  def text_digit():
103
122
  return Filter(lambda m: getattr(m, "text", "").isdigit() if getattr(m, "text", None) else False)
104
-
105
123
  def text_word_count(min_words: int = 1, max_words: int = None):
106
124
  def _filter(m):
107
125
  t = getattr(m, "text", "")
@@ -111,49 +129,28 @@ def text_word_count(min_words: int = 1, max_words: int = None):
111
129
  if max_words is not None and wc > max_words: return False
112
130
  return True
113
131
  return Filter(_filter)
114
-
115
132
  def text_contains_any(keywords: list):
116
133
  return Filter(lambda m: getattr(m, "text", "") and any(k in m.text for k in keywords))
117
-
118
134
  def text_equals(value: str):
119
135
  return Filter(lambda m: getattr(m, "text", None) == value)
120
-
121
136
  def text_not_equals(value: str):
122
137
  return Filter(lambda m: getattr(m, "text", None) != value)
123
-
124
-
125
-
126
-
127
138
  def file_size_gt(size: int):
128
139
  return Filter(lambda m: m.file and getattr(m.file, "size", 0) > size)
129
-
130
140
  def file_size_lt(size: int):
131
141
  return Filter(lambda m: m.file and getattr(m.file, "size", 0) < size)
132
-
133
142
  def file_name_contains(substring: str):
134
143
  return Filter(lambda m: m.file and substring in getattr(m.file, "file_name", ""))
135
-
136
144
  def file_extension(ext: str):
137
145
  return Filter(lambda m: m.file and getattr(m.file, "file_name", "").endswith(ext))
138
-
139
146
  def file_id_is(file_id: str):
140
147
  return Filter(lambda m: m.file and getattr(m.file, "file_id", None) == file_id)
141
-
142
-
143
-
144
-
145
148
  def sticker_id_is(sid: str):
146
149
  return Filter(lambda m: m.sticker and getattr(m.sticker, "sticker_id", None) == sid)
147
-
148
150
  def sticker_emoji_is(emoji: str):
149
151
  return Filter(lambda m: m.sticker and getattr(m.sticker, "emoji", None) == emoji)
150
-
151
-
152
-
153
-
154
152
  def poll_question_contains(keyword: str):
155
153
  return Filter(lambda m: m.poll and keyword in getattr(m.poll, "question", ""))
156
-
157
154
  def poll_option_count(min_options: int = 1, max_options: int = None):
158
155
  def _filter(m):
159
156
  if not getattr(m, "poll", None): return False
@@ -162,27 +159,18 @@ def poll_option_count(min_options: int = 1, max_options: int = None):
162
159
  if max_options is not None and len(options) > max_options: return False
163
160
  return True
164
161
  return Filter(_filter)
165
-
166
-
167
-
168
-
169
162
  def location_within(lat_min, lat_max, long_min, long_max):
170
163
  def _filter(m):
171
164
  loc = getattr(m, "location", None)
172
165
  if not loc: return False
173
166
  return lat_min <= getattr(loc, "lat", 0) <= lat_max and long_min <= getattr(loc, "long", 0) <= long_max
174
167
  return Filter(_filter)
175
-
176
168
  def live_location_within(lat_min, lat_max, long_min, long_max):
177
169
  def _filter(m):
178
170
  loc = getattr(m, "live_location", None)
179
171
  if not loc: return False
180
172
  return lat_min <= getattr(loc, "lat", 0) <= lat_max and long_min <= getattr(loc, "long", 0) <= long_max
181
173
  return Filter(_filter)
182
-
183
-
184
-
185
-
186
174
  def has_media_types(types: list):
187
175
  return Filter(lambda m: any(getattr(m, t, None) for t in types))
188
176
 
@@ -8,7 +8,7 @@ except FileNotFoundError:
8
8
 
9
9
  setup(
10
10
  name='Rubka',
11
- version='6.5.2',
11
+ version='6.6.2',
12
12
  description='A Python library for interacting with Rubika Bot API.',
13
13
  long_description=long_description,
14
14
  long_description_content_type='text/markdown',
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