botiksdk 0.1.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.
- botiksdk-0.1.0/PKG-INFO +40 -0
- botiksdk-0.1.0/README.md +25 -0
- botiksdk-0.1.0/__init__.py +20 -0
- botiksdk-0.1.0/bot.py +57 -0
- botiksdk-0.1.0/bot_types.py +87 -0
- botiksdk-0.1.0/botiksdk.egg-info/PKG-INFO +40 -0
- botiksdk-0.1.0/botiksdk.egg-info/SOURCES.txt +27 -0
- botiksdk-0.1.0/botiksdk.egg-info/dependency_links.txt +1 -0
- botiksdk-0.1.0/botiksdk.egg-info/requires.txt +1 -0
- botiksdk-0.1.0/botiksdk.egg-info/top_level.txt +1 -0
- botiksdk-0.1.0/client.py +160 -0
- botiksdk-0.1.0/dispatcher.py +89 -0
- botiksdk-0.1.0/exceptions.py +21 -0
- botiksdk-0.1.0/filters.py +101 -0
- botiksdk-0.1.0/pyproject.toml +32 -0
- botiksdk-0.1.0/router.py +30 -0
- botiksdk-0.1.0/setup.cfg +4 -0
- botiksdk-0.1.0/setup.py +3 -0
- botiksdk-0.1.0/test_bot.py +65 -0
botiksdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: botiksdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vondic Botik SDK
|
|
5
|
+
Author-email: Vondic <support@vondic.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vondic/botiksdk
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/vondic/botiksdk/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.32.5
|
|
15
|
+
|
|
16
|
+
# Vondic Botik SDK
|
|
17
|
+
|
|
18
|
+
A Python SDK for building bots on the Vondic platform.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install botiksdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from botiksdk import Bot, Dispatcher, Message
|
|
30
|
+
|
|
31
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
32
|
+
dp = Dispatcher(bot)
|
|
33
|
+
|
|
34
|
+
@dp.message_handler()
|
|
35
|
+
async def echo(message: Message):
|
|
36
|
+
await message.answer(message.text)
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
dp.start_polling()
|
|
40
|
+
```
|
botiksdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Vondic Botik SDK
|
|
2
|
+
|
|
3
|
+
A Python SDK for building bots on the Vondic platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install botiksdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from botiksdk import Bot, Dispatcher, Message
|
|
15
|
+
|
|
16
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
17
|
+
dp = Dispatcher(bot)
|
|
18
|
+
|
|
19
|
+
@dp.message_handler()
|
|
20
|
+
async def echo(message: Message):
|
|
21
|
+
await message.answer(message.text)
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
dp.start_polling()
|
|
25
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from botiksdk.bot import Bot
|
|
2
|
+
from botiksdk.bot_types import Chat, Message, Update, User
|
|
3
|
+
from botiksdk.client import PublicAPIClient
|
|
4
|
+
from botiksdk.dispatcher import Dispatcher
|
|
5
|
+
from botiksdk.filters import Command, F, Text
|
|
6
|
+
from botiksdk.router import Router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Bot",
|
|
10
|
+
"PublicAPIClient",
|
|
11
|
+
"Dispatcher",
|
|
12
|
+
"Router",
|
|
13
|
+
"Command",
|
|
14
|
+
"Text",
|
|
15
|
+
"F",
|
|
16
|
+
"Update",
|
|
17
|
+
"Message",
|
|
18
|
+
"User",
|
|
19
|
+
"Chat",
|
|
20
|
+
]
|
botiksdk-0.1.0/bot.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from botiksdk.client import PublicAPIClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Bot:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
bot_id: Optional[str] = None,
|
|
11
|
+
token: Optional[str] = None,
|
|
12
|
+
*,
|
|
13
|
+
base_url: str = "http://localhost:5050",
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
):
|
|
16
|
+
self.bot_id = bot_id
|
|
17
|
+
self.token = token
|
|
18
|
+
self.api_key = api_key
|
|
19
|
+
self.public = PublicAPIClient(base_url=base_url)
|
|
20
|
+
|
|
21
|
+
def set_token(self, token: str):
|
|
22
|
+
self.token = token
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def set_bot_id(self, bot_id: str):
|
|
26
|
+
self.bot_id = bot_id
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def set_api_key(self, api_key: str):
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def _ensure_ready(self):
|
|
34
|
+
if not self.bot_id:
|
|
35
|
+
raise ValueError("bot_id is required")
|
|
36
|
+
if not self.token:
|
|
37
|
+
raise ValueError("bot token is required")
|
|
38
|
+
|
|
39
|
+
def get_updates(self, *, offset: int = 0, limit: int = 100, timeout: int = 20):
|
|
40
|
+
self._ensure_ready()
|
|
41
|
+
return self.public.get_updates(
|
|
42
|
+
self.bot_id,
|
|
43
|
+
self.token,
|
|
44
|
+
offset=offset,
|
|
45
|
+
limit=limit,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def send_message(self, chat_id: str, text: str):
|
|
50
|
+
self._ensure_ready()
|
|
51
|
+
logging.getLogger(__name__).info(
|
|
52
|
+
"botiksdk_send_message bot_id=%s chat_id=%s text=%s",
|
|
53
|
+
self.bot_id,
|
|
54
|
+
chat_id,
|
|
55
|
+
text,
|
|
56
|
+
)
|
|
57
|
+
return self.public.send_message(self.bot_id, self.token, chat_id, text)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class User:
|
|
8
|
+
id: str
|
|
9
|
+
username: Optional[str] = None
|
|
10
|
+
avatar_url: Optional[str] = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
14
|
+
if data is None:
|
|
15
|
+
return None
|
|
16
|
+
return cls(
|
|
17
|
+
id=str(data.get("id") or ""),
|
|
18
|
+
username=data.get("username"),
|
|
19
|
+
avatar_url=data.get("avatar_url"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Chat:
|
|
25
|
+
id: str
|
|
26
|
+
type: str = "private"
|
|
27
|
+
title: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
31
|
+
if data is None:
|
|
32
|
+
return None
|
|
33
|
+
return cls(
|
|
34
|
+
id=str(data.get("id") or ""),
|
|
35
|
+
type=data.get("type") or "private",
|
|
36
|
+
title=data.get("title"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Message:
|
|
42
|
+
message_id: str
|
|
43
|
+
text: Optional[str]
|
|
44
|
+
from_user: Optional[User]
|
|
45
|
+
chat: Optional[Chat]
|
|
46
|
+
date: Optional[datetime]
|
|
47
|
+
raw: Dict[str, Any]
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
51
|
+
if data is None:
|
|
52
|
+
return None
|
|
53
|
+
ts = data.get("date")
|
|
54
|
+
if isinstance(ts, (int, float)):
|
|
55
|
+
date_value = datetime.fromtimestamp(ts)
|
|
56
|
+
elif isinstance(ts, str):
|
|
57
|
+
try:
|
|
58
|
+
date_value = datetime.fromisoformat(ts)
|
|
59
|
+
except ValueError:
|
|
60
|
+
date_value = None
|
|
61
|
+
else:
|
|
62
|
+
date_value = None
|
|
63
|
+
return cls(
|
|
64
|
+
message_id=str(data.get("message_id") or data.get("id") or ""),
|
|
65
|
+
text=data.get("text"),
|
|
66
|
+
from_user=User.from_dict(data.get("from_user")),
|
|
67
|
+
chat=Chat.from_dict(data.get("chat")),
|
|
68
|
+
date=date_value,
|
|
69
|
+
raw=data,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class Update:
|
|
75
|
+
update_id: str
|
|
76
|
+
message: Optional[Message]
|
|
77
|
+
raw: Dict[str, Any]
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
81
|
+
if data is None:
|
|
82
|
+
return None
|
|
83
|
+
return cls(
|
|
84
|
+
update_id=str(data.get("update_id") or data.get("id") or ""),
|
|
85
|
+
message=Message.from_dict(data.get("message")),
|
|
86
|
+
raw=data,
|
|
87
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: botiksdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vondic Botik SDK
|
|
5
|
+
Author-email: Vondic <support@vondic.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vondic/botiksdk
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/vondic/botiksdk/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.32.5
|
|
15
|
+
|
|
16
|
+
# Vondic Botik SDK
|
|
17
|
+
|
|
18
|
+
A Python SDK for building bots on the Vondic platform.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install botiksdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from botiksdk import Bot, Dispatcher, Message
|
|
30
|
+
|
|
31
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
32
|
+
dp = Dispatcher(bot)
|
|
33
|
+
|
|
34
|
+
@dp.message_handler()
|
|
35
|
+
async def echo(message: Message):
|
|
36
|
+
await message.answer(message.text)
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
dp.start_polling()
|
|
40
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
__init__.py
|
|
3
|
+
bot.py
|
|
4
|
+
bot_types.py
|
|
5
|
+
client.py
|
|
6
|
+
dispatcher.py
|
|
7
|
+
exceptions.py
|
|
8
|
+
filters.py
|
|
9
|
+
pyproject.toml
|
|
10
|
+
router.py
|
|
11
|
+
setup.py
|
|
12
|
+
test_bot.py
|
|
13
|
+
./__init__.py
|
|
14
|
+
./bot.py
|
|
15
|
+
./bot_types.py
|
|
16
|
+
./client.py
|
|
17
|
+
./dispatcher.py
|
|
18
|
+
./exceptions.py
|
|
19
|
+
./filters.py
|
|
20
|
+
./router.py
|
|
21
|
+
./setup.py
|
|
22
|
+
./test_bot.py
|
|
23
|
+
botiksdk.egg-info/PKG-INFO
|
|
24
|
+
botiksdk.egg-info/SOURCES.txt
|
|
25
|
+
botiksdk.egg-info/dependency_links.txt
|
|
26
|
+
botiksdk.egg-info/requires.txt
|
|
27
|
+
botiksdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.32.5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
botiksdk
|
botiksdk-0.1.0/client.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from botiksdk.exceptions import (
|
|
8
|
+
APIError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
UnauthorizedError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PublicAPIClient:
|
|
16
|
+
def __init__(self, base_url: str = "http://localhost:5050"):
|
|
17
|
+
normalized = (base_url or "http://localhost:5050").strip()
|
|
18
|
+
if "://" not in normalized:
|
|
19
|
+
normalized = f"http://{normalized}"
|
|
20
|
+
self.base_url = normalized.rstrip("/")
|
|
21
|
+
|
|
22
|
+
def _request(
|
|
23
|
+
self,
|
|
24
|
+
method: str,
|
|
25
|
+
path: str,
|
|
26
|
+
*,
|
|
27
|
+
access_token: Optional[str] = None,
|
|
28
|
+
api_key: Optional[str] = None,
|
|
29
|
+
bot_token: Optional[str] = None,
|
|
30
|
+
params: Optional[Dict[str, Any]] = None,
|
|
31
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
32
|
+
):
|
|
33
|
+
url = f"{self.base_url}{path}"
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
headers: Dict[str, str] = {"Content-Type": "application/json"}
|
|
36
|
+
if access_token:
|
|
37
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
38
|
+
if api_key:
|
|
39
|
+
headers["X-API-Key"] = api_key
|
|
40
|
+
if bot_token:
|
|
41
|
+
headers["X-Bot-Token"] = bot_token
|
|
42
|
+
try:
|
|
43
|
+
response = requests.request(
|
|
44
|
+
method,
|
|
45
|
+
url,
|
|
46
|
+
headers=headers,
|
|
47
|
+
params=params,
|
|
48
|
+
json=json_body,
|
|
49
|
+
timeout=30,
|
|
50
|
+
)
|
|
51
|
+
except requests.RequestException:
|
|
52
|
+
logger.exception(
|
|
53
|
+
"botiksdk_request_error method=%s url=%s params=%s",
|
|
54
|
+
method,
|
|
55
|
+
url,
|
|
56
|
+
params,
|
|
57
|
+
)
|
|
58
|
+
raise
|
|
59
|
+
if response.ok:
|
|
60
|
+
if not response.text:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return response.json()
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
return response.text
|
|
66
|
+
payload = None
|
|
67
|
+
try:
|
|
68
|
+
payload = response.json()
|
|
69
|
+
except json.JSONDecodeError:
|
|
70
|
+
payload = response.text
|
|
71
|
+
if response.status_code >= 400:
|
|
72
|
+
text_preview = str(payload)
|
|
73
|
+
if len(text_preview) > 500:
|
|
74
|
+
text_preview = f"{text_preview[:500]}..."
|
|
75
|
+
logger.info(
|
|
76
|
+
"botiksdk_request_failed method=%s url=%s status=%s body=%s",
|
|
77
|
+
method,
|
|
78
|
+
url,
|
|
79
|
+
response.status_code,
|
|
80
|
+
text_preview,
|
|
81
|
+
)
|
|
82
|
+
if response.status_code == 401:
|
|
83
|
+
raise UnauthorizedError(
|
|
84
|
+
response.status_code, "Unauthorized", payload)
|
|
85
|
+
if response.status_code == 404:
|
|
86
|
+
raise NotFoundError(response.status_code, "Not Found", payload)
|
|
87
|
+
if response.status_code == 400:
|
|
88
|
+
raise BadRequestError(response.status_code, "Bad Request", payload)
|
|
89
|
+
raise APIError(response.status_code, "API Error", payload)
|
|
90
|
+
|
|
91
|
+
def list_bots(self):
|
|
92
|
+
return self._request("GET", "/api/public/v1/bots")
|
|
93
|
+
|
|
94
|
+
def get_bot(self, bot_id: str):
|
|
95
|
+
return self._request("GET", f"/api/public/v1/bots/{bot_id}")
|
|
96
|
+
|
|
97
|
+
def get_bot_by_name(self, name: str):
|
|
98
|
+
return self._request("GET", f"/api/public/v1/bots/by-name/{name}")
|
|
99
|
+
|
|
100
|
+
def search_bots(self, query: str):
|
|
101
|
+
return self._request("GET", "/api/public/v1/bots/search", params={"q": query})
|
|
102
|
+
|
|
103
|
+
def generate_bot_token(self, bot_id: str, api_key: str):
|
|
104
|
+
return self._request(
|
|
105
|
+
"POST",
|
|
106
|
+
f"/api/public/v1/bots/{bot_id}/token",
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def get_updates(
|
|
111
|
+
self,
|
|
112
|
+
bot_id: str,
|
|
113
|
+
bot_token: str,
|
|
114
|
+
*,
|
|
115
|
+
offset: int = 0,
|
|
116
|
+
limit: int = 100,
|
|
117
|
+
timeout: int = 20,
|
|
118
|
+
):
|
|
119
|
+
data = self._request(
|
|
120
|
+
"GET",
|
|
121
|
+
f"/api/public/v1/bots/{bot_id}/updates",
|
|
122
|
+
params={"offset": offset, "limit": limit, "timeout": timeout},
|
|
123
|
+
bot_token=bot_token,
|
|
124
|
+
)
|
|
125
|
+
if isinstance(data, dict) and "items" in data:
|
|
126
|
+
return data["items"]
|
|
127
|
+
if data is None:
|
|
128
|
+
return []
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
def push_update(self, bot_id: str, bot_token: str, message: Dict[str, Any]):
|
|
132
|
+
return self._request(
|
|
133
|
+
"POST",
|
|
134
|
+
f"/api/public/v1/bots/{bot_id}/updates/push",
|
|
135
|
+
json_body={"message": message},
|
|
136
|
+
bot_token=bot_token,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def send_message(self, bot_id: str, bot_token: str, chat_id: str, text: str):
|
|
140
|
+
return self._request(
|
|
141
|
+
"POST",
|
|
142
|
+
f"/api/public/v1/bots/{bot_id}/send",
|
|
143
|
+
json_body={"chat_id": chat_id, "text": text},
|
|
144
|
+
bot_token=bot_token,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get_api_key(self, access_token: str):
|
|
148
|
+
return self._request(
|
|
149
|
+
"GET",
|
|
150
|
+
"/api/public/v1/account/api-key",
|
|
151
|
+
access_token=access_token,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def generate_api_key(self, access_token: str, rotate: bool = False):
|
|
155
|
+
return self._request(
|
|
156
|
+
"POST",
|
|
157
|
+
"/api/public/v1/account/api-key",
|
|
158
|
+
access_token=access_token,
|
|
159
|
+
json_body={"rotate": rotate},
|
|
160
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
from botiksdk.bot_types import Message, Update
|
|
6
|
+
from botiksdk.filters import BaseFilter
|
|
7
|
+
from botiksdk.router import Router
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Dispatcher:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self._routers = [Router()]
|
|
13
|
+
|
|
14
|
+
def include_router(self, router: Router):
|
|
15
|
+
self._routers.append(router)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def message(self, *filters: BaseFilter):
|
|
19
|
+
return self._routers[0].message(*filters)
|
|
20
|
+
|
|
21
|
+
async def feed_update(self, bot, update: Update):
|
|
22
|
+
if update is None:
|
|
23
|
+
return
|
|
24
|
+
message = update.message
|
|
25
|
+
if message is not None:
|
|
26
|
+
logging.getLogger(__name__).info(
|
|
27
|
+
"botiksdk_update_received bot_id=%s update_id=%s text=%s",
|
|
28
|
+
getattr(bot, "bot_id", None),
|
|
29
|
+
update.update_id,
|
|
30
|
+
message.text,
|
|
31
|
+
)
|
|
32
|
+
await self._dispatch_message(bot, message)
|
|
33
|
+
|
|
34
|
+
async def _dispatch_message(self, bot, message: Message):
|
|
35
|
+
for router in self._routers:
|
|
36
|
+
for handler in router.message_handlers:
|
|
37
|
+
if await self._check_filters(handler.filters, message):
|
|
38
|
+
logging.getLogger(__name__).info(
|
|
39
|
+
"botiksdk_handler_matched bot_id=%s text=%s",
|
|
40
|
+
getattr(bot, "bot_id", None),
|
|
41
|
+
message.text,
|
|
42
|
+
)
|
|
43
|
+
await handler.callback(message, bot)
|
|
44
|
+
|
|
45
|
+
async def _check_filters(self, filters: Iterable[BaseFilter], message: Message):
|
|
46
|
+
for f in filters:
|
|
47
|
+
result = f(message)
|
|
48
|
+
if asyncio.iscoroutine(result):
|
|
49
|
+
result = await result
|
|
50
|
+
if not result:
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
async def start_polling(self, *bots):
|
|
55
|
+
tasks = []
|
|
56
|
+
for bot in bots:
|
|
57
|
+
tasks.append(asyncio.create_task(self._poll_bot(bot)))
|
|
58
|
+
if not tasks:
|
|
59
|
+
return
|
|
60
|
+
await asyncio.gather(*tasks)
|
|
61
|
+
|
|
62
|
+
async def start_webhook(self, *bots, **kwargs):
|
|
63
|
+
raise NotImplementedError(
|
|
64
|
+
"Public API does not provide webhook updates")
|
|
65
|
+
|
|
66
|
+
async def _poll_bot(self, bot):
|
|
67
|
+
offset = 0
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
updates = bot.get_updates(offset=offset, timeout=20, limit=100)
|
|
71
|
+
except Exception:
|
|
72
|
+
logging.getLogger(__name__).exception(
|
|
73
|
+
"botiksdk_poll_error bot_id=%s", getattr(
|
|
74
|
+
bot, "bot_id", None)
|
|
75
|
+
)
|
|
76
|
+
await asyncio.sleep(1)
|
|
77
|
+
continue
|
|
78
|
+
if not updates:
|
|
79
|
+
await asyncio.sleep(0.2)
|
|
80
|
+
continue
|
|
81
|
+
for raw in updates:
|
|
82
|
+
update = Update.from_dict(raw)
|
|
83
|
+
if update is None:
|
|
84
|
+
continue
|
|
85
|
+
await self.feed_update(bot, update)
|
|
86
|
+
try:
|
|
87
|
+
offset = max(offset, int(update.update_id))
|
|
88
|
+
except Exception:
|
|
89
|
+
offset = offset
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class BotikSDKError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class APIError(BotikSDKError):
|
|
6
|
+
def __init__(self, status_code, message, payload=None):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.payload = payload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnauthorizedError(APIError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotFoundError(APIError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BadRequestError(APIError):
|
|
21
|
+
pass
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Iterable, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseFilter:
|
|
6
|
+
def __call__(self, message) -> bool:
|
|
7
|
+
return True
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(BaseFilter):
|
|
11
|
+
def __init__(self, *commands: str, prefix: str = "/"):
|
|
12
|
+
self.commands = {c.lstrip(prefix).lower() for c in commands if c}
|
|
13
|
+
self.prefix = prefix
|
|
14
|
+
|
|
15
|
+
def __call__(self, message) -> bool:
|
|
16
|
+
text = getattr(message, "text", None)
|
|
17
|
+
if not text:
|
|
18
|
+
return False
|
|
19
|
+
if not text.startswith(self.prefix):
|
|
20
|
+
return False
|
|
21
|
+
cmd = text[len(self.prefix):].split()[0].lower()
|
|
22
|
+
if not self.commands:
|
|
23
|
+
return True
|
|
24
|
+
return cmd in self.commands
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Text(BaseFilter):
|
|
28
|
+
def __init__(self, equals: Optional[str] = None, contains: Optional[str] = None):
|
|
29
|
+
self.equals = equals
|
|
30
|
+
self.contains_value = contains
|
|
31
|
+
|
|
32
|
+
def __call__(self, message) -> bool:
|
|
33
|
+
text = getattr(message, "text", None)
|
|
34
|
+
if text is None:
|
|
35
|
+
return False
|
|
36
|
+
if self.equals is not None:
|
|
37
|
+
return text == self.equals
|
|
38
|
+
if self.contains_value is not None:
|
|
39
|
+
return self.contains_value in text
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Regex(BaseFilter):
|
|
44
|
+
def __init__(self, pattern: str, flags: int = 0):
|
|
45
|
+
self.pattern = re.compile(pattern, flags=flags)
|
|
46
|
+
|
|
47
|
+
def __call__(self, message) -> bool:
|
|
48
|
+
text = getattr(message, "text", None)
|
|
49
|
+
if not text:
|
|
50
|
+
return False
|
|
51
|
+
return self.pattern.search(text) is not None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FilterExpression(BaseFilter):
|
|
55
|
+
def __init__(self, getter, op: str, value: Any):
|
|
56
|
+
self.getter = getter
|
|
57
|
+
self.op = op
|
|
58
|
+
self.value = value
|
|
59
|
+
|
|
60
|
+
def __call__(self, message) -> bool:
|
|
61
|
+
current = self.getter(message)
|
|
62
|
+
if self.op == "eq":
|
|
63
|
+
return current == self.value
|
|
64
|
+
if self.op == "contains":
|
|
65
|
+
return current is not None and self.value in current
|
|
66
|
+
if self.op == "in":
|
|
67
|
+
return current in self.value
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FieldRef:
|
|
72
|
+
def __init__(self, path: Iterable[str]):
|
|
73
|
+
self.path = tuple(path)
|
|
74
|
+
|
|
75
|
+
def __getattr__(self, name: str):
|
|
76
|
+
return FieldRef(self.path + (name,))
|
|
77
|
+
|
|
78
|
+
def _get_value(self, message):
|
|
79
|
+
current = message
|
|
80
|
+
for name in self.path:
|
|
81
|
+
if current is None:
|
|
82
|
+
return None
|
|
83
|
+
current = getattr(current, name, None)
|
|
84
|
+
return current
|
|
85
|
+
|
|
86
|
+
def __eq__(self, other):
|
|
87
|
+
return FilterExpression(self._get_value, "eq", other)
|
|
88
|
+
|
|
89
|
+
def contains(self, value):
|
|
90
|
+
return FilterExpression(self._get_value, "contains", value)
|
|
91
|
+
|
|
92
|
+
def isin(self, values):
|
|
93
|
+
return FilterExpression(self._get_value, "in", values)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FieldAccessor:
|
|
97
|
+
def __getattr__(self, name: str):
|
|
98
|
+
return FieldRef((name,))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
F = FieldAccessor()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "botiksdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Vondic Botik SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Vondic", email = "support@vondic.com" },
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.32.5",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
"Homepage" = "https://github.com/vondic/botiksdk"
|
|
26
|
+
"Bug Tracker" = "https://github.com/vondic/botiksdk/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["botiksdk"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-dir]
|
|
32
|
+
botiksdk = "."
|
botiksdk-0.1.0/router.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Awaitable, Callable, List
|
|
3
|
+
|
|
4
|
+
from botiksdk.filters import BaseFilter
|
|
5
|
+
|
|
6
|
+
HandlerCallable = Callable[..., Awaitable[None]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Handler:
|
|
11
|
+
filters: List[BaseFilter]
|
|
12
|
+
callback: HandlerCallable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Router:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.message_handlers: List[Handler] = []
|
|
18
|
+
|
|
19
|
+
def message(self, *filters: BaseFilter):
|
|
20
|
+
def decorator(func: HandlerCallable):
|
|
21
|
+
self.message_handlers.append(
|
|
22
|
+
Handler(filters=list(filters), callback=func)
|
|
23
|
+
)
|
|
24
|
+
return func
|
|
25
|
+
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
def include_router(self, router: "Router"):
|
|
29
|
+
self.message_handlers.extend(router.message_handlers)
|
|
30
|
+
return self
|
botiksdk-0.1.0/setup.cfg
ADDED
botiksdk-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from botiksdk import Bot, Command, Dispatcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
level=logging.INFO,
|
|
11
|
+
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
|
12
|
+
)
|
|
13
|
+
bot_id = os.environ.get(
|
|
14
|
+
"BOTIK_BOT_ID", "eee845bc-8bf7-49da-9ec9-304832e9189b"
|
|
15
|
+
)
|
|
16
|
+
bot_token = os.environ.get(
|
|
17
|
+
"BOTIK_BOT_TOKEN", "ktybQlSZb6xX5FH3j9FoeUKa79xCTUx3Gsio9_5dW3Y"
|
|
18
|
+
)
|
|
19
|
+
base_url = os.environ.get("BOTIK_BASE_URL", "http://localhost:5050")
|
|
20
|
+
logging.getLogger(__name__).info(
|
|
21
|
+
"botiksdk_config bot_id=%s base_url=%s", bot_id, base_url
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
bot = Bot(bot_id=bot_id, token=bot_token, base_url=base_url)
|
|
25
|
+
dp = Dispatcher()
|
|
26
|
+
|
|
27
|
+
@dp.message(Command("start"))
|
|
28
|
+
async def start_handler(message, bot_instance):
|
|
29
|
+
logging.getLogger(__name__).info(
|
|
30
|
+
"handler_start chat_id=%s", message.chat.id)
|
|
31
|
+
result = await asyncio.to_thread(
|
|
32
|
+
bot_instance.send_message,
|
|
33
|
+
message.chat.id,
|
|
34
|
+
"Привет! Команды: /id /help",
|
|
35
|
+
)
|
|
36
|
+
logging.getLogger(__name__).info("handler_start_result %s", result)
|
|
37
|
+
|
|
38
|
+
@dp.message(Command("id"))
|
|
39
|
+
async def id_handler(message, bot_instance):
|
|
40
|
+
user_id = message.from_user.id if message.from_user else "unknown"
|
|
41
|
+
logging.getLogger(__name__).info(
|
|
42
|
+
"handler_id chat_id=%s", message.chat.id)
|
|
43
|
+
result = await asyncio.to_thread(
|
|
44
|
+
bot_instance.send_message,
|
|
45
|
+
message.chat.id,
|
|
46
|
+
f"Ваш id: {user_id}",
|
|
47
|
+
)
|
|
48
|
+
logging.getLogger(__name__).info("handler_id_result %s", result)
|
|
49
|
+
|
|
50
|
+
@dp.message(Command("help"))
|
|
51
|
+
async def help_handler(message, bot_instance):
|
|
52
|
+
logging.getLogger(__name__).info(
|
|
53
|
+
"handler_help chat_id=%s", message.chat.id)
|
|
54
|
+
result = await asyncio.to_thread(
|
|
55
|
+
bot_instance.send_message,
|
|
56
|
+
message.chat.id,
|
|
57
|
+
"Доступные команды: /start /id /help",
|
|
58
|
+
)
|
|
59
|
+
logging.getLogger(__name__).info("handler_help_result %s", result)
|
|
60
|
+
|
|
61
|
+
await dp.start_polling(bot)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
asyncio.run(main())
|