Rubka 6.4.8__py3-none-any.whl → 6.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rubka/api.py CHANGED
@@ -3,6 +3,7 @@ from typing import List, Optional, Dict, Any, Literal
3
3
  from .exceptions import APIRequestError
4
4
  from .adaptorrubka import Client as Client_get
5
5
  from .logger import logger
6
+ from . import filters
6
7
  from typing import Callable
7
8
  from .context import Message,InlineMessage
8
9
  from typing import Optional, Union, Literal, Dict, Any
@@ -489,14 +490,28 @@ class Robot:
489
490
 
490
491
  return decorator
491
492
 
492
- def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
493
+ def on_message(
494
+ self,
495
+ filters: Optional[Callable[[Message], bool]] = None,
496
+ commands: Optional[List[str]] = None
497
+ ):
493
498
  def decorator(func: Callable[[Any, Message], None]):
499
+ def wrapper(bot, message: Message):
500
+ if filters and not filters(message):
501
+ return
502
+ if commands:
503
+ if not getattr(message, "is_command", False):
504
+ return
505
+ cmd = message.text.split()[0].lstrip("/") if message.text else ""
506
+ if cmd not in commands:
507
+ return
508
+ return func(bot, message)
494
509
  self._message_handlers.append({
495
- "func": func,
510
+ "func": wrapper,
496
511
  "filters": filters,
497
512
  "commands": commands
498
513
  })
499
- return func
514
+ return wrapper
500
515
  return decorator
501
516
  def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
502
517
  def decorator(func: Callable[[Any, Message], None]):
@@ -1654,9 +1669,11 @@ class Robot:
1654
1669
  self,
1655
1670
  chat_id: str,
1656
1671
  message_id: str,
1657
- inline_keypad: Dict[str, Any]
1672
+ inline_keypad: Dict[str, Any],
1673
+ text : str = None
1658
1674
  ) -> Dict[str, Any]:
