pyrobale 0.3.7__tar.gz → 0.3.8.5__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.
Files changed (57) hide show
  1. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/LICENSE +2 -2
  2. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/PKG-INFO +5 -6
  3. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/README.md +3 -4
  4. pyrobale-0.3.8.5/examples/command.py +12 -0
  5. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyproject.toml +1 -1
  6. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/StateMachine/__init__.py +21 -0
  7. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/__init__.py +3 -1
  8. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/client/__init__.py +141 -24
  9. pyrobale-0.3.8.5/pyrobale/exceptions/__init__.py +1 -0
  10. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/exceptions/common.py +6 -0
  11. pyrobale-0.3.8.5/pyrobale/filters/__init__.py +2 -0
  12. pyrobale-0.3.8.5/pyrobale/filters/enum_filters.py +12 -0
  13. pyrobale-0.3.8.5/pyrobale/filters/func_filters.py +17 -0
  14. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/chat.py +11 -0
  15. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/enums.py +1 -9
  16. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/inlinekeyboardmarkup.py +11 -4
  17. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/message.py +6 -8
  18. pyrobale-0.3.8.5/pyrobale/objects/utils.py +48 -0
  19. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale.png +0 -0
  20. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobaletext.png +0 -0
  21. pyrobale-0.3.7/pyrobale/exceptions/__init__.py +0 -0
  22. pyrobale-0.3.7/pyrobale/objects/utils.py +0 -27
  23. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/.github/ISSUE_TEMPLATE/issue-template-/342/204/271/357/270/217.md" +0 -0
  24. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/.github/workflows/docs.yml +0 -0
  25. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/examples/echo_bot.py +0 -0
  26. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/examples/handler_system.py +0 -0
  27. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/examples/inline_keyboard.py +0 -0
  28. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/__init__.py +0 -0
  29. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/animation.py +0 -0
  30. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/audio.py +0 -0
  31. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/callbackquery.py +0 -0
  32. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/chatmember.py +0 -0
  33. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/chatphoto.py +0 -0
  34. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/contact.py +0 -0
  35. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/copytextbutton.py +0 -0
  36. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/document.py +0 -0
  37. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/file.py +0 -0
  38. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/inlinekeyboardbutton.py +0 -0
  39. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/inputfile.py +0 -0
  40. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/inputmedias.py +0 -0
  41. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/invoice.py +0 -0
  42. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/keyboardbutton.py +0 -0
  43. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/labeledprice.py +0 -0
  44. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/location.py +0 -0
  45. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/messageid.py +0 -0
  46. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/photosize.py +0 -0
  47. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/precheckoutquery.py +0 -0
  48. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/replykeyboardmarkup.py +0 -0
  49. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/sticker.py +0 -0
  50. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/stickerset.py +0 -0
  51. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/successfulpayment.py +0 -0
  52. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/update.py +0 -0
  53. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/user.py +0 -0
  54. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/video.py +0 -0
  55. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/voice.py +0 -0
  56. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/webappdata.py +0 -0
  57. {pyrobale-0.3.7 → pyrobale-0.3.8.5}/pyrobale/objects/webappinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Ali Safamanesh
3
+ Copyright (c) 2025 PyroBale Team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrobale
3
- Version: 0.3.7
3
+ Version: 0.3.8.5
4
4
  Summary: A python wrapper for bale api
5
5
  Project-URL: github, https://github.com/pyrobale/pyrobale
6
6
  Project-URL: website, https://pyrobale.github.io
7
7
  Author-email: Ali Safamanesh <darg.q.a.a@gmail.com>, Aydin Rahbaran <codewizaard9@gmail.com>
8
8
  License: MIT License
9
9
 
10
- Copyright (c) 2025 Ali Safamanesh
10
+ Copyright (c) 2025 PyroBale Team
11
11
 
12
12
  Permission is hereby granted, free of charge, to any person obtaining a copy
13
13
  of this software and associated documentation files (the "Software"), to deal
