zalobot-python 0.1.3__tar.gz → 0.2.0__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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.3
2
+ Name: zalobot-python
3
+ Version: 0.2.0
4
+ Summary: Python SDK for the Zalo Bot API
5
+ Author: NovaH00
6
+ Author-email: NovaH00 <trantay2006super@gmail.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Dist: requests>=2.32.5
10
+ Requires-Python: >=3.14
11
+ Description-Content-Type: text/markdown
12
+
13
+ [Tiếng Việt](README-vi.md)
14
+ # ZaloBot Python SDK (Work in Progress)
15
+
16
+ A modern, fully-typed, and asynchronous Python SDK for the ZaloBot API. This library was built as a more ergonomic and developer-friendly alternative to the official SDK.
17
+
18
+ ## Features
19
+
20
+ | Feature | Description |
21
+ |---------|-------------|
22
+ | **Fully Typed & Documented** | Complete type hints and docstrings. No need to dig through source code to figure out expected data types. |
23
+ | **Pydantic Data Modeling** | All API responses are validated and converted into Pydantic models. This improves Developer Experience (DX) and significantly reduces errors related to raw JSON parsing. |
24
+ | **True Asynchronous Execution** | Non-blocking async operations designed to avoid runtime errors, even when used within applications where the main thread is fully occupied. |
25
+ | **Ergonomic Webhooks** | Simplified webhook configuration and event dispatching mechanisms. |
26
+
27
+ ## Installation
28
+
29
+ You can install the SDK using your preferred package manager:
30
+
31
+ **Using uv:**
32
+ ~~~bash
33
+ uv add zalobot_python
34
+ ~~~
35
+
36
+ **Using pip:**
37
+ ~~~bash
38
+ pip install zalobot_python
39
+ ~~~
40
+
41
+ ## API Coverage
42
+
43
+ Methods formatted in `camelCase` directly map to the equivalent Zalo API endpoints.
44
+
45
+ | Method | Zalo Endpoint | Status |
46
+ |--------|---------------|--------|
47
+ | `getMe()` | `/getMe` | Implemented |
48
+ | `getUpdates()` | `/getUpdates` | Implemented |
49
+ | `setWebhook()` | `/setWebhook` | Implemented |
50
+ | `deleteWebhook()` | `/deleteWebhook` | Implemented |
51
+ | `getWebhookInfo()` | `/getWebhookInfo` | Implemented |
52
+ | `sendMessage()` | `/sendMessage` | Implemented |
53
+ | `sendPhoto()` | `/sendPhoto` | Planned |
54
+ | `sendSticker()` | `/sendSticker` | Planned |
55
+ | `sendChatAction()` | `/sendChatAction` | Planned |
56
+
57
+ ## Usage
58
+
59
+ ### Basic Usage
60
+
61
+ The simplest way to use the bot is to instantiate it with your token and call the available endpoint methods.
62
+
63
+ ~~~python
64
+ import asyncio
65
+ from zalobot_python import ZaloBot, BotInfo
66
+
67
+ bot = ZaloBot(BOT_TOKEN="<BOT_TOKEN>")
68
+
69
+ async def main():
70
+ bot_info: BotInfo = await bot.getMe()
71
+
72
+ print(f"Bot ID: {bot_info.id}")
73
+ print(f"Bot Name: {bot_info.display_name}")
74
+
75
+ if __name__ == "__main__":
76
+ asyncio.run(main())
77
+ ~~~
78
+
79
+ ### Webhook Usage
80
+
81
+ While you can technically poll `.getUpdates()` to receive new events, using a webhook is significantly more efficient. The SDK provides built-in mechanisms to easily configure and handle webhooks.
82
+
83
+ ~~~python
84
+ from zalobot_python import ZaloBot, Context, AsyncWebhookHandler, ZaloAPIResponse, Event
85
+
86
+ # 1. Initialize the standard bot
87
+ normal_bot = ZaloBot("<BOT_TOKEN>")
88
+
89
+ # 2. Define your webhook handlers
90
+ async def echo(ctx: Context):
91
+ message_info = await ctx.reply(f"You sent: {ctx.text}")
92
+ print("Sent message ID:", message_info.message_id)
93
+
94
+ async def log_event(ctx: Context):
95
+ if ctx.is_text:
96
+ print("Chat ID:", ctx.chat_id)
97
+ print("Message:", ctx.text)
98
+
99
+ async def main():
100
+ # Convert the standard bot into a webhook-enabled bot
101
+ # (All previously available endpoint methods remain usable)
102
+ webhook_bot = await normal_bot.configure_webhook("https://your-domain.com/webhook")
103
+
104
+ # Retrieve the auto-generated secret token.
105
+ # Use this to validate the X-Bot-Api-Secret-Token header in incoming requests.
106
+ secret_token = webhook_bot.get_secret_token()
107
+
108
+ # Register handlers
109
+ webhook_bot.add_webhook_handler(echo)
110
+ webhook_bot.add_webhook_handler(log_event)
111
+
112
+ # Note: The request handling flow below should be implemented
113
+ # inside your web framework's endpoint (e.g., FastAPI, Flask).
114
+ ~~~
115
+
116
+ #### Request Handling Flow (For Web Frameworks)
117
+
118
+ When implementing the actual route in your web framework, follow these steps to process incoming events:
119
+
120
+ 1. **Parse the payload:** Parse the incoming JSON body into a `ZaloAPIResponse[Event]`. The actual payload resides in the `result` field.
121
+ ~~~python
122
+ parsed: ZaloAPIResponse[Event] = ZaloAPIResponse.model_validate(request.json())
123
+ ~~~
124
+
125
+ 2. **Extract the event:**
126
+ ~~~python
127
+ event = parsed.result
128
+ ~~~
129
+
130
+ 3. **Dispatch the event:** Pass the event to your registered handlers.
131
+ ~~~python
132
+ await webhook_bot.dispatch_webhook_handlers(event)
133
+ ~~~
134
+
135
+ ## Architecture Note: Webhook State Management
136
+
137
+ To ensure strict type safety, the `ZaloBot` class is a generic that relies on two specific states:
138
+
139
+ | State | Description |
140
+ |-------|-------------|
141
+ | `UnconfiguredWebhook` | The default state when `ZaloBot` is instantiated. |
142
+ | `ConfiguredWebhook` | The state returned after calling `.configure_webhook()`. |
143
+
144
+ **Why does this matter?**
145
+ All helper methods written in `snake_case` (such as `add_webhook_handler`) are specific to webhook operations. Because of the generic state design, static analyzers (like Pyright or MyPy) will flag an error if you attempt to call a `snake_case` method on an unconfigured bot (`ZaloBot[UnconfiguredWebhook]`). These methods are only exposed and valid on instances of `ZaloBot[ConfiguredWebhook]`.
@@ -0,0 +1,133 @@
1
+ [Tiếng Việt](README-vi.md)
2
+ # ZaloBot Python SDK (Work in Progress)
3
+
4
+ A modern, fully-typed, and asynchronous Python SDK for the ZaloBot API. This library was built as a more ergonomic and developer-friendly alternative to the official SDK.
5
+
6
+ ## Features
7
+
8
+ | Feature | Description |
9
+ |---------|-------------|
10
+ | **Fully Typed & Documented** | Complete type hints and docstrings. No need to dig through source code to figure out expected data types. |
11
+ | **Pydantic Data Modeling** | All API responses are validated and converted into Pydantic models. This improves Developer Experience (DX) and significantly reduces errors related to raw JSON parsing. |
12
+ | **True Asynchronous Execution** | Non-blocking async operations designed to avoid runtime errors, even when used within applications where the main thread is fully occupied. |
13
+ | **Ergonomic Webhooks** | Simplified webhook configuration and event dispatching mechanisms. |
14
+
15
+ ## Installation
16
+
17
+ You can install the SDK using your preferred package manager:
18
+
19
+ **Using uv:**
20
+ ~~~bash
21
+ uv add zalobot_python
22
+ ~~~
23
+
24
+ **Using pip:**
25
+ ~~~bash
26
+ pip install zalobot_python
27
+ ~~~
28
+
29
+ ## API Coverage
30
+
31
+ Methods formatted in `camelCase` directly map to the equivalent Zalo API endpoints.
32
+
33
+ | Method | Zalo Endpoint | Status |
34
+ |--------|---------------|--------|
35
+ | `getMe()` | `/getMe` | Implemented |
36
+ | `getUpdates()` | `/getUpdates` | Implemented |
37
+ | `setWebhook()` | `/setWebhook` | Implemented |
38
+ | `deleteWebhook()` | `/deleteWebhook` | Implemented |
39
+ | `getWebhookInfo()` | `/getWebhookInfo` | Implemented |
40
+ | `sendMessage()` | `/sendMessage` | Implemented |
41
+ | `sendPhoto()` | `/sendPhoto` | Planned |
42
+ | `sendSticker()` | `/sendSticker` | Planned |
43
+ | `sendChatAction()` | `/sendChatAction` | Planned |
44
+
45
+ ## Usage
46
+
47
+ ### Basic Usage
48
+
49
+ The simplest way to use the bot is to instantiate it with your token and call the available endpoint methods.
50
+
51
+ ~~~python
52
+ import asyncio
53
+ from zalobot_python import ZaloBot, BotInfo
54
+
55
+ bot = ZaloBot(BOT_TOKEN="<BOT_TOKEN>")
56
+
57
+ async def main():
58
+ bot_info: BotInfo = await bot.getMe()
59
+
60
+ print(f"Bot ID: {bot_info.id}")
61
+ print(f"Bot Name: {bot_info.display_name}")
62
+
63
+ if __name__ == "__main__":
64
+ asyncio.run(main())
65
+ ~~~
66
+
67
+ ### Webhook Usage
68
+
69
+ While you can technically poll `.getUpdates()` to receive new events, using a webhook is significantly more efficient. The SDK provides built-in mechanisms to easily configure and handle webhooks.
70
+
71
+ ~~~python
72
+ from zalobot_python import ZaloBot, Context, AsyncWebhookHandler, ZaloAPIResponse, Event
73
+
74
+ # 1. Initialize the standard bot
75
+ normal_bot = ZaloBot("<BOT_TOKEN>")
76
+
77
+ # 2. Define your webhook handlers
78
+ async def echo(ctx: Context):
79
+ message_info = await ctx.reply(f"You sent: {ctx.text}")
80
+ print("Sent message ID:", message_info.message_id)
81
+
82
+ async def log_event(ctx: Context):
83
+ if ctx.is_text:
84
+ print("Chat ID:", ctx.chat_id)
85
+ print("Message:", ctx.text)
86
+
87
+ async def main():
88
+ # Convert the standard bot into a webhook-enabled bot
89
+ # (All previously available endpoint methods remain usable)
90
+ webhook_bot = await normal_bot.configure_webhook("https://your-domain.com/webhook")
91
+
92
+ # Retrieve the auto-generated secret token.
93
+ # Use this to validate the X-Bot-Api-Secret-Token header in incoming requests.
94
+ secret_token = webhook_bot.get_secret_token()
95
+
96
+ # Register handlers
97
+ webhook_bot.add_webhook_handler(echo)
98
+ webhook_bot.add_webhook_handler(log_event)
99
+
100
+ # Note: The request handling flow below should be implemented
101
+ # inside your web framework's endpoint (e.g., FastAPI, Flask).
102
+ ~~~
103
+
104
+ #### Request Handling Flow (For Web Frameworks)
105
+
106
+ When implementing the actual route in your web framework, follow these steps to process incoming events:
107
+
108
+ 1. **Parse the payload:** Parse the incoming JSON body into a `ZaloAPIResponse[Event]`. The actual payload resides in the `result` field.
109
+ ~~~python
110
+ parsed: ZaloAPIResponse[Event] = ZaloAPIResponse.model_validate(request.json())
111
+ ~~~
112
+
113
+ 2. **Extract the event:**
114
+ ~~~python
115
+ event = parsed.result
116
+ ~~~
117
+
118
+ 3. **Dispatch the event:** Pass the event to your registered handlers.
119
+ ~~~python
120
+ await webhook_bot.dispatch_webhook_handlers(event)
121
+ ~~~
122
+
123
+ ## Architecture Note: Webhook State Management
124
+
125
+ To ensure strict type safety, the `ZaloBot` class is a generic that relies on two specific states:
126
+
127
+ | State | Description |
128
+ |-------|-------------|
129
+ | `UnconfiguredWebhook` | The default state when `ZaloBot` is instantiated. |
130
+ | `ConfiguredWebhook` | The state returned after calling `.configure_webhook()`. |
131
+
132
+ **Why does this matter?**
133
+ All helper methods written in `snake_case` (such as `add_webhook_handler`) are specific to webhook operations. Because of the generic state design, static analyzers (like Pyright or MyPy) will flag an error if you attempt to call a `snake_case` method on an unconfigured bot (`ZaloBot[UnconfiguredWebhook]`). These methods are only exposed and valid on instances of `ZaloBot[ConfiguredWebhook]`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "zalobot-python"
3
- version = "0.1.3"
3
+ version = "0.2.0"
4
4
  description = "Python SDK for the Zalo Bot API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,5 +1,5 @@