1659
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})
1660
1677
  return self._post("editMessageKeypad", {
1661
1678
  "chat_id": chat_id,
1662
1679
  "message_id": message_id,
rubka/asynco.py CHANGED
@@ -1,30 +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
- try:
9
- from .context import Message, InlineMessage
10
- except (ImportError, ModuleNotFoundError):
11
- from context import Message, InlineMessage
6
+ from . import filters
7
+ try:from .context import Message, InlineMessage
8
+ except (ImportError, ModuleNotFoundError):from context import Message, InlineMessage
12
9
  class FeatureNotAvailableError(Exception):
13
10
  pass
14
11
 
15
12
  from tqdm.asyncio import tqdm
16
13
  from urllib.parse import urlparse, parse_qs
17
14
  class InvalidTokenError(Exception):pass
18
- import mimetypes
19
15
  from pathlib import Path
20
- import time
21
- import datetime,json
22
- import tempfile
23
16
  from tqdm import tqdm
24
- import os
25
- import sys
26
- import subprocess
27
-
28
17
  API_URL = "https://botapi.rubika.ir/v3"
29
18
 
30
19
  def install_package(package_name: str) -> bool:
@@ -34,31 +23,7 @@ def install_package(package_name: str) -> bool:
34
23
  return True
35
24
  except Exception:
36
25
  return False
37
- class MessageResponse:
38
- def __init__(self, resp: dict, chat_id: str, file_id: str,
39
- type_file: str, reply_to_message_id: str,
40
- disable_notification: bool, text: str = None,
41
- chat_keypad=None, inline_keypad=None, chat_keypad_type="None"):
42
-
43
- self.status = resp.get("status")
44
- self.status_det = resp.get("status_det")
45
- self.file_id = file_id
46
- self.message_id = resp["data"].get("message_id")
47
- self.send_to_chat_id = chat_id
48
- self.reply_to_message_id = reply_to_message_id
49
- self.disable_notification = disable_notification
50
- self.type_file = type_file
51
- self.text = text
52
- self.chat_keypad = chat_keypad
53
- self.inline_keypad = inline_keypad
54
- self.chat_keypad_type = chat_keypad_type
55
- self.raw_response = resp
56
-
57
- def to_dict(self):
58
- return self.__dict__
59
-
60
- def to_json(self):
61
- return json.dumps(self.__dict__, ensure_ascii=False, indent=4)
26
+
62
27
  def get_importlib_metadata():
63
28
  """Dynamically imports and returns metadata functions from importlib."""
64
29
  try:
@@ -123,12 +88,40 @@ def show_last_six_words(text: str) -> str:
123
88
  """Returns the last 6 characters of a stripped string."""
124
89
  text = text.strip()
125
90
  return text[-6:]
126
-
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
127
97
 
128
98
  class Robot:
129
99
  """
130
- Main async class to interact with Rubika Bot API.
131
- 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
+
132
125
  """
133
126
 
134
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):
@@ -172,7 +165,6 @@ class Robot:
172
165
  print(data)
173
166
  json_url = data.get('url', self.web_hook)
174
167
  print(self.web_hook)
175
-
176
168
  for endpoint_type in [
177
169
  "ReceiveUpdate",
178
170
  "ReceiveInlineMessage",
@@ -186,8 +178,6 @@ class Robot:
186
178
  except Exception as e:
187
179
  logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
188
180
  self.web_hook = None
189
-
190
-
191
181
  async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
192
182
  url = f"{API_URL}/{self.token}/{method}"
193
183
  session = await self._get_session()
@@ -200,11 +190,9 @@ class Robot:
200
190
  text_resp = await response.text()
201
191
  logger.error(f"Invalid JSON response from {method}: {text_resp}")
202
192
  raise APIRequestError(f"Invalid JSON response: {text_resp}")
203
-
204
193
  if method != "getUpdates":
205
194
  logger.debug(f"API Response from {method}: {json_resp}")
206
-
207
- return json_resp
195
+ return AttrDict(json_resp)
208
196
  except aiohttp.ClientError as e:
209
197
  logger.error(f"API request failed: {e}")
210
198
  raise APIRequestError(f"API request failed: {e}") from e
@@ -274,8 +262,6 @@ class Robot:
274
262
  return
275
263
  if not allow_locations and (message.location or message.live_location):
276
264
  return
277
-
278
-
279
265
  if message.text:
280
266
  text = message.text if case_sensitive else message.text.lower()
281
267
  if min_text_length and len(message.text) < min_text_length:
@@ -288,8 +274,6 @@ class Robot:
288
274
  return
289
275
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
290
276
  return
291
-
292
-
293
277
  if commands:
294
278
  if not message.text:
295
279
  return
@@ -298,13 +282,9 @@ class Robot:
298
282
  if cmd not in commands:
299
283
  return
300
284
  message.args = parts[1:]
301
-
302
-
303
285
  if filters and not filters(message):
304
286
  return
305
-
306
287
  return await func(bot, message)
307
-
308
288
  self._message_handlers.append({
309
289
  "func": wrapper,
310
290
  "filters": filters,
@@ -345,30 +325,20 @@ class Robot:
345
325
 
346
326
  if not message.is_channel:
347
327
  return
348
-
349
-
350
328
  if chat_id:
351
329
  if isinstance(chat_id, str) and message.chat_id != chat_id:
352
330
  return
353
331
  if isinstance(chat_id, list) and message.chat_id not in chat_id:
354
332
  return
355
-
356
-
357
333
  if sender_id:
358
334
  if isinstance(sender_id, str) and message.sender_id != sender_id:
359
335
  return
360
336
  if isinstance(sender_id, list) and message.sender_id not in sender_id:
361
337
  return
362
-
363
-
364
338
  if sender_type and message.sender_type != sender_type:
365
339
  return
366
-
367
-
368
340
  if not allow_forwarded and message.forwarded_from:
369
341
  return
370
-
371
-
372
342
  if not allow_files and message.file:
373
343
  return
374
344
  if not allow_stickers and message.sticker:
@@ -379,8 +349,6 @@ class Robot:
379
349
  return
380
350
  if not allow_locations and (message.location or message.live_location):
381
351
  return
382
-
383
-
384
352
  if message.text:
385
353
  text = message.text if case_sensitive else message.text.lower()
386
354
  if min_text_length and len(message.text) < min_text_length:
@@ -393,8 +361,6 @@ class Robot:
393
361
  return
394
362
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
395
363
  return
396
-
397
-
398
364
  if commands:
399
365
  if not message.text:
400
366
  return
@@ -402,14 +368,10 @@ class Robot:
402
368
  cmd = parts[0].lstrip("/")
403
369
  if cmd not in commands:
404
370
  return
405
- message.args = parts[1:]
406
-
407
-
371
+ message.args = parts[1:]
408
372
  if filters and not filters(message):
409
373
  return
410
-
411
374
  return await func(bot, message)
412
-
413
375
  self._message_handlers.append({
414
376
  "func": wrapper,
415
377
  "filters": filters,
@@ -447,33 +409,22 @@ class Robot:
447
409
 
448
410
  def decorator(func: Callable[[Any, Message], None]):
449
411
  async def wrapper(bot, message: Message):
450
-
451
412
  if not message.is_group:
452
413
  return
453
-
454
-
455
414
  if chat_id:
456
415
  if isinstance(chat_id, str) and message.chat_id != chat_id:
457
416
  return
458
417
  if isinstance(chat_id, list) and message.chat_id not in chat_id:
459
418
  return
460
-
461
-
462
419
  if sender_id:
463
420
  if isinstance(sender_id, str) and message.sender_id != sender_id:
464
421
  return
465
422
  if isinstance(sender_id, list) and message.sender_id not in sender_id:
466
423
  return
467
-
468
-
469
424
  if sender_type and message.sender_type != sender_type:
470
425
  return
471
-
472
-
473
426
  if not allow_forwarded and message.forwarded_from:
474
427
  return
475
-
476
-
477
428
  if not allow_files and message.file:
478
429
  return
479
430
  if not allow_stickers and message.sticker:
@@ -484,8 +435,6 @@ class Robot:
484
435
  return
485
436
  if not allow_locations and (message.location or message.live_location):
486
437
  return
487
-
488
-
489
438
  if message.text:
490
439
  text = message.text if case_sensitive else message.text.lower()
491
440
  if min_text_length and len(message.text) < min_text_length:
@@ -498,8 +447,6 @@ class Robot:
498
447
  return
499
448
  if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
500
449
  return
501
-
502
-
503
450
  if commands:
504
451
  if not message.text:
505
452
  return
@@ -508,13 +455,9 @@ class Robot:
508
455
  if cmd not in commands:
509
456
  return
510
457
  message.args = parts[1:]
511
-
512
-
513
458
  if filters and not filters(message):
514
459
  return
515
-
516
460
  return await func(bot, message)
517
-
518
461
  self._message_handlers.append({
519
462
  "func": wrapper,
520
463
  "filters": filters,
@@ -527,15 +470,27 @@ class Robot:
527
470
  return wrapper
528
471
  return decorator
529
472
 
530
- def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
473
+ def on_message(
474
+ self,
475
+ filters: Optional[Callable[[Message], bool]] = None,
476
+ commands: Optional[List[str]] = None
477
+ ):
531
478
  def decorator(func: Callable[[Any, Message], None]):
479
+ async def wrapper(bot, message: Message):
480
+ if filters and not filters(message):return
481
+ if commands:
482
+ if not message.is_command:return
483
+ cmd = message.text.split()[0].lstrip("/")
484
+ if cmd not in commands:return
485
+ return await func(bot, message)
532
486
  self._message_handlers.append({
533
- "func": func,
487
+ "func": wrapper,
534
488
  "filters": filters,
535
489
  "commands": commands
536
490
  })
537
- return func
491
+ return wrapper
538
492
  return decorator
493
+
539
494
 
540
495
  def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
541
496
  def decorator(func: Callable[[Any, Message], None]):
@@ -725,13 +680,12 @@ class Robot:
725
680
 
726
681
  async def _handle_inline_query(self, inline_message: InlineMessage):
727
682
  aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
728
-
729
683
  for handler in self._inline_query_handlers:
730
684
  if handler["button_id"] is None or handler["button_id"] == aux_button_id:
731
685
  try:
732
686
  await handler["func"](self, inline_message)
733
687
  except Exception as e:
734
- print(f"Error in inline query handler: {e}")
688
+ raise Exception(f"Error in inline query handler: {e}")
735
689
 
736
690
  def on_inline_query(self, button_id: Optional[str] = None):
737
691
  def decorator(func: Callable[[Any, InlineMessage], None]):
@@ -744,33 +698,19 @@ class Robot:
744
698
  def on_inline_query_prefix(self, prefix: str, button_id: Optional[str] = None):
745
699
  if not prefix.startswith('/'):
746
700
  prefix = '/' + prefix
747
-
748
701
  def decorator(func: Callable[[Any, InlineMessage], None]):
749
-
750
702
  async def handler_wrapper(bot_instance, inline_message: InlineMessage):
751
-
752
703
  if not inline_message.raw_data or 'text' not in inline_message.raw_data:
753
704
  return
754
-
755
705
  query_text = inline_message.raw_data['text']
756
-
757
-
758
706
  if query_text.startswith(prefix):
759
-
760
-
761
-
762
707
  try:
763
708
  await func(bot_instance, inline_message)
764
709
  except Exception as e:
765
- print(f"Error in inline query prefix handler '{prefix}': {e}")
766
-
767
-
710
+ raise Exception(f"Error in inline query prefix handler '{prefix}': {e}")
768
711
  self._inline_query_handlers.append({
769
712
  "func": handler_wrapper,
770
- "button_id": button_id
771
-
772
-
773
-
713
+ "button_id": button_id
774
714
  })
775
715
  return func
776
716
  return decorator
@@ -778,40 +718,29 @@ class Robot:
778
718
  if update.get("type") == "ReceiveQuery":
779
719
  msg = update.get("inline_message", {})
780
720
  context = InlineMessage(bot=self, raw_data=msg)
781
-
782
-
783
721
  if hasattr(self, "_callback_handlers"):
784
722
  for handler in self._callback_handlers:
785
723
  if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
786
724
  asyncio.create_task(handler["func"](self, context))
787
-
788
-
789
725
  asyncio.create_task(self._handle_inline_query(context))
790
726
  return
791
727
 
792
728
  if update.get("type") == "NewMessage":
793
729
  msg = update.get("new_message", {})
794
730
  try:
795
- if msg.get("time") and (time.time() - float(msg["time"])) > 20:
796
- return
797
- except (ValueError, TypeError):
798
- return
799
-
731
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:return
732
+ except (ValueError, TypeError):return
800
733
  context = Message(bot=self,
801
734
  chat_id=update.get("chat_id"),
802
735
  message_id=msg.get("message_id"),
803
736
  sender_id=msg.get("sender_id"),
804
737
  text=msg.get("text"),
805
738
  raw_data=msg)
806
-
807
-
808
739
  if context.aux_data and self._callback_handlers:
809
740
  for handler in self._callback_handlers:
810
741
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
811
742
  asyncio.create_task(handler["func"](self, context))
812
743
  return
813
-
814
-
815
744
  if self._message_handlers:
816
745
  for handler_info in self._message_handlers:
817
746
 
@@ -823,18 +752,12 @@ class Robot:
823
752
  if cmd not in handler_info["commands"]:
824
753
  continue
825
754
  context.args = parts[1:]
826
-
827
-
828
755
  if handler_info["filters"]:
829
756
  if not handler_info["filters"](context):
830
757
  continue
831
-
832
-
833
758
  if not handler_info["commands"] and not handler_info["filters"]:
834
759
  asyncio.create_task(handler_info["func"](self, context))
835
760
  continue
836
-
837
-
838
761
  if handler_info["commands"] or handler_info["filters"]:
839
762
  asyncio.create_task(handler_info["func"](self, context))#jaq
840
763
  continue
@@ -851,25 +774,18 @@ class Robot:
851
774
  if offset_id: params['offset_id'] = offset_id
852
775
  if limit: params['limit'] = limit
853
776
  async with session.get(self.web_hook, params=params) as response:
854
- response.raise_for_status()
855
-
777
+ response.raise_for_status()
856
778
  return await response.json()
857
779
 
858
780
  def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
859
781
  now = time.time()
860
782
  expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
861
- for mid in expired:
862
- del self._processed_message_ids[mid]
863
-
864
- if message_id in self._processed_message_ids:
865
- return True
866
-
783
+ for mid in expired:del self._processed_message_ids[mid]
784
+ if message_id in self._processed_message_ids:return True
867
785
  self._processed_message_ids[message_id] = now
868
786
  return False
869
787
 
870
788
 
871
-
872
-
873
789
  async def run(
874
790
  self,
875
791
  debug: bool = False,
@@ -1436,8 +1352,7 @@ class Robot:
1436
1352
 
1437
1353
 
1438
1354
  _log("Auto-restart requested. You can call run(...) again as needed.", "warning")
1439
-
1440
-
1355
+
1441
1356
  async def send_message(
1442
1357
  self,
1443
1358
  chat_id: str,
@@ -1447,27 +1362,21 @@ class Robot:
1447
1362
  disable_notification: bool = False,
1448
1363
  reply_to_message_id: Optional[str] = None,
1449
1364
  chat_keypad_type: Optional[Literal["New", "Removed"]] = None
1450
- ) -> Dict[str, Any]:
1365
+ ) -> Dict[str, Any]:
1451
1366
  payload = {
1452
1367
  "chat_id": chat_id,
1453
1368
  "text": text,
1454
1369
  "disable_notification": disable_notification,
1455
1370
  }
1456
-
1457
1371
  if chat_keypad:
1458
1372
  payload["chat_keypad"] = chat_keypad
1459
1373
  payload["chat_keypad_type"] = chat_keypad_type or "New"
1460
-
1461
1374
  if inline_keypad:
1462
1375
  payload["inline_keypad"] = inline_keypad
1463
-
1464
1376
  if reply_to_message_id:
1465
1377
  payload["reply_to_message_id"] = reply_to_message_id
1466
-
1467
1378
  return await self._post("sendMessage", payload)
1468
1379
 
1469
-
1470
-
1471
1380
  async def get_url_file(self,file_id):
1472
1381
  data = await self._post("getFile", {'file_id': file_id})
1473
1382
  return data.get("data").get("download_url")
@@ -1480,14 +1389,11 @@ class Robot:
1480
1389
 
1481
1390
  async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
1482
1391
  client = self._get_client()
1483
-
1484
1392
  if chat_id:
1485
1393
  chat_info_data = await self.get_chat(chat_id)
1486
1394
  chat_info = chat_info_data.get('data', {}).get('chat', {})
1487
1395
  username = chat_info.get('username')
1488
1396
  user_id = chat_info.get('user_id')
1489
-
1490
-
1491
1397
  if username:
1492
1398
  result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
1493
1399
  members = result.get('in_chat_members', [])
@@ -1506,7 +1412,6 @@ class Robot:
1506
1412
  id="None"):
1507
1413
  from .button import InlineBuilder
1508
1414
  builder = InlineBuilder()
1509
-
1510
1415
  if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1511
1416
  for t, u in zip(title_button, username):
1512
1417
  builder = builder.row(
@@ -1576,55 +1481,41 @@ class Robot:
1576
1481
  url=url
1577
1482
  )
1578
1483
  )
1579
-
1580
1484
  return await self.send_message(
1581
1485
  chat_id=chat_id,
1582
1486
  text=text,
1583
1487
  inline_keypad=builder.build(),
1584
1488
  reply_to_message_id=reply_to_message_id
1585
1489
  )
1586
-
1587
1490
  def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
1588
-
1589
1491
  client = self._get_client()
1590
1492
  return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
1591
-
1592
1493
  async def send_poll(self, chat_id: str, question: str, options: List[str]) -> Dict[str, Any]:
1593
1494
  return await self._post("sendPoll", {"chat_id": chat_id, "question": question, "options": options})
1594
-
1595
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]:
1596
1496
  payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
1597
1497
  if inline_keypad: payload["inline_keypad"] = inline_keypad
1598
1498
  if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
1599
1499
  if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
1600
1500
  return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
1601
-
1602
1501
  async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str) -> Dict[str, Any]:
1603
1502
  return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number})
1604
-
1605
1503
  async def get_chat(self, chat_id: str) -> Dict[str, Any]:
1606
1504
  return await self._post("getChat", {"chat_id": chat_id})
1607
-
1608
1505
  async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
1609
1506
  is_temp_file = False
1610
1507
  session = await self._get_session()
1611
-
1612
1508
  if isinstance(path, str) and path.startswith("http"):
1613
1509
  async with session.get(path) as response:
1614
1510
  if response.status != 200:
1615
1511
  raise Exception(f"Failed to download file from URL ({response.status})")
1616
-
1617
1512
  content = await response.read()
1618
-
1619
1513
  with tempfile.NamedTemporaryFile(delete=False) as temp_file:
1620
1514
  temp_file.write(content)
1621
1515
  path = temp_file.name
1622
1516
  is_temp_file = True
1623
-
1624
1517
  file_size = os.path.getsize(path)
1625
-
1626
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)
1627
-
1628
1519
  async def file_progress_generator(file_path, chunk_size=8192):
1629
1520
  async with aiofiles.open(file_path, 'rb') as f:
1630
1521
  while True:
@@ -1633,7 +1524,6 @@ class Robot:
1633
1524
  break
1634
1525
  progress_bar.update(len(chunk))
1635
1526
  yield chunk
1636
-
1637
1527
  data = aiohttp.FormData()
1638
1528
  data.add_field('file', file_progress_generator(path), filename=name, content_type='application/octet-stream')
1639
1529
  try:
@@ -1648,12 +1538,9 @@ class Robot:
1648
1538
  return json_data.get('data', {}).get('file_id')
1649
1539
  except :
1650
1540
  raise FeatureNotAvailableError(f"files is not currently supported by the server.")
1651
-
1652
-
1653
1541
  def get_extension(content_type: str) -> str:
1654
1542
  ext = mimetypes.guess_extension(content_type)
1655
1543
  return ext if ext else ''
1656
-
1657
1544
  async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
1658
1545
  """
1659
1546
  Download a file from server using its file_id with chunked transfer,
@@ -1672,16 +1559,11 @@ class Robot:
1672
1559
  Returns:
1673
1560
  bool: True if success, raises exceptions otherwise.
1674
1561
  """
1675
-
1676
1562
  try:
1677
1563
  url = await self.get_url_file(file_id)
1678
- if not url:
1679
- raise ValueError("Download URL not found in response.")
1680
- except Exception as e:
1681
- raise ValueError(f"Failed to get download URL: {e}")
1682
-
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}")
1683
1566
  timeout = aiohttp.ClientTimeout(total=timeout_sec)