@@ -49,7 +49,7 @@ A modern, easy-to-use Python wrapper for the Bale Bot API that makes building Ba
49
49
  - 📁 **File Handling** - Easy upload and download of media files
50
50
  - 🛡️ **Error Handling** - Comprehensive exception handling
51
51
  - 📖 **Type Hints** - Full typing support for better development experience
52
- - ⚡ **Async Support** - Both synchronous and asynchronous operations
52
+ - ⚡ **Async Support** - asynchronous operations
53
53
 
54
54
  ## Installation
55
55
 
@@ -66,7 +66,7 @@ from pyrobale.objects import Message, UpdatesTypes
66
66
  bot = Client("YOUR_BOT_TOKEN")
67
67
 
68
68
  @bot.on_message()
69
- async def message_handler(message: User):
69
+ async def message_handler(message: Message):
70
70
  await message.reply("Hello, world!")
71
71
 
72
72
  bot.run()
@@ -78,13 +78,12 @@ bot.run()
78
78
  ```python
79
79
  from pyrobale.objects import *
80
80
  from pyrobale.client import Client, Message, UpdatesTypes
81
- import asyncio
82
81
 
83
82
  client = Client("YOUR_BOT_TOKEN")
84
83
 
85
84
  async def handle_message(message: Message):
86
85
  if message.text == "/start":
87
- await message.reply("سلام! من یک ربات PyRoBale هستم!")
86
+ await message.reply("Hi! Im a pyrobale RoBot!")
88
87
  await client.wait_for(UpdatesTypes.MESSAGE)
89
88
  await message.reply("Okay! wait_for Test Compeleted")
90
89
 
@@ -13,7 +13,7 @@ A modern, easy-to-use Python wrapper for the Bale Bot API that makes building Ba
13
13
  - 📁 **File Handling** - Easy upload and download of media files
14
14
  - 🛡️ **Error Handling** - Comprehensive exception handling
15
15
  - 📖 **Type Hints** - Full typing support for better development experience
16
- - ⚡ **Async Support** - Both synchronous and asynchronous operations
16
+ - ⚡ **Async Support** - asynchronous operations
17
17
 
18
18
  ## Installation
19
19
 
@@ -30,7 +30,7 @@ from pyrobale.objects import Message, UpdatesTypes
30
30
  bot = Client("YOUR_BOT_TOKEN")
31
31
 
32
32
  @bot.on_message()
33
- async def message_handler(message: User):
33
+ async def message_handler(message: Message):
34
34
  await message.reply("Hello, world!")
35
35
 
36
36
  bot.run()
@@ -42,13 +42,12 @@ bot.run()
42
42
  ```python
43
43
  from pyrobale.objects import *
44
44
  from pyrobale.client import Client, Message, UpdatesTypes
45
- import asyncio
46
45
 
47
46
  client = Client("YOUR_BOT_TOKEN")
48
47
 
49
48
  async def handle_message(message: Message):
50
49
  if message.text == "/start":
51
- await message.reply("سلام! من یک ربات PyRoBale هستم!")
50
+ await message.reply("Hi! Im a pyrobale RoBot!")
52
51
  await client.wait_for(UpdatesTypes.MESSAGE)
53
52
  await message.reply("Okay! wait_for Test Compeleted")
54
53
 
