pyrobale 0.3.7__tar.gz → 0.3.9__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 (61) hide show
  1. {pyrobale-0.3.7 → pyrobale-0.3.9}/.github/workflows/docs.yml +1 -1
  2. pyrobale-0.3.9/.github/workflows/pypi.yml +36 -0
  3. pyrobale-0.3.9/.gitignore +1 -0
  4. {pyrobale-0.3.7 → pyrobale-0.3.9}/LICENSE +2 -2
  5. {pyrobale-0.3.7 → pyrobale-0.3.9}/PKG-INFO +7 -7
  6. {pyrobale-0.3.7 → pyrobale-0.3.9}/README.md +3 -4
  7. pyrobale-0.3.9/examples/command.py +12 -0
  8. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyproject.toml +4 -4
  9. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/StateMachine/__init__.py +21 -0
  10. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/__init__.py +3 -1
  11. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/client/__init__.py +250 -27
  12. pyrobale-0.3.9/pyrobale/exceptions/__init__.py +1 -0
  13. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/exceptions/common.py +6 -0
  14. pyrobale-0.3.9/pyrobale/filters/__init__.py +2 -0
  15. pyrobale-0.3.9/pyrobale/filters/enum_filters.py +12 -0
  16. pyrobale-0.3.9/pyrobale/filters/func_filters.py +17 -0
  17. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/chat.py +11 -0
  18. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/enums.py +1 -9
  19. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/inlinekeyboardmarkup.py +11 -4
  20. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/message.py +6 -8
  21. pyrobale-0.3.9/pyrobale/objects/peerdata.py +25 -0
  22. pyrobale-0.3.9/pyrobale/objects/utils.py +48 -0
  23. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale.png +0 -0
  24. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobaletext.png +0 -0
  25. pyrobale-0.3.9/version.py +2 -0
  26. pyrobale-0.3.7/pyrobale/exceptions/__init__.py +0 -0
  27. pyrobale-0.3.7/pyrobale/objects/utils.py +0 -27
  28. {pyrobale-0.3.7 → pyrobale-0.3.9}/.github/ISSUE_TEMPLATE/issue-template-/342/204/271/357/270/217.md" +0 -0
  29. {pyrobale-0.3.7 → pyrobale-0.3.9}/examples/echo_bot.py +0 -0
  30. {pyrobale-0.3.7 → pyrobale-0.3.9}/examples/handler_system.py +0 -0
  31. {pyrobale-0.3.7 → pyrobale-0.3.9}/examples/inline_keyboard.py +0 -0
  32. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/__init__.py +0 -0
  33. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/animation.py +0 -0
  34. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/audio.py +0 -0
  35. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/callbackquery.py +0 -0
  36. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/chatmember.py +0 -0
  37. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/chatphoto.py +0 -0
  38. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/contact.py +0 -0
  39. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/copytextbutton.py +0 -0
  40. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/document.py +0 -0
  41. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/file.py +0 -0
  42. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/inlinekeyboardbutton.py +0 -0
  43. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/inputfile.py +0 -0
  44. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/inputmedias.py +0 -0
  45. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/invoice.py +0 -0
  46. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/keyboardbutton.py +0 -0
  47. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/labeledprice.py +0 -0
  48. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/location.py +0 -0
  49. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/messageid.py +0 -0
  50. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/photosize.py +0 -0
  51. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/precheckoutquery.py +0 -0
  52. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/replykeyboardmarkup.py +0 -0
  53. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/sticker.py +0 -0
  54. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/stickerset.py +0 -0
  55. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/successfulpayment.py +0 -0
  56. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/update.py +0 -0
  57. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/user.py +0 -0
  58. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/video.py +0 -0
  59. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/voice.py +0 -0
  60. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/webappdata.py +0 -0
  61. {pyrobale-0.3.7 → pyrobale-0.3.9}/pyrobale/objects/webappinfo.py +0 -0
@@ -22,7 +22,7 @@ jobs:
22
22
 
23
23
  - name: Install dependencies
24
24
  run: |
25
- pip install pdoc aiohttp
25
+ pip install pdoc aiohttp beautifulsoup4
26
26
 
27
27
  - name: Generate documentation