1684
-
1685
1567
  try:
1686
1568
  async with aiohttp.ClientSession(timeout=timeout) as session:
1687
1569
  async with session.get(url) as resp:
@@ -1693,21 +1575,17 @@ class Robot:
1693
1575
  message="Failed to download file.",
1694
1576
  headers=resp.headers
1695
1577
  )
1696
-
1697
1578
  if not save_as:
1698
1579
  content_disp = resp.headers.get("Content-Disposition", "")
1699
1580
  import re
1700
1581
  match = re.search(r'filename="?([^\";]+)"?', content_disp)
1701
- if match:
1702
- save_as = match.group(1)
1582
+ if match:save_as = match.group(1)
1703
1583
  else:
1704
1584
  content_type = resp.headers.get("Content-Type", "").split(";")[0]
1705
1585
  extension = mimetypes.guess_extension(content_type) or ".bin"
1706
1586
  save_as = f"{file_id}{extension}"
1707
-
1708
1587
  total_size = int(resp.headers.get("Content-Length", 0))
1709
1588
  progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
1710
-
1711
1589
  async with aiofiles.open(save_as, "wb") as f:
1712
1590
  async for chunk in resp.content.iter_chunked(chunk_size):
1713
1591
  await f.write(chunk)
@@ -1770,7 +1648,7 @@ class Robot:
1770
1648
  "inline_keypad":inline_keypad,
