pykodaribot 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.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: pykodaribot
3
+ Version: 0.1.0
4
+ Summary: Python SDK для создания ботов в Kodari Messenger
5
+ License: MIT
6
+ Project-URL: Homepage, https://msg.kodari.ru
7
+ Project-URL: Documentation, https://docs.kodari.ru/
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: AsyncIO
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: aiohttp>=3.9
15
+
16
+ # pykodaribot
17
+
18
+ Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
19
+
20
+ **Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
21
+
22
+ ## Установка
23
+
24
+ ```bash
25
+ pip install pykodaribot
26
+ ```
27
+
28
+ **Требования:** Python 3.9+
29
+
30
+ ## Быстрый старт
31
+
32
+ ```python
33
+ import asyncio
34
+ from pykodaribot import Bot, Dispatcher, filters
35
+
36
+ bot = Bot("bot_ВАШ_ТОКЕН")
37
+ dp = Dispatcher()
38
+
39
+ @dp.message(filters.command("start"))
40
+ async def start(msg):
41
+ await msg.answer("Привет!")
42
+
43
+ @dp.message()
44
+ async def echo(msg):
45
+ if msg.text:
46
+ await msg.reply(msg.text)
47
+
48
+ asyncio.run(dp.start_polling(bot))
49
+ ```
50
+
51
+ ## Токен
52
+
53
+ Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
54
+
55
+ ## API
56
+
57
+ ### Bot
58
+
59
+ | Метод | Описание |
60
+ |---|---|
61
+ | `get_me()` | Информация о боте |
62
+ | `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
63
+ | `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
64
+ | `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
65
+ | `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
66
+ | `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
67
+ | `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
68
+ | `delete_message(chat_id, message_id)` | Удалить сообщение |
69
+ | `get_updates(offset, timeout, limit)` | Получить апдейты |
70
+ | `answer_callback_query(callback_query_id)` | Ответить на callback |
71
+ | `set_my_commands(commands)` | Установить команды меню |
72
+ | `get_my_commands()` | Получить команды меню |
73
+ | `set_my_name(name)` | Изменить имя бота |
74
+ | `set_my_description(description)` | Изменить описание |
75
+ | `set_my_short_description(short_description)` | Изменить краткое описание |
76
+ | `get_chat(chat_id)` | Информация о чате |
77
+
78
+ ### Dispatcher
79
+
80
+ ```python
81
+ dp = Dispatcher()
82
+
83
+ @dp.message(*filters) # новое сообщение
84
+ @dp.edited_message(*filters) # изменённое сообщение
85
+ @dp.callback_query(*filters) # нажатие inline-кнопки
86
+ ```
87
+
88
+ ### Фильтры
89
+
90
+ ```python
91
+ from pykodaribot import filters
92
+
93
+ filters.command("start", "help") # команды /start или /help
94
+ filters.text(r"привет|hello") # regex по тексту
95
+ filters.callback_data("yes") # точное совпадение callback_data
96
+ filters.callback_data_prefix("action:") # callback_data начинается с префикса
97
+ ```
98
+
99
+ ### Inline-клавиатура
100
+
101
+ ```python
102
+ from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
103
+
104
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
105
+ [
106
+ InlineKeyboardButton(text="Да", callback_data="yes"),
107
+ InlineKeyboardButton(text="Нет", callback_data="no"),
108
+ ],
109
+ [
110
+ InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
111
+ ],
112
+ ])
113
+
114
+ await msg.answer("Выберите:", reply_markup=keyboard)
115
+ ```
116
+
117
+ ### Отправка медиа
118
+
119
+ `media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
120
+
121
+ ```python
122
+ await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
123
+ await bot.send_photo(chat_id, Path("/tmp/image.png"))
124
+ await bot.send_document(chat_id, open("report.pdf", "rb"))
125
+
126
+ with open("clip.mp4", "rb") as f:
127
+ await bot.send_video(chat_id, f, caption="Видео")
128
+ ```
129
+
130
+ ## Примеры
131
+
132
+ - [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
133
+ - [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
134
+ - [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
@@ -0,0 +1,119 @@
1
+ # pykodaribot
2
+
3
+ Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
4
+
5
+ **Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
6
+
7
+ ## Установка
8
+
9
+ ```bash
10
+ pip install pykodaribot
11
+ ```
12
+
13
+ **Требования:** Python 3.9+
14
+
15
+ ## Быстрый старт
16
+
17
+ ```python
18
+ import asyncio
19
+ from pykodaribot import Bot, Dispatcher, filters
20
+
21
+ bot = Bot("bot_ВАШ_ТОКЕН")
22
+ dp = Dispatcher()
23
+
24
+ @dp.message(filters.command("start"))
25
+ async def start(msg):
26
+ await msg.answer("Привет!")
27
+
28
+ @dp.message()
29
+ async def echo(msg):
30
+ if msg.text:
31
+ await msg.reply(msg.text)
32
+
33
+ asyncio.run(dp.start_polling(bot))
34
+ ```
35
+
36
+ ## Токен
37
+
38
+ Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
39
+
40
+ ## API
41
+
42
+ ### Bot
43
+
44
+ | Метод | Описание |
45
+ |---|---|
46
+ | `get_me()` | Информация о боте |
47
+ | `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
48
+ | `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
49
+ | `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
50
+ | `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
51
+ | `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
52
+ | `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
53
+ | `delete_message(chat_id, message_id)` | Удалить сообщение |
54
+ | `get_updates(offset, timeout, limit)` | Получить апдейты |
55
+ | `answer_callback_query(callback_query_id)` | Ответить на callback |
56
+ | `set_my_commands(commands)` | Установить команды меню |
57
+ | `get_my_commands()` | Получить команды меню |
58
+ | `set_my_name(name)` | Изменить имя бота |
59
+ | `set_my_description(description)` | Изменить описание |
60
+ | `set_my_short_description(short_description)` | Изменить краткое описание |
61
+ | `get_chat(chat_id)` | Информация о чате |
62
+
63
+ ### Dispatcher
64
+
65
+ ```python
66
+ dp = Dispatcher()
67
+
68
+ @dp.message(*filters) # новое сообщение
69
+ @dp.edited_message(*filters) # изменённое сообщение
70
+ @dp.callback_query(*filters) # нажатие inline-кнопки
71
+ ```
72
+
73
+ ### Фильтры
74
+
75
+ ```python
76
+ from pykodaribot import filters
77
+
78
+ filters.command("start", "help") # команды /start или /help
79
+ filters.text(r"привет|hello") # regex по тексту
80
+ filters.callback_data("yes") # точное совпадение callback_data
81
+ filters.callback_data_prefix("action:") # callback_data начинается с префикса
82
+ ```
83
+
84
+ ### Inline-клавиатура
85
+
86
+ ```python
87
+ from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
88
+
89
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
90
+ [
91
+ InlineKeyboardButton(text="Да", callback_data="yes"),
92
+ InlineKeyboardButton(text="Нет", callback_data="no"),
93
+ ],
94
+ [
95
+ InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
96
+ ],
97
+ ])
98
+
99
+ await msg.answer("Выберите:", reply_markup=keyboard)
100
+ ```
101
+
102
+ ### Отправка медиа
103
+
104
+ `media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
105
+
106
+ ```python
107
+ await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
108
+ await bot.send_photo(chat_id, Path("/tmp/image.png"))
109
+ await bot.send_document(chat_id, open("report.pdf", "rb"))
110
+
111
+ with open("clip.mp4", "rb") as f:
112
+ await bot.send_video(chat_id, f, caption="Видео")
113
+ ```
114
+
115
+ ## Примеры
116
+
117
+ - [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
118
+ - [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
119
+ - [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
@@ -0,0 +1,31 @@
1
+ from .bot import Bot, BotError
2
+ from .dispatcher import Dispatcher, Router
3
+ from .types import (
4
+ BotCommand,
5
+ CallbackQuery,
6
+ Chat,
7
+ InlineKeyboardButton,
8
+ InlineKeyboardMarkup,
9
+ Message,
10
+ Update,
11
+ User,
12
+ )
13
+ from . import filters
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "Bot",
19
+ "BotError",
20
+ "Dispatcher",
21
+ "Router",
22
+ "User",
23
+ "Chat",
24
+ "Message",
25
+ "CallbackQuery",
26
+ "Update",
27
+ "InlineKeyboardMarkup",
28
+ "InlineKeyboardButton",
29
+ "BotCommand",
30
+ "filters",
31
+ ]
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import BinaryIO, List, Optional, Union
4
+
5
+ import aiohttp
6
+
7
+ from .types import (
8
+ BotCommand, Chat, InlineKeyboardMarkup,
9
+ Message, Update, User,
10
+ )
11
+
12
+ MediaInput = Union[str, bytes, Path, BinaryIO]
13
+
14
+
15
+ class BotError(Exception):
16
+ def __init__(self, error_code: int, description: str):
17
+ self.error_code = error_code
18
+ self.description = description
19
+ super().__init__(f"[{error_code}] {description}")
20
+
21
+
22
+ class Bot:
23
+ BASE_URL = "https://api.kodari.ru/msg/bot.php"
24
+
25
+ def __init__(self, token: str):
26
+ self._token = token
27
+ self._headers = {"Authorization": f"Bearer {token}"}
28
+ self._session: Optional[aiohttp.ClientSession] = None
29
+
30
+ async def _get_session(self) -> aiohttp.ClientSession:
31
+ if self._session is None or self._session.closed:
32
+ self._session = aiohttp.ClientSession(headers=self._headers)
33
+ return self._session
34
+
35
+ async def close(self) -> None:
36
+ if self._session and not self._session.closed:
37
+ await self._session.close()
38
+
39
+ async def __aenter__(self) -> "Bot":
40
+ return self
41
+
42
+ async def __aexit__(self, *_) -> None:
43
+ await self.close()
44
+
45
+ async def _get(self, method: str, **params) -> object:
46
+ session = await self._get_session()
47
+ params["method"] = method
48
+ async with session.get(self.BASE_URL, params=params) as r:
49
+ data = await r.json()
50
+ if not data.get("ok"):
51
+ raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
52
+ return data["result"]
53
+
54
+ async def _post(self, method: str, **body) -> object:
55
+ session = await self._get_session()
56
+ url = f"{self.BASE_URL}?method={method}"
57
+ body = {k: v for k, v in body.items() if v is not None}
58
+ async with session.post(url, json=body) as r:
59
+ data = await r.json()
60
+ if not data.get("ok"):
61
+ raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
62
+ return data["result"]
63
+
64
+ async def _post_media(self, method: str, chat_id: int, media: MediaInput,
65
+ caption: Optional[str], reply_to_message_id: Optional[int]) -> object:
66
+ session = await self._get_session()
67
+ url = f"{self.BASE_URL}?method={method}"
68
+ form = aiohttp.FormData()
69
+ form.add_field("chat_id", str(chat_id))
70
+ if caption is not None:
71
+ form.add_field("caption", caption)
72
+ if reply_to_message_id is not None:
73
+ form.add_field("reply_to_message_id", str(reply_to_message_id))
74
+ if isinstance(media, (str, Path)):
75
+ path = Path(media)
76
+ with open(path, "rb") as f:
77
+ form.add_field("media", f, filename=path.name)
78
+ async with session.post(url, data=form) as r:
79
+ data = await r.json()
80
+ else:
81
+ filename = getattr(media, "name", "file")
82
+ form.add_field("media", media, filename=Path(filename).name)
83
+ async with session.post(url, data=form) as r:
84
+ data = await r.json()
85
+ if not data.get("ok"):
86
+ raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
87
+ return data["result"]
88
+
89
+ def _markup(self, reply_markup) -> Optional[dict]:
90
+ if reply_markup is None:
91
+ return None
92
+ return reply_markup.to_dict() if hasattr(reply_markup, "to_dict") else reply_markup
93
+
94
+ async def get_me(self) -> User:
95
+ return User.from_dict(await self._get("getMe"))
96
+
97
+ async def send_message(
98
+ self,
99
+ chat_id: int,
100
+ text: str,
101
+ reply_to_message_id: Optional[int] = None,
102
+ reply_markup: Optional[Union[InlineKeyboardMarkup, dict]] = None,
103
+ ) -> Message:
104
+ return Message.from_dict(
105
+ await self._post(
106
+ "sendMessage",
107
+ chat_id=chat_id,
108
+ text=text,
109
+ reply_to_message_id=reply_to_message_id,
110
+ reply_markup=self._markup(reply_markup),
111
+ ),
112
+ self,
113
+ )
114
+
115
+ async def send_photo(
116
+ self,
117
+ chat_id: int,
118
+ media: MediaInput,
119
+ caption: Optional[str] = None,
120
+ reply_to_message_id: Optional[int] = None,
121
+ ) -> Message:
122
+ return Message.from_dict(
123
+ await self._post_media("sendPhoto", chat_id, media, caption, reply_to_message_id),
124
+ self,
125
+ )
126
+
127
+ async def send_document(
128
+ self,
129
+ chat_id: int,
130
+ media: MediaInput,
131
+ caption: Optional[str] = None,
132
+ reply_to_message_id: Optional[int] = None,
133
+ ) -> Message:
134
+ return Message.from_dict(
135
+ await self._post_media("sendDocument", chat_id, media, caption, reply_to_message_id),
136
+ self,
137
+ )
138
+
139
+ async def send_video(
140
+ self,
141
+ chat_id: int,
142
+ media: MediaInput,
143
+ caption: Optional[str] = None,
144
+ reply_to_message_id: Optional[int] = None,
145
+ ) -> Message:
146
+ return Message.from_dict(
147
+ await self._post_media("sendVideo", chat_id, media, caption, reply_to_message_id),
148
+ self,
149
+ )
150
+
151
+ async def send_audio(
152
+ self,
153
+ chat_id: int,
154
+ media: MediaInput,
155
+ caption: Optional[str] = None,
156
+ reply_to_message_id: Optional[int] = None,
157
+ ) -> Message:
158
+ return Message.from_dict(
159
+ await self._post_media("sendAudio", chat_id, media, caption, reply_to_message_id),
160
+ self,
161
+ )
162
+
163
+ async def edit_message_text(
164
+ self,
165
+ chat_id: int,
166
+ message_id: int,
167
+ text: str,
168
+ reply_markup: Optional[Union[InlineKeyboardMarkup, dict]] = None,
169
+ ) -> Message:
170
+ return Message.from_dict(
171
+ await self._post("editMessageText", chat_id=chat_id, message_id=message_id,
172
+ text=text, reply_markup=self._markup(reply_markup)),
173
+ self,
174
+ )
175
+
176
+ async def delete_message(self, chat_id: int, message_id: int) -> bool:
177
+ return bool(await self._post("deleteMessage", chat_id=chat_id, message_id=message_id))
178
+
179
+ async def get_updates(
180
+ self,
181
+ offset: int = 0,
182
+ timeout: int = 20,
183
+ limit: int = 100,
184
+ ) -> List[Update]:
185
+ result = await self._get("getUpdates", offset=offset, timeout=timeout, limit=limit)
186
+ return [Update.from_dict(u, self) for u in result]
187
+
188
+ async def answer_callback_query(self, callback_query_id: str) -> bool:
189
+ return bool(await self._post("answerCallbackQuery", callback_query_id=callback_query_id))
190
+
191
+ async def set_my_commands(self, commands: List[BotCommand]) -> bool:
192
+ return bool(await self._post("setMyCommands", commands=[c.to_dict() for c in commands]))
193
+
194
+ async def get_my_commands(self) -> List[BotCommand]:
195
+ result = await self._get("getMyCommands")
196
+ return [BotCommand(c["command"], c["description"]) for c in result]
197
+
198
+ async def set_my_name(self, name: str) -> bool:
199
+ return bool(await self._post("setMyName", name=name))
200
+
201
+ async def set_my_description(self, description: str) -> bool:
202
+ return bool(await self._post("setMyDescription", description=description))
203
+
204
+ async def set_my_short_description(self, short_description: str) -> bool:
205
+ return bool(await self._post("setMyShortDescription", short_description=short_description))
206
+
207
+ async def get_chat(self, chat_id: int) -> Chat:
208
+ return Chat.from_dict(await self._get("getChat", chat_id=chat_id))
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import logging
4
+ from typing import Callable, List, Tuple
5
+
6
+ from .bot import Bot, BotError
7
+ from .filters import Filter
8
+ from .types import CallbackQuery, Message, Update
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ HandlerList = List[Tuple[List[Filter], Callable]]
13
+
14
+
15
+ class Router:
16
+ def __init__(self):
17
+ self._message_handlers: HandlerList = []
18
+ self._edited_message_handlers: HandlerList = []
19
+ self._callback_query_handlers: HandlerList = []
20
+
21
+ def message(self, *filters: Filter):
22
+ def decorator(func: Callable):
23
+ self._message_handlers.append((list(filters), func))
24
+ return func
25
+ return decorator
26
+
27
+ def edited_message(self, *filters: Filter):
28
+ def decorator(func: Callable):
29
+ self._edited_message_handlers.append((list(filters), func))
30
+ return func
31
+ return decorator
32
+
33
+ def callback_query(self, *filters: Filter):
34
+ def decorator(func: Callable):
35
+ self._callback_query_handlers.append((list(filters), func))
36
+ return func
37
+ return decorator
38
+
39
+ async def _run_handlers(self, handlers: HandlerList, obj) -> None:
40
+ for filters, handler in handlers:
41
+ if all([await f(obj) for f in filters]):
42
+ try:
43
+ await handler(obj)
44
+ except Exception:
45
+ logger.exception("Ошибка в обработчике %s", handler.__name__)
46
+ return
47
+
48
+ async def process_update(self, update: Update) -> None:
49
+ if update.message:
50
+ await self._run_handlers(self._message_handlers, update.message)
51
+ elif update.edited_message:
52
+ await self._run_handlers(self._edited_message_handlers, update.edited_message)
53
+ elif update.callback_query:
54
+ await self._run_handlers(self._callback_query_handlers, update.callback_query)
55
+
56
+
57
+ class Dispatcher(Router):
58
+ async def start_polling(self, bot: Bot, timeout: int = 20) -> None:
59
+ offset = 0
60
+ logger.info("Поллинг запущен")
61
+ try:
62
+ while True:
63
+ try:
64
+ updates = await bot.get_updates(offset=offset, timeout=timeout)
65
+ for update in updates:
66
+ offset = update.update_id + 1
67
+ await self.process_update(update)
68
+ except BotError as e:
69
+ logger.error("Ошибка API: %s", e)
70
+ await asyncio.sleep(1)
71
+ except asyncio.CancelledError:
72
+ raise
73
+ except Exception:
74
+ logger.exception("Ошибка поллинга")
75
+ await asyncio.sleep(1)
76
+ finally:
77
+ await bot.close()
78
+ logger.info("Поллинг остановлен")
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+ import re
3
+ from typing import Union
4
+
5
+ from .types import CallbackQuery, Message
6
+
7
+
8
+ class Filter:
9
+ async def __call__(self, obj) -> bool:
10
+ raise NotImplementedError
11
+
12
+
13
+ class CommandFilter(Filter):
14
+ def __init__(self, *commands: str):
15
+ self.commands = {c.lstrip("/") for c in commands}
16
+
17
+ async def __call__(self, msg: Message) -> bool:
18
+ if not msg.text:
19
+ return False
20
+ first = msg.text.split()[0].lstrip("/").split("@")[0]
21
+ return first in self.commands
22
+
23
+
24
+ class TextFilter(Filter):
25
+ def __init__(self, pattern: Union[str, re.Pattern]):
26
+ self.pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
27
+
28
+ async def __call__(self, msg: Message) -> bool:
29
+ return bool(msg.text and self.pattern.search(msg.text))
30
+
31
+
32
+ class CallbackDataFilter(Filter):
33
+ def __init__(self, data: str):
34
+ self.data = data
35
+
36
+ async def __call__(self, cb: CallbackQuery) -> bool:
37
+ return cb.data == self.data
38
+
39
+
40
+ class CallbackDataPrefixFilter(Filter):
41
+ def __init__(self, prefix: str):
42
+ self.prefix = prefix
43
+
44
+ async def __call__(self, cb: CallbackQuery) -> bool:
45
+ return cb.data is not None and cb.data.startswith(self.prefix)
46
+
47
+
48
+ def command(*commands: str) -> CommandFilter:
49
+ return CommandFilter(*commands)
50
+
51
+
52
+ def text(pattern: Union[str, re.Pattern]) -> TextFilter:
53
+ return TextFilter(pattern)
54
+
55
+
56
+ def callback_data(data: str) -> CallbackDataFilter:
57
+ return CallbackDataFilter(data)
58
+
59
+
60
+ def callback_data_prefix(prefix: str) -> CallbackDataPrefixFilter:
61
+ return CallbackDataPrefixFilter(prefix)
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, List, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .bot import Bot
7
+
8
+
9
+ @dataclass
10
+ class User:
11
+ id: int
12
+ is_bot: bool
13
+ first_name: str
14
+ username: Optional[str] = None
15
+
16
+ @classmethod
17
+ def from_dict(cls, d: dict) -> "User":
18
+ return cls(
19
+ id=d["id"],
20
+ is_bot=d.get("is_bot", False),
21
+ first_name=d.get("first_name", ""),
22
+ username=d.get("username"),
23
+ )
24
+
25
+
26
+ @dataclass
27
+ class Chat:
28
+ id: int
29
+ type: str
30
+
31
+ @classmethod
32
+ def from_dict(cls, d: dict) -> "Chat":
33
+ return cls(id=d["id"], type=d.get("type", ""))
34
+
35
+
36
+ @dataclass
37
+ class InlineKeyboardButton:
38
+ text: str
39
+ callback_data: Optional[str] = None
40
+ url: Optional[str] = None
41
+
42
+ def to_dict(self) -> dict:
43
+ d: dict = {"text": self.text}
44
+ if self.callback_data is not None:
45
+ d["callback_data"] = self.callback_data
46
+ if self.url is not None:
47
+ d["url"] = self.url
48
+ return d
49
+
50
+
51
+ @dataclass
52
+ class InlineKeyboardMarkup:
53
+ inline_keyboard: List[List[InlineKeyboardButton]] = field(default_factory=list)
54
+
55
+ def to_dict(self) -> dict:
56
+ return {
57
+ "inline_keyboard": [
58
+ [btn.to_dict() for btn in row]
59
+ for row in self.inline_keyboard
60
+ ]
61
+ }
62
+
63
+
64
+ @dataclass
65
+ class Message:
66
+ message_id: int
67
+ chat: Chat
68
+ date: int
69
+ from_user: Optional[User] = None
70
+ text: Optional[str] = None
71
+ _bot: Any = field(default=None, repr=False, compare=False)
72
+
73
+ @classmethod
74
+ def from_dict(cls, d: dict, bot: Any = None) -> "Message":
75
+ return cls(
76
+ message_id=d["message_id"],
77
+ chat=Chat.from_dict(d["chat"]),
78
+ date=d.get("date", 0),
79
+ from_user=User.from_dict(d["from"]) if "from" in d else None,
80
+ text=d.get("text"),
81
+ _bot=bot,
82
+ )
83
+
84
+ async def answer(self, text: str, **kwargs) -> "Message":
85
+ return await self._bot.send_message(self.chat.id, text, **kwargs)
86
+
87
+ async def reply(self, text: str, **kwargs) -> "Message":
88
+ return await self._bot.send_message(
89
+ self.chat.id, text, reply_to_message_id=self.message_id, **kwargs
90
+ )
91
+
92
+
93
+ @dataclass
94
+ class CallbackQuery:
95
+ id: str
96
+ from_user: User
97
+ data: Optional[str] = None
98
+ message: Optional[Message] = None
99
+ _bot: Any = field(default=None, repr=False, compare=False)
100
+
101
+ @classmethod
102
+ def from_dict(cls, d: dict, bot: Any = None) -> "CallbackQuery":
103
+ return cls(
104
+ id=d["id"],
105
+ from_user=User.from_dict(d["from"]),
106
+ data=d.get("data"),
107
+ message=Message.from_dict(d["message"], bot) if "message" in d else None,
108
+ _bot=bot,
109
+ )
110
+
111
+ async def answer(self) -> None:
112
+ await self._bot.answer_callback_query(self.id)
113
+
114
+
115
+ @dataclass
116
+ class Update:
117
+ update_id: int
118
+ message: Optional[Message] = None
119
+ edited_message: Optional[Message] = None
120
+ callback_query: Optional[CallbackQuery] = None
121
+
122
+ @classmethod
123
+ def from_dict(cls, d: dict, bot: Any = None) -> "Update":
124
+ return cls(
125
+ update_id=d["update_id"],
126
+ message=Message.from_dict(d["message"], bot) if "message" in d else None,
127
+ edited_message=Message.from_dict(d["edited_message"], bot) if "edited_message" in d else None,
128
+ callback_query=CallbackQuery.from_dict(d["callback_query"], bot) if "callback_query" in d else None,
129
+ )
130
+
131
+
132
+ @dataclass
133
+ class BotCommand:
134
+ command: str
135
+ description: str
136
+
137
+ def to_dict(self) -> dict:
138
+ return {"command": self.command, "description": self.description}
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: pykodaribot
3
+ Version: 0.1.0
4
+ Summary: Python SDK для создания ботов в Kodari Messenger
5
+ License: MIT
6
+ Project-URL: Homepage, https://msg.kodari.ru
7
+ Project-URL: Documentation, https://docs.kodari.ru/
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: AsyncIO
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: aiohttp>=3.9
15
+
16
+ # pykodaribot
17
+
18
+ Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
19
+
20
+ **Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
21
+
22
+ ## Установка
23
+
24
+ ```bash
25
+ pip install pykodaribot
26
+ ```
27
+
28
+ **Требования:** Python 3.9+
29
+
30
+ ## Быстрый старт
31
+
32
+ ```python
33
+ import asyncio
34
+ from pykodaribot import Bot, Dispatcher, filters
35
+
36
+ bot = Bot("bot_ВАШ_ТОКЕН")
37
+ dp = Dispatcher()
38
+
39
+ @dp.message(filters.command("start"))
40
+ async def start(msg):
41
+ await msg.answer("Привет!")
42
+
43
+ @dp.message()
44
+ async def echo(msg):
45
+ if msg.text:
46
+ await msg.reply(msg.text)
47
+
48
+ asyncio.run(dp.start_polling(bot))
49
+ ```
50
+
51
+ ## Токен
52
+
53
+ Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
54
+
55
+ ## API
56
+
57
+ ### Bot
58
+
59
+ | Метод | Описание |
60
+ |---|---|
61
+ | `get_me()` | Информация о боте |
62
+ | `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
63
+ | `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
64
+ | `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
65
+ | `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
66
+ | `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
67
+ | `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
68
+ | `delete_message(chat_id, message_id)` | Удалить сообщение |
69
+ | `get_updates(offset, timeout, limit)` | Получить апдейты |
70
+ | `answer_callback_query(callback_query_id)` | Ответить на callback |
71
+ | `set_my_commands(commands)` | Установить команды меню |
72
+ | `get_my_commands()` | Получить команды меню |
73
+ | `set_my_name(name)` | Изменить имя бота |
74
+ | `set_my_description(description)` | Изменить описание |
75
+ | `set_my_short_description(short_description)` | Изменить краткое описание |
76
+ | `get_chat(chat_id)` | Информация о чате |
77
+
78
+ ### Dispatcher
79
+
80
+ ```python
81
+ dp = Dispatcher()
82
+
83
+ @dp.message(*filters) # новое сообщение
84
+ @dp.edited_message(*filters) # изменённое сообщение
85
+ @dp.callback_query(*filters) # нажатие inline-кнопки
86
+ ```
87
+
88
+ ### Фильтры
89
+
90
+ ```python
91
+ from pykodaribot import filters
92
+
93
+ filters.command("start", "help") # команды /start или /help
94
+ filters.text(r"привет|hello") # regex по тексту
95
+ filters.callback_data("yes") # точное совпадение callback_data
96
+ filters.callback_data_prefix("action:") # callback_data начинается с префикса
97
+ ```
98
+
99
+ ### Inline-клавиатура
100
+
101
+ ```python
102
+ from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
103
+
104
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
105
+ [
106
+ InlineKeyboardButton(text="Да", callback_data="yes"),
107
+ InlineKeyboardButton(text="Нет", callback_data="no"),
108
+ ],
109
+ [
110
+ InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
111
+ ],
112
+ ])
113
+
114
+ await msg.answer("Выберите:", reply_markup=keyboard)
115
+ ```
116
+
117
+ ### Отправка медиа
118
+
119
+ `media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
120
+
121
+ ```python
122
+ await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
123
+ await bot.send_photo(chat_id, Path("/tmp/image.png"))
124
+ await bot.send_document(chat_id, open("report.pdf", "rb"))
125
+
126
+ with open("clip.mp4", "rb") as f:
127
+ await bot.send_video(chat_id, f, caption="Видео")
128
+ ```
129
+
130
+ ## Примеры
131
+
132
+ - [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
133
+ - [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
134
+ - [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ pykodaribot/__init__.py
4
+ pykodaribot/bot.py
5
+ pykodaribot/dispatcher.py
6
+ pykodaribot/filters.py
7
+ pykodaribot/types.py
8
+ pykodaribot.egg-info/PKG-INFO
9
+ pykodaribot.egg-info/SOURCES.txt
10
+ pykodaribot.egg-info/dependency_links.txt
11
+ pykodaribot.egg-info/requires.txt
12
+ pykodaribot.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ aiohttp>=3.9
@@ -0,0 +1 @@
1
+ pykodaribot
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pykodaribot"
7
+ version = "0.1.0"
8
+ description = "Python SDK для создания ботов в Kodari Messenger"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ dependencies = ["aiohttp>=3.9"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Framework :: AsyncIO",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://msg.kodari.ru"
22
+ Documentation = "https://docs.kodari.ru/"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
26
+ include = ["pykodaribot*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+