28
28
  run: |
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+
7
+ jobs:
8
+ deploy:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v4
16
+ with:
17
+ python-version: '3.x'
18
+
19
+ - name: Extract version info
20
+ id: version
21
+ run: |
22
+ STABLE=$(python -c "from version import stable; print(str(stable).lower())")
23
+ VERSION=$(python -c "from version import version; print(version)")
24
+ echo "stable=$STABLE" >> $GITHUB_OUTPUT
25
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
26
+ echo "Stable: $STABLE, Version: $VERSION"
27
+
28
+ - name: Build and publish
29
+ if: steps.version.outputs.stable == 'true'
30
+ run: |
31
+ pip install build twine
32
+ python -m build
33
+ twine upload dist/* --username __token__ --password ${{ secrets.PYPI }}
34
+ env:
35
+ TWINE_USERNAME: __token__
36
+ TWINE_PASSWORD: ${{ secrets.PYPI }}
@@ -0,0 +1 @@
1
+ dist
@@ -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.9
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
- Author-email: Ali Safamanesh <darg.q.a.a@gmail.com>, Aydin Rahbaran <codewizaard9@gmail.com>
7
+ Author-email: Ali Safamanesh <daradege@proton.me>, 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
@@ -32,6 +32,7 @@ Classifier: Operating System :: OS Independent
32
32
  Classifier: Programming Language :: Python :: 3
33
33
  Requires-Python: >=3.9
34
34
  Requires-Dist: aiohttp
35
+ Requires-Dist: beautifulsoup4
35
36
  Description-Content-Type: text/markdown
36
37
 
37
38
  ![pyrobaletext](https://raw.githubusercontent.com/pyrobale/pyrobale/refs/heads/main/pyrobaletext.png)
@@ -49,7 +50,7 @@ A modern, easy-to-use Python wrapper for the Bale Bot API that makes building Ba
49
50
  - 📁 **File Handling** - Easy upload and download of media files
50
51
  - 🛡️ **Error Handling** - Comprehensive exception handling
51
52
  - 📖 **Type Hints** - Full typing support for better development experience
52
- - ⚡ **Async Support** - Both synchronous and asynchronous operations
53
+ - ⚡ **Async Support** - asynchronous operations
53
54
 
54
55
  ## Installation
55
56
 
@@ -66,7 +67,7 @@ from pyrobale.objects import Message, UpdatesTypes
66
67
  bot = Client("YOUR_BOT_TOKEN")
67
68
 
68
69
  @bot.on_message()
69
- async def message_handler(message: User):
70
+ async def message_handler(message: Message):
70
71
  await message.reply("Hello, world!")
71
72
 
72
73
  bot.run()
@@ -78,13 +79,12 @@ bot.run()
78
79
  ```python
79
80
  from pyrobale.objects import *
80
81
  from pyrobale.client import Client, Message, UpdatesTypes
81
- import asyncio
82
82
 
83
83
  client = Client("YOUR_BOT_TOKEN")
84
84
 
85
85
  async def handle_message(message: Message):
86
86
  if message.text == "/start":
87
- await message.reply("سلام! من یک ربات PyRoBale هستم!")
87
+ await message.reply("Hi! Im a pyrobale RoBot!")
88
88
  await client.wait_for(UpdatesTypes.MESSAGE)
89
89
  await message.reply("Okay! wait_for Test Compeleted")
90
90
 
@@ -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,9 +1,9 @@
1
1
  [project]
2
2
  name = "pyrobale"
3
- version = "0.3.7"
3
+ version = "0.3.9"
4
4
  authors = [
5
- { name = "Ali Safamanesh", email = "darg.q.a.a@gmail.com" },
6
- { name = "Aydin Rahbaran", email = "codewizaard9@gmail.com"},
5
+ { name = "Ali Safamanesh", email = "daradege@proton.me" },
6
+ { name = "Aydin Rahbaran", email = "codewizaard9@gmail.com"}
7
7
  ]
8
8
  description = "A python wrapper for bale api"
9
9
  readme = "README.md"
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Operating System :: OS Independent",
15
15
  ]
16
16
  license = { file = "LICENSE" }
17
- dependencies = ["aiohttp"]
17
+ dependencies = ["aiohttp", "beautifulsoup4"]
18
18
  [project.urls]
19
19
  github = "https://github.com/pyrobale/pyrobale"
20
20
  website = "https://pyrobale.github.io"
@@ -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
@@ -37,11 +37,16 @@ from ..objects.voice import Voice
37
37
  from ..objects.webappdata import WebAppData
38
38
  from ..objects.webappinfo import WebAppInfo
39
39
  from ..objects.utils import *
40
- import asyncio
41
- from enum import Enum
42
40
  from ..objects.enums import UpdatesTypes, ChatAction, ChatType
41
+ from ..objects.peerdata import PeerData
42
+ from ..filters import Filters, equals
43
43
  from ..StateMachine import StateMachine
44
+ from ..exceptions import NotFoundException, InvalidTokenException, PyroBaleException
44
45
 
46
+ from enum import Enum
47
+ import asyncio
48
+ from bs4 import BeautifulSoup
49
+ from json import loads, JSONDecodeError
45
50
 
46
51
  class Client:
47
52
  """A client for interacting with the Bale messenger API.
@@ -62,6 +67,9 @@ class Client:
62
67
  self.last_update_id = 0
63
68
  self.state_machine = StateMachine()
64
69
 
70
+ self.check_defined_message = True
71
+ self.defined_messages = {}
72
+
65
73
  async def get_updates(
66
74
  self,
67
75
  offset: Optional[int] = None,
@@ -81,7 +89,12 @@ class Client:
81
89
  self.requests_base
82
90
  + f"/getUpdates?offset={offset}&limit={limit}&timeout={timeout}"
83
91
  )
84
- return data["result"]
92
+ if data['ok']:
93
+ if 'result' in data.keys():
94
+ return data["result"]
95
+ else:
96
+ if data['error_code'] == 403:
97
+ raise InvalidTokenException("Forbidden 403 : --ENTERED TOKEN IS NOT VALID--")
85
98
 
86
99
  async def set_webhook(self, url: str) -> bool:
87
100
  """Set the webhook for the bot.
@@ -421,6 +434,7 @@ class Client:
421
434
  chat_id: int,
422
435
  latitude: float,
423
436
  longitude: float,
437
+ horizontal_accuracy: Optional[float] = None,
424
438
  reply_to_message_id: Optional[int] = None,
425
439
  reply_markup: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
426
440
  ) -> Message:
@@ -441,6 +455,7 @@ class Client:
441
455
  "chat_id": chat_id,
442
456
  "latitude": latitude,
443
457
  "longitude": longitude,
458
+ "horizontal_accuracy": horizontal_accuracy,
444
459
  "reply_to_message_id": reply_to_message_id,
445
460
  "reply_markup": reply_markup.to_dict() if reply_markup else None,
446
461
  },
@@ -507,6 +522,9 @@ class Client:
507
522
  Returns:
508
523
  Message: returns the sent message with invoice
509
524
  """
525
+ new_prices = []
526
+ for price in prices:
527
+ new_prices.append(price.json)
510
528
  data = await make_post(
511
529
  self.requests_base + "/sendInvoice",
512
530
  data={
@@ -515,7 +533,7 @@ class Client:
515
533
  "description": description,
516
534
  "payload": payload,
517
535
  "provider_token": provider_token,
518
- "prices": prices,
536
+ "prices": new_prices,
519
537
  "photo_url": photo_url,
520
538
  "reply_to_message_id": reply_to_message_id,
521
539
  },
@@ -683,6 +701,22 @@ class Client:
683
701
  self.requests_base + "/leaveChat", data={"chat_id": chat_id}
684
702
  )
685
703
  return data.get("ok", False)
704
+
705
+ async def is_joined(self, user_id: int, chat_id: int) -> bool:
706
+ """Check if a user is joined to a chat.
707
+
708
+ Args:
709
+ user_id (int): Unique identifier for the target chat
710
+ chat_id (int): Unique identifier for the target chat
711
+
712
+ Returns:
713
+ bool: True if the user is joined to the chat, False otherwise
714
+ """
715
+ data = await make_post(
716
+ self.requests_base + "/getChatMember",
717
+ data={"chat_id": chat_id, "user_id": user_id},
718
+ )
719
+ return data.get("result", {}).get("status") in ["member", "creator", "administrator"]
686
720
 
687
721
  async def get_chat(self, chat_id: int) -> Chat:
688
722
  """Get up to date information about the chat.
@@ -697,7 +731,100 @@ class Client:
697
731
  self.requests_base + "/getChat", data={"chat_id": chat_id}
698
732
  )
699
733
  return Chat(**pythonize(data["result"]))
700
-
734
+
735
+ @staticmethod
736
+ async def get_ble_ir_page(username_or_phone_number: str) -> Union[dict, PeerData]:
737
+ """Get BleIR user/group information.
738
+
739
+ Args:
740
+ username_or_phone_number (str): Username or phone number
741
+
742
+ Returns:
743
+ Union[dict, PeerData]: User/group information or error dict
744
+ """
745
+ url = f"https://ble.ir/{username_or_phone_number}"
746
+
747
+ async with aiohttp.ClientSession() as session:
748
+ async with session.get(url) as response:
749
+ req = await response.text()
750
+
751
+ if """<p class="__404_title__lxIKL">گفتگوی مورد نظر وجود ندارد.</p>""" in req:
752
+ return PeerData(
753
+ is_ok=False,
754
+ avatar=None,
755
+ description=None,
756
+ name=None,
757
+ is_bot=None,
758
+ is_verified=None,
759
+ is_private=None,
760
+ members=None,
761
+ last_message=None,
762
+ user_id=None,
763
+ username=None,
764
+ )
765
+
766
+ soup = BeautifulSoup(req, "html.parser")
767
+ json_data = {}
768
+
769
+ try:
770
+ json_script = soup.find("script", id="__NEXT_DATA__").text
771
+ json_data = loads(json_script)
772
+ page_props = json_data.get("props", {}).get("pageProps", {})
773
+ user_data = page_props.get("user", {})
774
+ group_data = page_props.get("group", {})
775
+ messages = page_props.get("messages", [])
776
+ except (AttributeError, KeyError, JSONDecodeError):
777
+ pass
778
+
779
+ try:
780
+ avatar = soup.find("img", class_="Avatar_img___C2_3")["src"]
781
+ except (AttributeError, KeyError):
782
+ avatar = None
783
+
784
+ try:
785
+ description = soup.find("div", class_="Profile_description__YTAr_").text
786
+ except AttributeError:
787
+ description = None
788
+
789
+ try:
790
+ name = soup.find("h1", class_="Profile_name__pQglx").text
791
+ except AttributeError:
792
+ name = None
793
+
794
+ is_bot = user_data.get("isBot", False)
795
+ is_verified = user_data.get("isVerified", group_data.get("isVerified", False))
796
+ is_private = user_data.get("isPrivate", group_data.get("isPrivate", False))
797
+ members = group_data.get("members")
798
+ username = user_data.get("nick")
799
+ user_id = page_props.get("peer", {}).get("id")
800
+
801
+ last_message = None
802
+ if messages:
803
+ try:
804
+ last_msg = messages[-1]["message"]
805
+ last_message = (
806
+ last_msg.get("documentMessage", {}).get("caption", {}).get("text")
807
+ or last_msg.get("textMessage", {}).get("text")
808
+ )
809
+ if last_message:
810
+ last_message = last_message.replace("&zwnj;", "")
811
+ except (KeyError, IndexError):
812
+ pass
813
+
814
+ return PeerData(
815
+ True,
816
+ avatar,
817
+ description,
818
+ name,
819
+ is_bot,
820
+ is_verified,
821
+ is_private,
822
+ members,
823
+ last_message,
824
+ user_id,
825
+ username
826
+ )
827
+
701
828
  async def get_chat_members_count(self, chat_id: int) -> int:
702
829
  """Get the number of members in a chat.
703
830
 
@@ -913,6 +1040,11 @@ class Client:
913
1040
  if update_id:
914
1041
  self.last_update_id = update_id + 1
915
1042
 
1043
+ if self.check_defined_message:
1044
+ update_raw = update['message']
1045
+ if update_raw.get("text") in self.defined_messages.keys():
1046
+ await self.send_message(update_raw.get('chat').get('id'), self.defined_messages.get(update_raw.get("text")), update.get('message_id'))
1047
+
916
1048
  for waiter in list(self._waiters):
917
1049
  w_type, check, future = waiter
918
1050
  if w_type.value in update:
@@ -922,51 +1054,138 @@ class Client:
922
1054
  if not future.done():
923
1055
  future.set_result(event)
924
1056
  self._waiters.remove(waiter)
925
- return update
1057
+ return
926
1058
 
927
1059
  for handler in self.handlers:
928
1060
  update_type = handler["type"].value
929
1061
  if update_type in update:
930
- event = update[update_type]
931
-
932
- event = self._convert_event(handler["type"], event)
933
-
1062
+ raw_event = update[update_type]
1063
+
1064
+
1065
+ event = self._convert_event(handler["type"], raw_event)
1066
+
1067
+ if handler["type"] == UpdatesTypes.COMMAND:
1068
+ if hasattr(event, 'text') and event.text and event.text.startswith('/'):
1069
+ command_text = event.text[1:]
1070
+ command_parts = command_text.split()
1071
+ if command_parts:
1072
+ actual_command = command_parts[0]
1073
+ expected_command = handler.get("command", "")
1074
+
1075
+ if actual_command != expected_command:
1076
+ continue
1077
+
1078
+ flt = handler.get("filter")
1079
+ if flt is not None:
1080
+ if callable(flt):
1081
+ try:
1082
+ if not flt(event):
1083
+ continue
1084
+ except Exception as e:
1085
+ print(f"[Filter Error] {e}")
1086
+ continue
1087
+ elif isinstance(flt, Filters):
1088
+ if not hasattr(event, flt.value):
1089
+ continue
1090
+
934
1091
  if asyncio.iscoroutinefunction(handler["callback"]):
935
1092
  asyncio.create_task(handler["callback"](event))
936
1093
  else:
937
1094
  handler["callback"](event)
938
1095
 
1096
+
1097
+
939
1098
  def base_handler_decorator(self, update_type: UpdatesTypes):
1099
+ """Base decorator for handling different types of updates.
1100
+
1101
+ Args:
1102
+ update_type (UpdatesTypes): The type of update to handle.
1103
+
1104
+ Returns:
1105
+ Callable: A decorator function that registers the callback for the specified update type.
1106
+ """
1107
+ def wrapper(filter: Optional[Filters] = None):
1108
+ def decorator(callback: Callable[[Any], Union[None, Awaitable[None]]]):
1109
+ self.add_handler(update_type, callback, filter)
1110
+ return callback
1111
+ return decorator
1112
+ return wrapper
1113
+
1114
+ def on_command(self, command: str, filter: Optional[Filters] = None):
1115
+ """Decorator for handling command updates.
1116
+
1117
+ Args:
1118
+ command (str): The command to handle.
1119
+ filter (Optional[Filters]): An optional filter to apply to the command.
1120
+ Returns:
1121
+ Callable: A decorator function that registers the callback for the specified command.
1122
+ """
940
1123
  def decorator(callback: Callable[[Any], Union[None, Awaitable[None]]]):
941
- self.add_handler(update_type, callback)
1124
+ self.add_handler(UpdatesTypes.COMMAND, callback, filter, command=command)
942
1125
  return callback
943
-
944
1126
  return decorator
945
1127
 
946
- def on_message(self):
947
- return self.base_handler_decorator(UpdatesTypes.MESSAGE)
1128
+
1129
+ def on_message(self, filter: Optional[Filters] = None):
1130
+ """Decorator for handling new message updates.
1131
+
1132
+ Returns:
1133
+ Callable: A decorator function that registers the callback for message updates.
1134
+ """
1135
+ return self.base_handler_decorator(UpdatesTypes.MESSAGE)(filter)
1136
+
948
1137
 
949
1138
  def on_edited_message(self):
1139
+ """Decorator for handling edited message updates.
1140
+
1141
+ Returns:
1142
+ Callable: A decorator function that registers the callback for edited message updates.
1143
+ """
950
1144
  return self.base_handler_decorator(UpdatesTypes.MESSAGE_EDITED)
951
1145
 
952
1146
  def on_callback_query(self):
1147
+ """Decorator for handling callback query updates.
1148
+
1149
+ Returns:
1150
+ Callable: A decorator function that registers the callback for callback query updates.
1151
+ """
953
1152
  return self.base_handler_decorator(UpdatesTypes.CALLBACK_QUERY)
954
1153
 
955
1154
  def on_new_members(self):
1155
+ """Decorator for handling new chat members updates.
1156
+
1157
+ Returns:
1158
+ Callable: A decorator function that registers the callback for new members updates.
1159
+ """
956
1160
  return self.base_handler_decorator(UpdatesTypes.MEMBER_JOINED)
957
1161
 
958
- def on_memebers_left(self):
1162
+ def on_members_left(self):
959
1163
  return self.base_handler_decorator(UpdatesTypes.MEMBER_LEFT)
960
1164
 
961
1165
  def on_pre_checkout_query(self):
1166
+ """Decorator for handling pre-checkout query updates.
1167
+
1168
+ Returns:
1169
+ Callable: A decorator function that registers the callback for pre-checkout query updates.
1170
+ """
962
1171
  return self.base_handler_decorator(UpdatesTypes.PRE_CHECKOUT_QUERY)
963
1172
 
964
1173
  def on_photo(self):
1174
+ """Decorator for handling photo updates.
1175
+
1176
+ Returns:
1177
+ Callable: A decorator function that registers the callback for photo updates.
1178
+ """
965
1179
  return self.base_handler_decorator(UpdatesTypes.PHOTO)
966
1180
 
967
1181
  def on_successful_payment(self):
968
- return self.base_handler_decorator(UpdatesTypes.SUCCESSFUL_PAYMENT)
1182
+ """Decorator for handling successful payment updates.
969
1183
 
1184
+ Returns:
1185
+ Callable: A decorator function that registers the callback for successful payment updates.
1186
+ """
1187
+ return self.base_handler_decorator(UpdatesTypes.SUCCESSFUL_PAYMENT)
1188
+
970
1189
  def _convert_event(self, handler_type: UpdatesTypes, event: Dict[str, Any]) -> Any:
971
1190
  """Convert raw event data to appropriate object type.
972
1191
 
@@ -983,6 +1202,7 @@ class Client:
983
1202
  UpdatesTypes.MEMBER_JOINED,
984
1203
  UpdatesTypes.MEMBER_LEFT,
985
1204
  UpdatesTypes.SUCCESSFUL_PAYMENT,
1205
+ UpdatesTypes.COMMAND
986
1206
  ):
987
1207
  if (
988
1208
  event.get("new_chat_member", False)
@@ -1033,18 +1253,22 @@ class Client:
1033
1253
 
1034
1254
  return event
1035
1255
 
1036
- def add_handler(
1037
- self,
1038
- update_type: UpdatesTypes,
1039
- callback: Callable[[Any], Union[None, Awaitable[None]]],
1040
- ) -> None:
1256
+
1257
+ def add_handler(self, update_type, callback, filter: Optional[Filters] = None, **kwargs):
1041
1258
  """Register a handler for specific update type.
1042
1259
 
1043
1260
  Args:
1044
1261
  update_type (UpdatesTypes): Type of update to handle
1045
1262
  callback (Callable): Function to call when update is received
1046
1263
  """
1047
- self.handlers.append({"type": update_type, "callback": callback})
1264
+ data = {
1265
+ "type": update_type,
1266
+ "callback": callback,
1267
+ "filter": filter,
1268
+ }
1269
+ data.update(kwargs)
1270
+ self.handlers.append(data)
1271
+
1048
1272
 
1049
1273
  def remove_handler(
1050
1274
  self, callback: Callable[[Any], Union[None, Awaitable[None]]]
@@ -1077,17 +1301,16 @@ class Client:
1077
1301
 
1078
1302
  self.running = True
1079
1303
  while self.running:
1080
- if 1:
1304
+ try:
1081
1305
  updates = await self.get_updates(
1082
1306
  offset=self.last_update_id, limit=limit, timeout=timeout
1083
1307
  )
1084
1308
 
1085
1309
  for update in updates:
1086
1310
  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)
1311
+
1312
+ except Exception as e:
1313
+ raise e
1091
1314
 
1092
1315
  async def stop_polling(self) -> None:
1093
1316
  """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,25 @@
1
+ class PeerData:
2
+ """A class to parse data received from ble.ir pages."""
3
+ def __init__(self,
4
+ is_ok: bool,
5
+ avatar: str,
6
+ description: str,
7
+ name: str,
8
+ is_bot: bool,
9
+ is_verified: bool,
10
+ is_private: bool,
11
+ members: int,
12
+ last_message: str,
13
+ user_id: int,
14
+ username: str):
15
+ self.is_ok = is_ok
16
+ self.avatar = avatar
17
+ self.description = description
18
+ self.name = name
19
+ self.is_bot = is_bot
20
+ self.is_verified = is_verified
21
+ self.is_private = is_private
22
+ self.members = members
23
+ self.last_message = last_message
24
+ self.user_id = user_id
25
+ self.username = username
@@ -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
@@ -0,0 +1,2 @@
1
+ stable = True
2
+ version = "0.3.9"
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
File without changes