1771
1649
  "chat_keypad_type":chat_keypad_type
1772
1650
  }
1773
- return result
1651
+ return AttrDict(result)
1774
1652
 
1775
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):
1776
1654
  if path:
@@ -1789,43 +1667,31 @@ class Robot:
1789
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)
1790
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]:
1791
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)
1792
-
1793
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]:
1794
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)
1795
-
1796
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]:
1797
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)
1798
-
1799
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]:
1800
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)
1801
-
1802
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]:
1803
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)
1804
-
1805
1678
  async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
1806
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})
1807
-
1808
1680
  async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Dict[str, Any]:
1809
1681
  return await self._post("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text})
1810
-
1811
- async def edit_inline_keypad(self, chat_id: str, message_id: str, inline_keypad: Dict[str, Any]) -> Dict[str, Any]:
1812
- return await self._post("editMessageKeypad", {"chat_id": chat_id, "message_id": message_id, "inline_keypad": inline_keypad})
1813
-
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})
1814
1685
  async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
1815
1686
  return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
1816
-
1817
1687
  async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
1818
1688
  return await self._post("setCommands", {"bot_commands": bot_commands})
1819
-
1820
1689
  async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
1821
1690
  return await self._post("updateBotEndpoints", {"url": url, "type": type})
1822
-
1823
1691
  async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