@@ -0,0 +1,12 @@
1
+ from pyrobale.client import Client
2
+ from pyrobale.objects import Message, UpdatesTypes
3
+
4
+ token = "YOUR_BOT_TOKEN"
5
+
6
+ bot = Client(token)
7
+
8
+ @bot.on_command('start')
9
+ async def start(message: Message):
10
+ await message.reply("Hello, world!")
11
+
12
+ bot.run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyrobale"
3
- version = "0.3.7"
3
+ version = "0.3.8.5"
4
4
  authors = [
5
5
  { name = "Ali Safamanesh", email = "darg.q.a.a@gmail.com" },
6
6
  { name = "Aydin Rahbaran", email = "codewizaard9@gmail.com"},
@@ -43,3 +43,24 @@ class StateMachine:
43
43
  del self.__states[user_id]
44
44
  else:
45
45
  raise KeyError
46
+
47
+ def save_local(self, file_name: str):
48
+ """Saves the state of all users to a file
49
+
50
+ Args:
51
+ file_name (string): name of file to save the state of users
52
+ """
53
+ with open(file_name, "w") as f:
54
+ for user_id, state in self.__states.items():
55
+ f.write(f"{user_id} {state}\n")
56
+
57
+ def load_local(self, file_name: str):
58
+ """Loads the state of all users from a file
59
+
60
+ Args:
61
+ file_name (string): name of file to load the state of users
62
+ """
63
+ with open(file_name, "r") as f:
64
+ for line in f:
65
+ user_id, state = line.split()
66
+ self.__states[user_id] = state
@@ -31,7 +31,7 @@ from pyrobale.objects import Message, UpdatesTypes
31
31
  bot = Client("YOUR_BOT_TOKEN")
32
32
 
33
33
  @bot.on_message()
34
- async def message_handler(message: User):
34
+ async def message_handler(message: Message):
35
35
  await message.reply("Hello, world!")
36
36
 
37
37
  bot.run()
@@ -116,8 +116,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
116
116
  - 📖 [Documentation](https://pyrobale.readthedocs.io)
117
117
  - 🐛 [Issue Tracker](https://github.com/pyrobale/pyrobale/issues)
118
118
  - 💬 [Discussions](https://github.com/pyrobale/pyrobale/discussions)
119
+
119
120
  """
120
121
 
121
122
  from .objects.utils import *
122
123
  from .exceptions import *
123
124
  from .objects import *
125
+ from .client import Client
@@ -40,7 +40,9 @@ from ..objects.utils import *
40
40
  import asyncio
41
41
  from enum import Enum
42
42
  from ..objects.enums import UpdatesTypes, ChatAction, ChatType
43
+ from ..filters import Filters, equals
43
44
  from ..StateMachine import StateMachine
45
+ from ..exceptions import NotFoundException, InvalidTokenException, PyroBaleException
44
46
 
45
47
 
46
48
  class Client:
@@ -81,7 +83,12 @@ class Client:
81
83
  self.requests_base
82
84
  + f"/getUpdates?offset={offset}&limit={limit}&timeout={timeout}"
83
85
  )
84
- return data["result"]
86
+ if data['ok']:
87
+ if 'result' in data.keys():
88
+ return data["result"]
89
+ else:
90
+ if data['error_code'] == 403:
91
+ raise InvalidTokenException("Forbidden 403 : --ENTERED TOKEN IS NOT VALID--")
85
92
 
86
93
  async def set_webhook(self, url: str) -> bool:
87
94
  """Set the webhook for the bot.
@@ -421,6 +428,7 @@ class Client:
421
428
  chat_id: int,
422
429
  latitude: float,
423
430
  longitude: float,
431
+ horizontal_accuracy: Optional[float] = None,
424
432
  reply_to_message_id: Optional[int] = None,
425
433
  reply_markup: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
426
434
  ) -> Message:
@@ -441,6 +449,7 @@ class Client:
441
449
  "chat_id": chat_id,
442
450
  "latitude": latitude,
443
451
  "longitude": longitude,
452
+ "horizontal_accuracy": horizontal_accuracy,
444
453
  "reply_to_message_id": reply_to_message_id,
445
454
  "reply_markup": reply_markup.to_dict() if reply_markup else None,
446
455
  },
@@ -507,6 +516,9 @@ class Client:
507
516
  Returns:
508
517
  Message: returns the sent message with invoice
509
518
  """
519
+ new_prices = []
520
+ for price in prices:
521
+ new_prices.append(price.json)
510
522
  data = await make_post(
511
523
  self.requests_base + "/sendInvoice",
512
524
  data={
@@ -515,7 +527,7 @@ class Client:
515
527
  "description": description,
516
528
  "payload": payload,
517
529
  "provider_token": provider_token,
518
- "prices": prices,
530
+ "prices": new_prices,
519
531
  "photo_url": photo_url,
520
532
  "reply_to_message_id": reply_to_message_id,
521
533
  },
@@ -683,6 +695,22 @@ class Client:
683
695
  self.requests_base + "/leaveChat", data={"chat_id": chat_id}
684
696
  )
685
697
  return data.get("ok", False)
698
+
699
+ async def is_joined(self, user_id: int, chat_id: int) -> bool:
700
+ """Check if a user is joined to a chat.
701
+
702
+ Args:
703
+ user_id (int): Unique identifier for the target chat
704
+ chat_id (int): Unique identifier for the target chat
705
+
706
+ Returns:
707
+ bool: True if the user is joined to the chat, False otherwise
708
+ """
709
+ data = await make_post(
710
+ self.requests_base + "/getChatMember",
711
+ data={"chat_id": chat_id, "user_id": user_id},
712
+ )
713
+ return data.get("result", {}).get("status") in ["member", "creator", "administrator"]
686
714
 
687
715
  async def get_chat(self, chat_id: int) -> Chat:
688
716
  """Get up to date information about the chat.
@@ -922,51 +950,136 @@ class Client:
922
950
  if not future.done():
923
951
  future.set_result(event)
924
952
  self._waiters.remove(waiter)
925
- return update
953
+ return
926
954
 
927
955
  for handler in self.handlers:
928
956
  update_type = handler["type"].value
929
957
  if update_type in update:
930
- event = update[update_type]
931
-
932
- event = self._convert_event(handler["type"], event)
933
-
958
+ raw_event = update[update_type]
959
+ event = self._convert_event(handler["type"], raw_event)
960
+
961
+ if handler["type"] == UpdatesTypes.COMMAND:
962
+ if hasattr(event, 'text') and event.text and event.text.startswith('/'):
963
+ command_text = event.text[1:]
964
+ command_parts = command_text.split()
965
+ if command_parts:
966
+ actual_command = command_parts[0]
967
+ expected_command = handler.get("command", "")
968
+
969
+ if actual_command != expected_command:
970
+ continue
971
+
972
+ flt = handler.get("filter")
973
+ if flt is not None:
974
+ if callable(flt):
975
+ try:
976
+ if not flt(event):
977
+ continue
978
+ except Exception as e:
979
+ print(f"[Filter Error] {e}")
980
+ continue
981
+ elif isinstance(flt, Filters):
982
+ if not hasattr(event, flt.value):
983
+ continue
984
+
934
985
  if asyncio.iscoroutinefunction(handler["callback"]):
935
986
  asyncio.create_task(handler["callback"](event))
936
987
  else:
937
988
  handler["callback"](event)
938
989
 
990
+
991
+
939
992
  def base_handler_decorator(self, update_type: UpdatesTypes):
993
+ """Base decorator for handling different types of updates.
994
+
995
+ Args:
996
+ update_type (UpdatesTypes): The type of update to handle.
997
+
998
+ Returns:
999
+ Callable: A decorator function that registers the callback for the specified update type.
1000
+ """
1001
+ def wrapper(filter: Optional[Filters] = None):
1002
+ def decorator(callback: Callable[[Any], Union[None, Awaitable[None]]]):
1003
+ self.add_handler(update_type, callback, filter)
1004
+ return callback
1005
+ return decorator
1006
+ return wrapper
1007
+
1008
+ def on_command(self, command: str, filter: Optional[Filters] = None):
1009
+ """Decorator for handling command updates.
1010
+
1011
+ Args:
1012
+ command (str): The command to handle.
1013
+ filter (Optional[Filters]): An optional filter to apply to the command.
1014
+ Returns:
1015
+ Callable: A decorator function that registers the callback for the specified command.
1016
+ """
940
1017
  def decorator(callback: Callable[[Any], Union[None, Awaitable[None]]]):
941
- self.add_handler(update_type, callback)
1018
+ self.add_handler(UpdatesTypes.COMMAND, callback, filter, command=command)
942
1019
  return callback
943
-
944
1020
  return decorator
945
1021
 
946
- def on_message(self):
947
- return self.base_handler_decorator(UpdatesTypes.MESSAGE)
1022
+
1023
+ def on_message(self, filter: Optional[Filters] = None):
1024
+ """Decorator for handling new message updates.
1025
+
1026
+ Returns:
1027
+ Callable: A decorator function that registers the callback for message updates.
1028
+ """
1029
+ return self.base_handler_decorator(UpdatesTypes.MESSAGE)(filter)
1030
+
948
1031
 
949
1032
  def on_edited_message(self):
1033
+ """Decorator for handling edited message updates.
1034
+
1035
+ Returns:
1036
+ Callable: A decorator function that registers the callback for edited message updates.
1037
+ """
950
1038
  return self.base_handler_decorator(UpdatesTypes.MESSAGE_EDITED)
951
1039
 
952
1040
  def on_callback_query(self):
1041
+ """Decorator for handling callback query updates.
1042
+
1043
+ Returns:
1044
+ Callable: A decorator function that registers the callback for callback query updates.
1045
+ """
953
1046
  return self.base_handler_decorator(UpdatesTypes.CALLBACK_QUERY)
954
1047
 
955
1048
  def on_new_members(self):
1049
+ """Decorator for handling new chat members updates.
1050
+
1051
+ Returns:
1052
+ Callable: A decorator function that registers the callback for new members updates.
1053
+ """
956
1054
  return self.base_handler_decorator(UpdatesTypes.MEMBER_JOINED)
957
1055
 
958
- def on_memebers_left(self):
1056
+ def on_members_left(self):
959
1057
  return self.base_handler_decorator(UpdatesTypes.MEMBER_LEFT)
960
1058
 
961
1059
  def on_pre_checkout_query(self):
1060
+ """Decorator for handling pre-checkout query updates.
1061
+
1062
+ Returns:
1063
+ Callable: A decorator function that registers the callback for pre-checkout query updates.
1064
+ """
962
1065
  return self.base_handler_decorator(UpdatesTypes.PRE_CHECKOUT_QUERY)
963
1066
 
964
1067
  def on_photo(self):
1068
+ """Decorator for handling photo updates.
1069
+
1070
+ Returns:
1071
+ Callable: A decorator function that registers the callback for photo updates.
1072
+ """
965
1073
  return self.base_handler_decorator(UpdatesTypes.PHOTO)
966
1074
 
967
1075
  def on_successful_payment(self):
968
- return self.base_handler_decorator(UpdatesTypes.SUCCESSFUL_PAYMENT)
1076
+ """Decorator for handling successful payment updates.
969
1077
 
1078
+ Returns:
1079
+ Callable: A decorator function that registers the callback for successful payment updates.
1080
+ """
1081
+ return self.base_handler_decorator(UpdatesTypes.SUCCESSFUL_PAYMENT)
1082
+
970
1083
  def _convert_event(self, handler_type: UpdatesTypes, event: Dict[str, Any]) -> Any:
971
1084
  """Convert raw event data to appropriate object type.
972
1085
 
@@ -983,6 +1096,7 @@ class Client:
983
1096
  UpdatesTypes.MEMBER_JOINED,
984
1097
  UpdatesTypes.MEMBER_LEFT,
985
1098
  UpdatesTypes.SUCCESSFUL_PAYMENT,
1099
+ UpdatesTypes.COMMAND
986
1100
  ):
987
1101
  if (
988
1102
  event.get("new_chat_member", False)
@@ -1033,18 +1147,22 @@ class Client:
1033
1147
 
1034
1148
  return event
1035
1149
 
1036
- def add_handler(
1037
- self,
1038
- update_type: UpdatesTypes,
1039
- callback: Callable[[Any], Union[None, Awaitable[None]]],
1040
- ) -> None:
1150
+
1151
+ def add_handler(self, update_type, callback, filter: Optional[Filters] = None, **kwargs):
1041
1152
  """Register a handler for specific update type.
1042
1153
 
1043
1154
  Args:
1044
1155
  update_type (UpdatesTypes): Type of update to handle
1045
1156
  callback (Callable): Function to call when update is received
1046
1157
  """
1047
- self.handlers.append({"type": update_type, "callback": callback})
1158
+ data = {
1159
+ "type": update_type,
1160
+ "callback": callback,
1161
+ "filter": filter,
1162
+ }
1163
+ data.update(kwargs)
1164
+ self.handlers.append(data)
1165
+
1048
1166
 
1049
1167
  def remove_handler(
1050
1168
  self, callback: Callable[[Any], Union[None, Awaitable[None]]]
@@ -1077,17 +1195,16 @@ class Client:
1077
1195
 
1078
1196
  self.running = True
1079
1197
  while self.running:
1080
- if 1:
1198
+ try:
1081
1199
  updates = await self.get_updates(
1082
1200
  offset=self.last_update_id, limit=limit, timeout=timeout
1083
1201
  )
1084
1202
 
1085
1203
  for update in updates:
1086
1204
  await self.process_update(update)
1087
-
1088
- else: # except Exception as e:
1089
- # print(f"Error while polling updates: {e}")
1090
- await asyncio.sleep(5)
1205
+
1206
+ except Exception as e:
1207
+ raise e
1091
1208
 
1092
1209
  async def stop_polling(self) -> None:
1093
1210
  """Stop polling updates."""
@@ -0,0 +1 @@
1
+ from .common import *
@@ -8,3 +8,9 @@ class InvalidTokenException(PyroBaleException):
8
8
 
9
9
  class NotFoundException(PyroBaleException):
10
10
  pass
11
+
12
+ class ForbiddenException(PyroBaleException):
13
+ pass
14
+
15
+ class InternalServerException(PyroBaleException):
16
+ pass
@@ -0,0 +1,2 @@
1
+ from .enum_filters import *
2
+ from .func_filters import *
@@ -0,0 +1,12 @@
1
+ from enum import Enum
2
+
3
+ class Filters(Enum):
4
+ """Filters that you can use in handlers"""
5
+
6
+ TEXT = "text"
7
+ PHOTO = "photo"
8
+ VIDEO = "video"
9
+ AUDIO = "audio"
10
+ VOICE = "voice"
11
+ CONTACT = "contact"
12
+ LOCATION = "location"
@@ -0,0 +1,17 @@
1
+ def equals(expected_text: str):
2
+ """
3
+ Check if the event text or caption is equal to the expected text.
4
+
5
+ Args:
6
+ expected_text (str): The expected text to compare with.
7
+
8
+ Returns:
9
+ Callable: A function that checks if the event text or caption is equal to the expected text.
10
+ """
11
+ def check(event):
12
+ try:
13
+ return getattr(event, "text", None) == expected_text or getattr(event, "caption", None) == expected_text
14
+ except:
15
+ return False
16
+ return check
17
+
@@ -386,6 +386,17 @@ class Chat:
386
386
  bool: True on success
387
387
  """
388
388
  return await self.client.leave_chat(chat_id=self.id)
389
+
390
+ async def is_joined(self, user_id: int) -> bool:
391
+ """Check if a user is joined to the chat.
392
+
393
+ Parameters:
394
+ user_id (int): Unique identifier of the target user
395
+
396
+ Returns:
397
+ bool: True if the user is joined to the chat, False otherwise
398
+ """
399
+ return await self.client.is_joined(user_id, self.id)
389
400
 
390
401
  async def pin(self, message_id: int) -> bool:
391
402
  """Pin a message in the chat.
@@ -11,18 +11,10 @@ class UpdatesTypes(Enum):
11
11
  MEMBER_JOINED = "member_joined"
12
12
  MEMBER_LEFT = "member_left"
13
13
  SUCCESSFUL_PAYMENT = "successful_payment"
14
+ COMMAND = "message"
14
15
 
15
16
 
16
- class Filters(Enum):
17
- """Filters that you can use in handlers"""
18
17
 
19
- TEXT = "text"
20
- PHOTO = "photo"
21
- VIDEO = "video"
22
- AUDIO = "audio"
23
- VOICE = "voice"
24
- CONTACT = "contact"
25
- LOCATION = "location"
26
18
 
27
19
 
28
20
  class ChatAction(Enum):
@@ -15,7 +15,7 @@ class InlineKeyboardMarkup:
15
15
  callback_data: Optional[str] = None,
16
16
  url: Optional[str] = None,
17
17
  web_app: Optional[Union["WebAppInfo", str]] = None,
18
- copy_text_button: Optional["CopyTextButton"] = None,
18
+ copy_text_button: Optional[Union["CopyTextButton", str]] = None,
19
19
  **kwargs
20
20
  ) -> "InlineKeyboardMarkup":
21
21
  """Adds a button to the inline keyboard.
@@ -24,8 +24,8 @@ class InlineKeyboardMarkup:
24
24
  text (str): The text to display on the button.
25
25
  callback_data (str, optional): The callback data to send when the button is clicked.
26
26
  url (str, optional): The URL to open when the button is clicked.
27
- web_app (WebAppInfo, optional): The web app to open when the button is clicked.
28
- copy_text_button (CopyTextButton, optional): The copy text button to add to the button.
27
+ web_app (WebAppInfo OR string, optional): The web app to open when the button is clicked.
28
+ copy_text_button (CopyTextButton OR string, optional): The copy text button to add to the button.
29
29
 
30
30
  Returns:
31
31
  InlineKeyboardMarkup: The updated InlineKeyboardMarkup object.
@@ -59,7 +59,14 @@ class InlineKeyboardMarkup:
59
59
  "web_app must be a string URL or an object with to_dict() method."
60
60
  )
61
61
  elif copy_text_button:
62
- button["copy_text"] = {"text": copy_text_button.text}
62
+ if isinstance(copy_text_button, str):
63
+ button["copy_text"] = {"text": copy_text_button}
64
+ elif hasattr(copy_text_button, "text"):
65
+ button["copy_text"] = {"text": copy_text_button.text}
66
+ else:
67
+ raise ValueError(
68
+ "copy_text_button must be a string or an object with a 'text' attribute."
69
+ )
63
70
 
64
71
  if not self.inline_keyboard:
65
72
  self.inline_keyboard.append([])
@@ -124,6 +124,7 @@ class Message:
124
124
  reply_markup: Inline keyboard markup
125
125
  **kwargs: Additional keyword arguments
126
126
  """
127
+ self.client: Client = kwargs.get("kwargs", {}).get("client")
127
128
  self.id: int = message_id
128
129
  self.user: "User" = (
129
130
  User(**from_user, kwargs={"client": self.client}) if from_user else None
@@ -154,7 +155,7 @@ class Message:
154
155
  self.successful_payment: Optional["SuccessfulPayment"] = successful_payment
155
156
  self.web_app_data: Optional["WebAppData"] = web_app_data
156
157
  self.reply_markup: Optional["InlineKeyboardMarkup"] = reply_markup
157
- self.client: Client = kwargs.get("kwargs", {}).get("client")
158
+
158
159
 
159
160
  async def reply(
160
161
  self,
@@ -313,6 +314,7 @@ class Message:
313
314
  self,
314
315
  latitude: float,
315
316
  longitude: float,
317
+ horizontal_accuracy: Optional[float] = None,
316
318
  reply_markup: Union["ReplyKeyboardMarkup", "InlineKeyboardMarkup"] = None,
317
319
  ):
318
320
  """Reply with a location to the current message.
@@ -327,6 +329,7 @@ class Message:
327
329
  self.chat.id,
328
330
  latitude=latitude,
329
331
  longitude=longitude,
332
+ horizontal_accuracy=horizontal_accuracy,
330
333
  reply_to_message_id=self.id,
331
334
  reply_markup=reply_markup,
332
335
  )