1
- from .zalobot import ZaloBot, WebhookHandler
2
- from .models import (
1
+ from .zalobot import ZaloBot, Context, AsyncWebhookHandler
2
+ from .types import (
3
3
  SuccessfulResponse,
4
4
  ErrorResponse,
5
5
  ZaloAPIResponse,
@@ -19,7 +19,8 @@ __all__ = [
19
19
  "ErrorResponse",
20
20
  "ZaloAPIResponse",
21
21
  "ZaloBot",
22
- "WebhookHandler",
22
+ "Context",
23
+ "AsyncWebhookHandler",
23
24
  "BotInfo",
24
25
  "WebhookInfo",
25
26
  "MessageInfo",
@@ -0,0 +1,33 @@
1
+ from .responses import (
2
+ ZaloAPIResponse,
3
+ SuccessfulResponse,
4
+ ErrorResponse,
5
+ Result
6
+ )
7
+ from .entities import (
8
+ BotInfo,
9
+ MessageInfo,
10
+ WebhookInfo,
11
+ EventName,
12
+ From,
13
+ Chat,
14
+ Message,
15
+ Event
16
+ )
17
+ from .errors import ZaloAPIError
18
+
19
+ __all__ = [
20
+ "SuccessfulResponse",
21
+ "ErrorResponse",
22
+ "ZaloAPIResponse",
23
+ "Result",
24
+ "BotInfo",
25
+ "WebhookInfo",
26
+ "MessageInfo",
27
+ "EventName",
28
+ "From",
29
+ "Chat",
30
+ "Message",
31
+ "Event",
32
+ "ZaloAPIError"
33
+ ]
@@ -0,0 +1,47 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel, Field
3
+
4
+ from .responses import Result
5
+
6
+ class BotInfo(Result):
7
+ id: str
8
+ account_name: str
9
+ account_type: str
10
+ can_join_groups: bool
11
+ display_name: str
12
+
13
+ class WebhookInfo(Result):
14
+ url: str
15
+ updated_at: int
16
+
17
+ class MessageInfo(Result):
18
+ message_id: str
19
+ date: int
20
+
21
+ class EventName(StrEnum):
22
+ TEXT_RECEIVED = "message.text.received"
23
+ IMAGE_RECEIVED = "message.image.received"
24
+ STICKER_RECEIVED = "message.sticker.received"
25
+ UNSUPPORTED_RECEIVED = "message.unsupported.received"
26
+
27
+ class From(BaseModel):
28
+ """Sender information"""
29
+ id: str
30
+ display_name: str
31
+ is_bot: bool
32
+
33
+ class Chat(BaseModel):
34
+ """Current chat information"""
35
+ id: str
36
+ chat_type: str
37
+
38
+ class Message(BaseModel):
39
+ sender: From = Field(alias="from")
40
+ chat: Chat
41
+ text: str
42
+ message_id: str
43
+ date: int
44
+
45
+ class Event(Result):
46
+ event_name: EventName
47
+ message: Message
@@ -0,0 +1,13 @@
1
+ from typing import override
2
+
3
+ class ZaloAPIError(BaseException):
4
+ """Exception when using the Zalo API"""
5
+ def __init__(self, error_code: int, description: str):
6
+ self.error_code: int = error_code
7
+ self.description: str = description
8
+ super().__init__()
9
+
10
+ @override
11
+ def __str__(self):
12
+ return f"error_code: {self.error_code}; description: {self.description}"
13
+
@@ -0,0 +1,21 @@
1
+ from typing import Literal, Annotated
2
+ from pydantic import BaseModel, Field
3
+
4
+ class Result(BaseModel):
5
+ """Represents the result field in the Zalo API successful response"""
6
+
7
+ class SuccessfulResponse[T: Result](BaseModel):
8
+ """Represents the successful response of the Zalo API"""
9
+ ok: Literal[True] = True
10
+ result: T = Field(description="The result when the API call is successful.")
11
+
12
+ class ErrorResponse(BaseModel):
13
+ """Represents the error response of Zalo API"""
14
+ ok: Literal[False] = False
15
+ description: str = Field(description="The description of the error")
16
+ error_code: int = Field(description="The error code")
17
+
18
+ type ZaloAPIResponse[T: Result] = Annotated[
19
+ SuccessfulResponse[T] | ErrorResponse,
20
+ "Represents the response of the Zalo API"
21
+ ]
@@ -0,0 +1,8 @@
1
+ from .context import Context
2
+ from .core import ZaloBot, AsyncWebhookHandler
3
+
4
+ __all__ = [
5
+ "Context",
6
+ "ZaloBot",
7
+ "AsyncWebhookHandler"
8
+ ]
@@ -0,0 +1,52 @@
1
+ from typing import final
2
+ from ..types import Event, EventName, MessageInfo, From, Chat
3
+ from .interfaces import ZaloBotAPI
4
+
5
+ @final
6
+ class Context:
7
+ def __init__(self, *, _update: Event, _bot: ZaloBotAPI):
8
+ self._update = _update
9
+ self._bot = _bot
10
+
11
+ async def reply(self, text: str) -> MessageInfo:
12
+ return await self._bot.sendMessage(self.chat_id, text)
13
+
14
+ @property
15
+ def chat_id(self) -> str:
16
+ return self._update.message.chat.id
17
+
18
+ @property
19
+ def user_id(self) -> str:
20
+ return self._update.message.sender.id
21
+
22
+ @property
23
+ def text(self) -> str:
24
+ return self._update.message.text
25
+
26
+ @property
27
+ def message_id(self) -> str:
28
+ return self._update.message.message_id
29
+
30
+ @property
31
+ def sender(self) -> From:
32
+ return self._update.message.sender
33
+
34
+ @property
35
+ def chat(self) -> Chat:
36
+ return self._update.message.chat
37
+
38
+ @property
39
+ def is_text(self) -> bool:
40
+ return self._update.event_name == EventName.TEXT_RECEIVED
41
+
42
+ @property
43
+ def is_image(self) -> bool:
44
+ return self._update.event_name == EventName.IMAGE_RECEIVED
45
+
46
+ @property
47
+ def is_sticker(self) -> bool:
48
+ return self._update.event_name == EventName.STICKER_RECEIVED
49
+
50
+ @property
51
+ def is_unsupported(self) -> bool:
52
+ return self._update.event_name == EventName.UNSUPPORTED_RECEIVED
@@ -1,9 +1,8 @@
1
- from typing import Any, Literal, Protocol
2
- import httpx
1
+ import secrets
2
+ from typing import Annotated, Protocol
3
3
 
4
- from .models import (
4
+ from ..types import (
5
5
  ZaloAPIResponse,
6
- SuccessfulResponse,
7
6
  ErrorResponse,
8
7
  ZaloAPIError,
9
8
  BotInfo,
@@ -11,50 +10,46 @@ from .models import (
11
10
  MessageInfo,
12
11
  Event
13
12
  )
14
- from .config import ZaloAPIConfig
15
-
16
- async def fetch[T](
17
- url: str,
18
- *,
19
- method: Literal["GET", "POST"] = "GET",
20
- body: dict[str, Any] | None = None,
21
- ) -> ZaloAPIResponse[T]:
22
- async with httpx.AsyncClient() as client:
23
-
24
- if method == "GET":
25
- response = await client.get(url)
26
-
27
- elif method == "POST":
28
- response = await client.post(url, json=body)
29
-
30
- response_json = response.json()
31
-
32
- if response_json.get("ok"):
33
- return SuccessfulResponse(**response_json)
34
-
35
- return ErrorResponse(**response_json)
13
+ from ..config import ZaloAPIConfig
14
+ from .http import fetch
15
+ from .context import Context
16
+
17
+ class UnconfiguredWebhook:
18
+ """Represents the state of the bot where webhook have not yet configured"""
19
+ class ConfiguredWebhook:
20
+ """Represents the state of the bot where webhook is configured"""
21
+ type BotStates = Annotated[
22
+ UnconfiguredWebhook | ConfiguredWebhook,
23
+ "Represents the states of the bot"
24
+ ]
36
25
 
37
26
  class AsyncWebhookHandler(Protocol):
38
- async def __call__(self, update_event: Event, bot: ZaloBot) -> None: ...
27
+ async def __call__(self, ctx: Context) -> None: ...
39
28
 
40
- class ZaloBot:
41
- def __init__(self, BOT_TOKEN: str):
29
+ class ZaloBot[S: BotStates = UnconfiguredWebhook]:
30
+ def __init__(
31
+ self,
32
+ BOT_TOKEN: str,
33
+ *,
34
+ _secret_token: str | None = None
35
+ ):
42
36
  self._BOT_TOKEN: str = BOT_TOKEN
43
37
  self._base_url: str = f"{ZaloAPIConfig.BASE_URL}/bot{BOT_TOKEN}"
44
38
  self._webhook_handlers: list[AsyncWebhookHandler] = []
39
+ if _secret_token is not None:
40
+ self._secret_token: str = _secret_token
45
41
 
46
42
  async def getMe(self) -> BotInfo:
47
43
  url = f"{self._base_url}/getMe"
48
44
 
49
- res: ZaloAPIResponse[BotInfo] = await fetch(url)
50
-
45
+ res = await fetch(url, result_schema=BotInfo)
46
+
51
47
  if isinstance(res, ErrorResponse):
52
48
  raise ZaloAPIError(
53
49
  res.error_code,
54
50
  res.description
55
51
  )
56
52
 
57
-
58
53
  return res.result
59
54
 
60
55
  async def getUpdates(self, timeout: int = 30) -> Event:
@@ -64,7 +59,7 @@ class ZaloBot:
64
59
  "timeout": timeout
65
60
  }
66
61
 
67
- res: ZaloAPIResponse[Event] = await fetch(url, method="POST", body=payload)
62
+ res = await fetch(url, result_schema=Event, method="POST", body=payload, timeout=timeout)
68
63
 
69
64
  if isinstance(res, ErrorResponse):
70
65
  raise ZaloAPIError(
@@ -75,7 +70,7 @@ class ZaloBot:
75
70
  return res.result
76
71
 
77
72
  async def setWebhook(
78
- self,
73
+ self: ZaloBot[UnconfiguredWebhook],
79
74
  url: str,
80
75
  secret_token: str,
81
76
  ) -> WebhookInfo:
@@ -86,7 +81,7 @@ class ZaloBot:
86
81
  "secret_token": secret_token
87
82
  }
88
83
 
89
- res: ZaloAPIResponse[WebhookInfo] = await fetch(url, method="POST", body=payload)
84
+ res = await fetch(url, result_schema=WebhookInfo, method="POST", body=payload)
90
85
 
91
86
  if isinstance(res, ErrorResponse):
92
87
  raise ZaloAPIError(
@@ -99,7 +94,7 @@ class ZaloBot:
99
94
  async def deleteWebhook(self) -> WebhookInfo:
100
95
  url = f"{self._base_url}/deleteWebhook"
101
96
 
102
- res: ZaloAPIResponse[WebhookInfo] = await fetch(url)
97
+ res = await fetch(url, result_schema=WebhookInfo)
103
98
 
104
99
  if isinstance(res, ErrorResponse):
105
100
  raise ZaloAPIError(
@@ -112,7 +107,7 @@ class ZaloBot:
112
107
  async def getWebhookInfo(self) -> WebhookInfo:
113
108
  url = f"{self._base_url}/getWebhookInfo"
114
109
 
115
- res: ZaloAPIResponse[WebhookInfo] = await fetch(url)
110
+ res = await fetch(url, result_schema=WebhookInfo)
116
111
 
117
112
  if isinstance(res, ErrorResponse):
118
113
  raise ZaloAPIError(
@@ -122,15 +117,6 @@ class ZaloBot:
122
117
 
123
118
  return res.result
124
119
 
125
- def on_webhook_update(self, handler: AsyncWebhookHandler) -> None:
126
- """Registers a handler to handle on webhook event update"""
127
- self._webhook_handlers.append(handler)
128
-
129
- async def dispatch_webhook_handlers(self, update_event: Event) -> None:
130
- """Run all handlers given a webhook event"""
131
- for handler in self._webhook_handlers:
132
- await handler(update_event, self)
133
-
134
120
  async def sendMessage(self, chat_id: str, text: str) -> MessageInfo:
135
121
  url = f"{self._base_url}/sendMessage"
136
122
 
@@ -138,7 +124,7 @@ class ZaloBot:
138
124
  "chat_id": chat_id,
139
125
  "text": text
140
126
  }
141
- res: ZaloAPIResponse[MessageInfo] = await fetch(url, method="POST", body=payload)
127
+ res = await fetch(url, result_schema=MessageInfo, method="POST", body=payload)
142
128
 
143
129
  if isinstance(res, ErrorResponse):
144
130
  raise ZaloAPIError(
@@ -157,3 +143,31 @@ class ZaloBot:
157
143
 
158
144
  async def sendChatAction(self, chat_id: str, action: str) -> None:
159
145
  raise NotImplementedError()
146
+
147
+ async def configure_webhook(
148
+ self: ZaloBot[UnconfiguredWebhook],
149
+ url: str
150
+ ) -> ZaloBot[ConfiguredWebhook]:
151
+
152
+ secret_token = secrets.token_urlsafe(192)
153
+ _ = await self.setWebhook(url, secret_token)
154
+
155
+ return ZaloBot(self._BOT_TOKEN, _secret_token = secret_token)
156
+
157
+ def get_secret_token(self: ZaloBot[ConfiguredWebhook]):
158
+ return self._secret_token
159
+
160
+ def add_webhook_handler(
161
+ self: ZaloBot[ConfiguredWebhook],
162
+ handler: AsyncWebhookHandler
163
+ ) -> None:
164
+ """Registers a handler to handle on webhook event update"""
165
+ self._webhook_handlers.append(handler)
166
+
167
+ async def dispatch_webhook_handlers(
168
+ self: ZaloBot[ConfiguredWebhook],
169
+ update_event: Event
170
+ ) -> None:
171
+ """Run all handlers given a webhook event"""
172
+ for handler in self._webhook_handlers:
173
+ await handler(Context(_update=update_event, _bot=self))
@@ -0,0 +1,36 @@
1
+ from typing import Literal, Any
2
+
3
+ import httpx
4
+
5
+ from ..types import ZaloAPIResponse, Result, SuccessfulResponse, ErrorResponse
6
+
7
+ async def fetch[T: Result](
8
+ url: str,
9
+ *,
10
+ result_schema: type[T],
11
+ method: Literal["GET", "POST"] = "GET",
12
+ body: dict[str, Any] | None = None,
13
+ timeout: int = 30,
14
+ ) -> ZaloAPIResponse[T]:
15
+ """Ultility function to fetch the data from the Zalo API asynchronously"""
16
+ async with httpx.AsyncClient(timeout=timeout) as client:
17
+
18
+ if method == "GET":
19
+ response = await client.get(url)
20
+
21
+ elif method == "POST":
22
+ response = await client.post(url, json=body)
23
+
24
+ response_json = response.json()
25
+
26
+ if response_json.get("ok"):
27
+ return SuccessfulResponse(
28
+ ok=True,
29
+ result=result_schema.model_validate(response_json.get("result"))
30
+ )
31
+
32
+ return ErrorResponse(
33
+ ok=False,
34
+ description=response_json.get("description"),
35
+ error_code=response_json.get("error_code")
36
+ )
@@ -0,0 +1,8 @@
1
+ from typing import Protocol
2
+ from ..types import MessageInfo
3
+
4
+ class ZaloBotAPI(Protocol):
5
+ async def sendMessage(self, chat_id: str, text: str) -> MessageInfo: ...
6
+ async def sendPhoto(self, chat_id: str, caption: str, photo_url: str) -> None: ...
7
+ async def sendSticker(self, chat_id: str, sticker: str) -> None: ...
8
+ async def sendChatAction(self, chat_id: str, action: str) -> None: ...
@@ -1,39 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: zalobot-python
3
- Version: 0.1.3
4
- Summary: Python SDK for the Zalo Bot API
5
- Author: NovaH00
6
- Author-email: NovaH00 <trantay2006super@gmail.com>
7
- Requires-Dist: httpx>=0.28.1
8
- Requires-Dist: pydantic>=2.12.5
9
- Requires-Dist: requests>=2.32.5
10
- Requires-Python: >=3.14
11
- Description-Content-Type: text/markdown
12
-
13
- # WIP: Python SDK for the ZaloBot API
14
-
15
- ## Installation
16
- For uv-astral
17
- ```bash
18
- uv add zalobot_python
19
- ```
20
- For pip
21
- ```bash
22
- pip install zalobot_python
23
- ```
24
-
25
- Usage
26
- ```python
27
- import asyncio
28
- from zalobot_python import ZaloBot, BotInfo
29
-
30
- bot = ZaloBot(BOT_TOKEN="<BOT TOKEN>")
31
-
32
- async def main():
33
- bot_info: BotInfo = await bot.getMe()
34
-
35
- print(f"Bot ID: {bot_info.id}")
36
- print(f"Bot Name: {bot_info.display_name}")
37
-
38
- asyncio.run(main())
39
- ```
@@ -1,27 +0,0 @@
1
- # WIP: Python SDK for the ZaloBot API
2
-
3
- ## Installation
4
- For uv-astral
5
- ```bash
6
- uv add zalobot_python
7
- ```
8
- For pip
9
- ```bash
10
- pip install zalobot_python
11
- ```
12
-
13
- Usage
14
- ```python
15
- import asyncio
16
- from zalobot_python import ZaloBot, BotInfo
17
-
18
- bot = ZaloBot(BOT_TOKEN="<BOT TOKEN>")
19
-
20
- async def main():
21
- bot_info: BotInfo = await bot.getMe()
22
-
23
- print(f"Bot ID: {bot_info.id}")
24
- print(f"Bot Name: {bot_info.display_name}")
25
-
26
- asyncio.run(main())
27
- ```
@@ -1,78 +0,0 @@
1
- from enum import StrEnum
2
- from typing import Literal, TypeVar, override, Annotated
3
- from pydantic import BaseModel, Field
4
-
5
- class Result(BaseModel):
6
- """Represents the result field in the Zalo API successful response"""
7
- pass
8
-
9
- T = TypeVar("T", bound=Result)
10
- class SuccessfulResponse[T](BaseModel):
11
- """Represents the successful response of the Zalo API"""
12
- ok: Literal[True] = True
13
- result: T = Field(description="The result when the API call is successful.")
14
-
15
- class ErrorResponse(BaseModel):
16
- """Represents the error response of Zalo API"""
17
- ok: Literal[False] = False
18
- description: str = Field(description="The description of the error")
19
- error_code: int = Field(description="The error code")
20
-
21
- type ZaloAPIResponse[T] = Annotated[
22
- SuccessfulResponse[T] | ErrorResponse,
23
- "Represents the response of the Zalo API"
24
- ]
25
-
26
- class ZaloAPIError(BaseException):
27
- """Exception when using the Zalo API"""
28
- def __init__(self, error_code: int, description: str):
29
- self.error_code: int = error_code
30
- self.description: str = description
31
- super().__init__()
32
-
33
- @override
34
- def __str__(self):
35
- return f"error_code: {self.error_code}; description: {self.description}"
36
-
37
- class BotInfo(Result):
38
- id: str
39
- account_name: str
40
- account_type: str
41
- can_join_groups: bool
42
- display_name: str
43
-
44
- class WebhookInfo(Result):
45
- url: str
46
- updated_at: int
47
-
48
- class MessageInfo(Result):
49
- message_id: str
50
- date: int
51
-
52
- class EventName(StrEnum):
53
- TEXT_RECEIVED = "message.text.received"
54
- IMAGE_RECEIVED = "message.image.received"
55
- STICKER_RECEIVED = "message.sticker.received"
56
- UNSUPPORTED_RECEIVED = "message.unsupported.received"
57
-
58
- class From(BaseModel):
59
- """Sender information"""
60
- id: str
61
- display_name: str
62
- is_bot: bool
63
-
64
- class Chat(BaseModel):
65
- """Current chat information"""
66
- id: str
67
- chat_type: str
68
-
69
- class Message(BaseModel):
70
- sender: From = Field(alias="from")
71
- chat: Chat
72
- text: str
73
- message_id: str
74
- date: int
75
-
76
- class Event(BaseModel):
77
- event_name: EventName
78
- message: Message