1824
1692
  return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Removed"})
1825
-
1826
1693
  async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
1827
1694
  return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
1828
-
1829
1695
  async def get_name(self, chat_id: str) -> str:
1830
1696
  try:
1831
1697
  chat = await self.get_chat(chat_id)
@@ -1841,9 +1707,6 @@ class Robot:
1841
1707
  return title if title else "null"
1842
1708
  else:return "null"
1843
1709
  except Exception:return "null"
1844
-
1845
-
1846
-
1847
1710
  async def get_username(self, chat_id: str) -> str:
1848
1711
  chat_info = await self.get_chat(chat_id)
1849
1712
  return chat_info.get("data", {}).get("chat", {}).get("username", "None")
rubka/filters.py ADDED
@@ -0,0 +1,305 @@
1
+ from typing import Callable
2
+ import re
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", ""))
9
+ class Filter:
10
+ def __init__(self, func: Callable):
11
+ self.func = func
12
+
13
+ def __call__(self, message):
14
+ return self.func(message)
15
+
16
+ def __and__(self, other):
17
+ return Filter(lambda m: self(m) and other(m))
18
+
19
+ def __or__(self, other):
20
+ return Filter(lambda m: self(m) or other(m))
21
+
22
+ def __invert__(self):
23
+ return Filter(lambda m: not self(m))
24
+
25
+ def __xor__(self, other):
26
+ return Filter(lambda m: self(m) != other(m))
27
+
28
+ def __eq__(self, other):
29
+ return Filter(lambda m: self(m) == other)
30
+
31
+ def __ne__(self, other):
32
+ return Filter(lambda m: self(m) != other)
33
+
34
+ def __lt__(self, other):
35
+ return Filter(lambda m: self(m) < other)
36
+
37
+ def __le__(self, other):
38
+ return Filter(lambda m: self(m) <= other)
39
+
40
+ def __gt__(self, other):
41
+ return Filter(lambda m: self(m) > other)
42
+
43
+ def __ge__(self, other):
44
+ return Filter(lambda m: self(m) >= other)
45
+
46
+ def __add__(self, other):
47
+ return Filter(lambda m: self(m) + (other(m) if callable(other) else other))
48
+
49
+ def __sub__(self, other):
50
+ return Filter(lambda m: self(m) - (other(m) if callable(other) else other))
51
+
52
+ def __mul__(self, other):
53
+ return Filter(lambda m: self(m) * (other(m) if callable(other) else other))
54
+
55
+ def __truediv__(self, other):
56
+ return Filter(lambda m: self(m) / (other(m) if callable(other) else other))
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))
83
+ is_file = Filter(lambda m: getattr(m, "file", None) is not None)
84
+ is_sticker = Filter(lambda m: getattr(m, "sticker", None) is not None)
85
+ is_contact = Filter(lambda m: getattr(m, "contact_message", None) is not None)
86
+ is_poll = Filter(lambda m: getattr(m, "poll", None) is not None)
87
+ is_location = Filter(lambda m: getattr(m, "location", None) is not None)
88
+ is_live_location = Filter(lambda m: getattr(m, "live_location", None) is not None)
89
+ has_any_media = Filter(lambda m: getattr(m, "has_any_media", False))
90
+ has_media = Filter(lambda m: getattr(m, "has_media", False))
91
+ is_text = IsText()
92
+ is_command = IsCommand()
93
+ is_user = Filter(lambda m: getattr(m, "is_user", False))
94
+ is_private = Filter(lambda m: getattr(m, "is_private", False))
95
+ is_group = Filter(lambda m: getattr(m, "is_group", False))
96
+ is_channel = Filter(lambda m: getattr(m, "is_channel", False))
97
+ is_reply = Filter(lambda m: getattr(m, "is_reply", False))
98
+ is_forwarded = Filter(lambda m: getattr(m, "is_forwarded", False))
99
+ is_edited = Filter(lambda m: getattr(m, "is_edited", False))
100
+ def text(keyword: str):
101
+ return Filter(lambda m: getattr(m, "text", "") and keyword in m.text)
102
+ def text_length(min_len: int = 0, max_len: int = None):
103
+ def _filter(m):
104
+ t = getattr(m, "text", "")
105
+ if not t: return False
106
+ if len(t) < min_len: return False
107
+ if max_len is not None and len(t) > max_len: return False
108
+ return True
109
+ return Filter(_filter)
110
+ def text_regex(pattern: str):
111
+ regex = re.compile(pattern)
112
+ return Filter(lambda m: getattr(m, "text", "") and regex.search(m.text))
113
+ def text_startswith(prefix: str):
114
+ return Filter(lambda m: getattr(m, "text", "").startswith(prefix) if getattr(m, "text", None) else False)
115
+ def text_endswith(suffix: str):
116
+ return Filter(lambda m: getattr(m, "text", "").endswith(suffix) if getattr(m, "text", None) else False)
117
+ def text_upper():
118
+ return Filter(lambda m: getattr(m, "text", "").isupper() if getattr(m, "text", None) else False)
119
+ def text_lower():
120
+ return Filter(lambda m: getattr(m, "text", "").islower() if getattr(m, "text", None) else False)
121
+ def text_digit():
122
+ return Filter(lambda m: getattr(m, "text", "").isdigit() if getattr(m, "text", None) else False)
123
+ def text_word_count(min_words: int = 1, max_words: int = None):
124
+ def _filter(m):
125
+ t = getattr(m, "text", "")
126
+ if not t: return False
127
+ wc = len(t.split())
128
+ if wc < min_words: return False
129
+ if max_words is not None and wc > max_words: return False
130
+ return True
131
+ return Filter(_filter)
132
+ def text_contains_any(keywords: list):
133
+ return Filter(lambda m: getattr(m, "text", "") and any(k in m.text for k in keywords))
134
+ def text_equals(value: str):
135
+ return Filter(lambda m: getattr(m, "text", None) == value)
136
+ def text_not_equals(value: str):
137
+ return Filter(lambda m: getattr(m, "text", None) != value)
138
+ def file_size_gt(size: int):
139
+ return Filter(lambda m: m.file and getattr(m.file, "size", 0) > size)
140
+ def file_size_lt(size: int):
141
+ return Filter(lambda m: m.file and getattr(m.file, "size", 0) < size)
142
+ def file_name_contains(substring: str):
143
+ return Filter(lambda m: m.file and substring in getattr(m.file, "file_name", ""))
144
+ def file_extension(ext: str):
145
+ return Filter(lambda m: m.file and getattr(m.file, "file_name", "").endswith(ext))
146
+ def file_id_is(file_id: str):
147
+ return Filter(lambda m: m.file and getattr(m.file, "file_id", None) == file_id)
148
+ def sticker_id_is(sid: str):
149
+ return Filter(lambda m: m.sticker and getattr(m.sticker, "sticker_id", None) == sid)
150
+ def sticker_emoji_is(emoji: str):
151
+ return Filter(lambda m: m.sticker and getattr(m.sticker, "emoji", None) == emoji)
152
+ def poll_question_contains(keyword: str):
153
+ return Filter(lambda m: m.poll and keyword in getattr(m.poll, "question", ""))
154
+ def poll_option_count(min_options: int = 1, max_options: int = None):
155
+ def _filter(m):
156
+ if not getattr(m, "poll", None): return False
157
+ options = getattr(m.poll, "options", [])
158
+ if len(options) < min_options: return False
159
+ if max_options is not None and len(options) > max_options: return False
160
+ return True
161
+ return Filter(_filter)
162
+ def location_within(lat_min, lat_max, long_min, long_max):
163
+ def _filter(m):
164
+ loc = getattr(m, "location", None)
165
+ if not loc: return False
166
+ return lat_min <= getattr(loc, "lat", 0) <= lat_max and long_min <= getattr(loc, "long", 0) <= long_max
167
+ return Filter(_filter)
168
+ def live_location_within(lat_min, lat_max, long_min, long_max):
169
+ def _filter(m):
170
+ loc = getattr(m, "live_location", None)
171
+ if not loc: return False
172
+ return lat_min <= getattr(loc, "lat", 0) <= lat_max and long_min <= getattr(loc, "long", 0) <= long_max
173
+ return Filter(_filter)
174
+ def has_media_types(types: list):
175
+ return Filter(lambda m: any(getattr(m, t, None) for t in types))
176
+
177
+ def message_id_is(mid: str):
178
+ return Filter(lambda m: getattr(m, "message_id", None) == mid)
179
+
180
+ def is_reply_to_user(user_id: str):
181
+ return Filter(lambda m: getattr(m, "reply_to_message_id", None) == user_id)
182
+
183
+ def is_forwarded_from(user_id: str):
184
+ return Filter(lambda m: getattr(m.forwarded_from, "sender_id", None) == user_id if getattr(m, "forwarded_from", None) else False)
185
+
186
+ def edited_text_contains(keyword: str):
187
+ return Filter(lambda m: getattr(m, "edited_text", "") and keyword in m.edited_text)
188
+
189
+ def aux_data_contains(key: str, value):
190
+ return Filter(lambda m: getattr(m.aux_data, key, None) == value if getattr(m, "aux_data", None) else False)
191
+
192
+
193
+
194
+
195
+ def file_attr(attr_name):
196
+ return Filter(lambda m: m.file and getattr(m.file, attr_name, None))
197
+
198
+ def sticker_attr(attr_name):
199
+ return Filter(lambda m: m.sticker and getattr(m.sticker, attr_name, None))
200
+
201
+ def poll_attr(attr_name):
202
+ return Filter(lambda m: m.poll and getattr(m.poll, attr_name, None))
203
+
204
+ def location_attr(attr_name):
205
+ return Filter(lambda m: m.location and getattr(m.location, attr_name, None))
206
+
207
+ def live_location_attr(attr_name):
208
+ return Filter(lambda m: m.live_location and getattr(m.live_location, attr_name, None))
209
+
210
+
211
+
212
+
213
+ file_size = file_attr("size")
214
+ file_name = file_attr("file_name")
215
+ sticker_id = sticker_attr("sticker_id")
216
+ poll_question = poll_attr("question")
217
+ location_lat = location_attr("lat")
218
+ location_long = location_attr("long")
219
+ live_location_lat = live_location_attr("lat")
220
+ live_location_long = live_location_attr("long")
221
+
222
+ _custom_filters = {}
223
+ def chat_title_contains(keyword: str):
224
+
225
+ return Filter(lambda m: getattr(m, "chat", None) and keyword in getattr(m.chat, "title", ""))
226
+
227
+ def chat_title_equals(value: str):
228
+
229
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "title", "") == value)
230
+
231
+ def chat_id_is(cid: str):
232
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "id", None) == cid)
233
+
234
+ def chat_member_count(min_count: int = 0, max_count: int = None):
235
+
236
+ def _filter(m):
237
+ c = getattr(m, "chat", None)
238
+ if not c: return False
239
+ count = getattr(c, "member_count", 0)
240
+ if count < min_count: return False
241
+ if max_count is not None and count > max_count: return False
242
+ return True
243
+ return Filter(_filter)
244
+
245
+ def chat_type_is(chat_type: str):
246
+
247
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "type", None) == chat_type)
248
+
249
+ def chat_username_contains(keyword: str):
250
+
251
+ return Filter(lambda m: getattr(m, "chat", None) and keyword in getattr(m.chat, "username", ""))
252
+
253
+ def chat_username_equals(value: str):
254
+
255
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "username", "") == value)
256
+
257
+ def chat_has_link():
258
+
259
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "invite_link", None) is not None)
260
+
261
+ def chat_is_private():
262
+
263
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "type", None) in ["group", "channel"])
264
+
265
+ def chat_member_count_gt(count: int):
266
+
267
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "member_count", 0) > count)
268
+
269
+ def chat_member_count_lt(count: int):
270
+
271
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "member_count", 0) < count)
272
+
273
+ def chat_has_username():
274
+
275
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "username", None) is not None)
276
+
277
+ def chat_type_in(types: list):
278
+
279
+ return Filter(lambda m: getattr(m, "chat", None) and getattr(m.chat, "type", None) in types)
280
+
281
+
282
+ def chat_title_regex(pattern: str):
283
+ regex = re.compile(pattern)
284
+ return Filter(lambda m: getattr(m, "chat", None) and regex.search(getattr(m.chat, "title", "")))
285
+
286
+ def chat_username_regex(pattern: str):
287
+ regex = re.compile(pattern)
288
+ return Filter(lambda m: getattr(m, "chat", None) and regex.search(getattr(m.chat, "username", "")))
289
+ def custom(name):
290
+ def wrapper(func):
291
+ _custom_filters[name] = Filter(func)
292
+ return _custom_filters[name]
293
+ return wrapper
294
+
295
+ def get_custom(name):
296
+ return _custom_filters.get(name)
297
+
298
+ def and_(*filters):
299
+ return Filter(lambda m: all(f(m) for f in filters))
300
+
301
+ def or_(*filters):
302
+ return Filter(lambda m: any(f(m) for f in filters))
303
+
304
+ def not_(filter_):
305
+ return Filter(lambda m: not filter_(m))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rubka
3
- Version: 6.4.8
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,11 +1,12 @@
1
1
  rubka/__init__.py,sha256=TR1DABU5Maz2eO62ZEFiwOqNU0dH6l6HZfqRUxeo4eY,194