@@ -359,9 +362,7 @@ class Message:
359
362
  description: str,
360
363
  payload: str,
361
364
  provider_token: str,
362
- currency: str,
363
- prices: list,
364
- reply_markup: Union["ReplyKeyboardMarkup", "InlineKeyboardMarkup"] = None,
365
+ prices: list
365
366
  ):
366
367
  """Reply with an invoice to the current message.
367
368
 
@@ -370,7 +371,6 @@ class Message:
370
371
  description: Product description
371
372
  payload: Bot-defined invoice payload
372
373
  provider_token: Payment provider token
373
- currency: Three-letter ISO 4217 currency code
374
374
  prices: Price breakdown (amount in smallest units)
375
375
  reply_markup: Optional keyboard markup
376
376
  """
@@ -381,7 +381,5 @@ class Message:
381
381
  description=description,
382
382
  payload=payload,
383
383
  provider_token=provider_token,
384
- currency=currency,
385
- prices=prices,
386
- reply_markup=reply_markup,
384
+ prices=prices
387
385
  )
@@ -0,0 +1,48 @@
1
+ from ..exceptions import *
2
+ import aiohttp
3
+
4
+
5
+ def build_api_url(base: str, endpoint: str) -> str:
6
+ return f"{base}/{endpoint}"
7
+
8
+
9
+ async def make_post(url: str, data: dict = None, headers: dict = None) -> dict:
10
+ async with aiohttp.ClientSession() as session:
11
+ async with session.post(url, json=data, headers=headers) as response:
12
+ json = await response.json()
13
+ if json['ok']:
14
+ if 'result' in json.keys():
15
+ return json
16
+ else:
17
+ if json['error_code'] == 404:
18
+ raise NotFoundException(f"Error not found 404 : {json['description'] if json['description'] else 'No description returned in error'}")
19
+ elif json['error_code'] == 403:
20
+ raise ForbiddenException(f"Error Forbidden 403 : {json['description'] if json['description'] else 'No description returned in error'}")
21
+ else:
22
+ raise PyroBaleException(f"unknown error : {json['description'] if json['description'] else 'No description!'}")
23
+
24
+
25
+ async def make_get(url: str, headers: dict = None) -> dict:
26
+ async with aiohttp.ClientSession() as session:
27
+ async with session.get(url, headers=headers) as response:
28
+ json = await response.json()
29
+ if json['ok']:
30
+ if 'result' in json.keys():
31
+ return json
32
+ else:
33
+ if json['error_code'] == 404:
34
+ raise NotFoundException(f"Error not found 404 : {json['description'] if json['description'] else 'No description returned in error'}")
35
+ elif json['error_code'] == 403:
36
+ raise ForbiddenException(f"Error Forbidden 403 : {json['description'] if json['description'] else 'No description returned in error'}")
37
+ else:
38
+ raise PyroBaleException(f"unknown error : {json['description'] if json['description'] else 'No description'}")
39
+
40
+
41
+ def pythonize(dictionary: dict) -> dict:
42
+ """Converts a dictionary with keys in snake_case to camelCase."""
43
+ result = {}
44
+ for key, value in dictionary.items():
45
+ if key == "from":
46
+ key = "from_user"
47
+ result[key] = value
48
+ return result
File without changes
File without changes
File without changes
@@ -1,27 +0,0 @@
1
- import aiohttp
2
-
3
-
4
- def build_api_url(base: str, endpoint: str) -> str:
5
- return f"{base}/{endpoint}"
6
-
7
-
8
- async def make_post(url: str, data: dict = None, headers: dict = None) -> dict:
9
- async with aiohttp.ClientSession() as session:
10
- async with session.post(url, json=data, headers=headers) as response:
11
- return await response.json()
12
-
13
-
14
- async def make_get(url: str, headers: dict = None) -> dict:
15
- async with aiohttp.ClientSession() as session:
16
- async with session.get(url, headers=headers) as response:
17
- return await response.json()
18
-
19
-
20
- def pythonize(dictionary: dict) -> dict:
21
- """Converts a dictionary with keys in snake_case to camelCase."""
22
- result = {}
23
- for key, value in dictionary.items():
24
- if key == "from":
25
- key = "from_user"
26
- result[key] = value
27
- return result