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.
- zalobot_python-0.2.0/PKG-INFO +145 -0
- zalobot_python-0.2.0/README.md +133 -0
- {zalobot_python-0.1.3 → zalobot_python-0.2.0}/pyproject.toml +1 -1
- {zalobot_python-0.1.3 → zalobot_python-0.2.0}/src/zalobot_python/__init__.py +4 -3
- zalobot_python-0.2.0/src/zalobot_python/types/__init__.py +33 -0
- zalobot_python-0.2.0/src/zalobot_python/types/entities.py +47 -0
- zalobot_python-0.2.0/src/zalobot_python/types/errors.py +13 -0
- zalobot_python-0.2.0/src/zalobot_python/types/responses.py +21 -0
- zalobot_python-0.2.0/src/zalobot_python/zalobot/__init__.py +8 -0
- zalobot_python-0.2.0/src/zalobot_python/zalobot/context.py +52 -0
- zalobot_python-0.1.3/src/zalobot_python/zalobot.py → zalobot_python-0.2.0/src/zalobot_python/zalobot/core.py +61 -47
- zalobot_python-0.2.0/src/zalobot_python/zalobot/http.py +36 -0
- zalobot_python-0.2.0/src/zalobot_python/zalobot/interfaces.py +8 -0
- zalobot_python-0.1.3/PKG-INFO +0 -39
- zalobot_python-0.1.3/README.md +0 -27
- zalobot_python-0.1.3/src/zalobot_python/models.py +0 -78
- {zalobot_python-0.1.3 → zalobot_python-0.2.0}/src/zalobot_python/config.py +0 -0
- {zalobot_python-0.1.3 → zalobot_python-0.2.0}/src/zalobot_python/py.typed +0 -0
|
@@ -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,5 +1,5 @@
|
|
|
1
|
-
from .zalobot import ZaloBot,
|
|
2
|
-
from .
|
|
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
|
-
"
|
|
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,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
|
-
|
|
2
|
-
import
|
|
1
|
+
import secrets
|
|
2
|
+
from typing import Annotated, Protocol
|
|
3
3
|
|
|
4
|
-
from
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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,
|
|
27
|
+
async def __call__(self, ctx: Context) -> None: ...
|
|
39
28
|
|
|
40
|
-
class ZaloBot:
|
|
41
|
-
def __init__(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: ...
|
zalobot_python-0.1.3/PKG-INFO
DELETED
|
@@ -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
|
-
```
|
zalobot_python-0.1.3/README.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|