2
- rubka/api.py,sha256=J0fa4XXjImFulLXAaJY2pbE46e03i-Y5QggxaAZq6xw,68159
3
- rubka/asynco.py,sha256=wiuw2oLaYH5a99W7_37sjP29hXabAbThnkLk1sPIzUk,83919
2
+ rubka/api.py,sha256=FkvXi6lGyDhy-GStfT9IPtmnpir75ggNsC4bsgy2tKw,68838
3
+ rubka/asynco.py,sha256=O5sr6Ev7LCsC5egjDk5uzJyeZEPqmxOH27lUBgzHM8A,83517
4
4
  rubka/button.py,sha256=vU9OvWXCD4MRrTJ8Xmivd4L471-06zrD2qpZBTw5vjY,13305
5
5
  rubka/config.py,sha256=Bck59xkOiqioLv0GkQ1qPGnBXVctz1hKk6LT4h2EPx0,78
6
6
  rubka/context.py,sha256=4YZs7DiZD_HWOqY76hwwajG0J-bLy6wjeKtQT3EatZU,19341
7
7
  rubka/decorators.py,sha256=hGwUoE4q2ImrunJIGJ_kzGYYxQf1ueE0isadqraKEts,1157
8
8
  rubka/exceptions.py,sha256=tujZt1XrhWaw-lmdeVadVceUptpw4XzNgE44sAAY0gs,90
9
+ rubka/filters.py,sha256=DY1bdkpRKIiLtVcy6X3hOnlGPcVOK4HFb3QgmaPx6Oo,12116
9
10
  rubka/jobs.py,sha256=GvLMLsVhcSEzRTgkvnPISPEBN71suW2xXI0hUaUZPTo,378
10
11
  rubka/keyboards.py,sha256=7nr-dT2bQJVQnQ6RMWPTSjML6EEk6dsBx-4d8pab8xk,488
11
12
  rubka/keypad.py,sha256=yGsNt8W5HtUFBzVF1m_p7GySlu1hwIcSvXZ4BTdrlvg,9558
@@ -34,7 +35,7 @@ rubka/adaptorrubka/types/socket/message.py,sha256=0WgLMZh4eow8Zn7AiSX4C3GZjQTkIg
34
35
  rubka/adaptorrubka/utils/__init__.py,sha256=OgCFkXdNFh379quNwIVOAWY2NP5cIOxU5gDRRALTk4o,54
35
36
  rubka/adaptorrubka/utils/configs.py,sha256=nMUEOJh1NqDJsf9W9PurkN_DLYjO6kKPMm923i4Jj_A,492
36
37
  rubka/adaptorrubka/utils/utils.py,sha256=5-LioLNYX_TIbQGDeT50j7Sg9nAWH2LJUUs-iEXpsUY,8816
37
- rubka-6.4.8.dist-info/METADATA,sha256=2FefyxguhKHS07vKRgGlhhzHBi9ojoGe0U9ymQvDNTg,33335
38
- rubka-6.4.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- rubka-6.4.8.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
40
- rubka-6.4.8.dist-info/RECORD,,
38
+ rubka-6.6.2.dist-info/METADATA,sha256=-XVEYYI0-PnUjEzLHgnEifIpRem8h7RqL3V7sA07jhQ,33335
39
+ rubka-6.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
+ rubka-6.6.2.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
41
+ rubka-6.6.2.dist-info/RECORD,,
File without changes