yuchatlib 0.2.2__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.
- yuchatlib-0.2.2/PKG-INFO +190 -0
- yuchatlib-0.2.2/README.md +166 -0
- yuchatlib-0.2.2/pyproject.toml +34 -0
- yuchatlib-0.2.2/setup.cfg +4 -0
- yuchatlib-0.2.2/src/yuchatlib/__init__.py +7 -0
- yuchatlib-0.2.2/src/yuchatlib/api/__init__.py +23 -0
- yuchatlib-0.2.2/src/yuchatlib/api/_transport.py +59 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/__init__.py +1 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/me/__init__.py +1 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/me/me_info.py +19 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/me/profile.py +13 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/__init__.py +1 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/button.py +13 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/button_bar.py +13 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/button_group.py +11 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/command_button.py +12 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/get_messages_response.py +11 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/message/send_message_response.py +8 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/__init__.py +1 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/message.py +20 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/message_action.py +15 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/message_content.py +11 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/pressed_button_command.py +9 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/update.py +21 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/update/updates_response.py +9 -0
- yuchatlib-0.2.2/src/yuchatlib/api/dto/webhook_info.py +19 -0
- yuchatlib-0.2.2/src/yuchatlib/bot.py +220 -0
- yuchatlib-0.2.2/src/yuchatlib/config.py +120 -0
- yuchatlib-0.2.2/src/yuchatlib/cursor.py +89 -0
- yuchatlib-0.2.2/src/yuchatlib/dispatcher.py +284 -0
- yuchatlib-0.2.2/src/yuchatlib/exceptions.py +33 -0
- yuchatlib-0.2.2/src/yuchatlib/fsm/__init__.py +6 -0
- yuchatlib-0.2.2/src/yuchatlib/fsm/context.py +100 -0
- yuchatlib-0.2.2/src/yuchatlib/fsm/sqlite.py +110 -0
- yuchatlib-0.2.2/src/yuchatlib/router.py +70 -0
- yuchatlib-0.2.2/src/yuchatlib/webhook.py +62 -0
- yuchatlib-0.2.2/src/yuchatlib.egg-info/PKG-INFO +190 -0
- yuchatlib-0.2.2/src/yuchatlib.egg-info/SOURCES.txt +54 -0
- yuchatlib-0.2.2/src/yuchatlib.egg-info/dependency_links.txt +1 -0
- yuchatlib-0.2.2/src/yuchatlib.egg-info/requires.txt +23 -0
- yuchatlib-0.2.2/src/yuchatlib.egg-info/top_level.txt +1 -0
- yuchatlib-0.2.2/tests/test_bot_transport_contract.py +41 -0
- yuchatlib-0.2.2/tests/test_button_bar.py +44 -0
- yuchatlib-0.2.2/tests/test_config.py +128 -0
- yuchatlib-0.2.2/tests/test_dispatcher.py +100 -0
- yuchatlib-0.2.2/tests/test_dispatcher_contract.py +123 -0
- yuchatlib-0.2.2/tests/test_full_stand_check.py +505 -0
- yuchatlib-0.2.2/tests/test_message_methods.py +167 -0
- yuchatlib-0.2.2/tests/test_mvp.py +209 -0
- yuchatlib-0.2.2/tests/test_polling_baseline.py +22 -0
- yuchatlib-0.2.2/tests/test_polling_cursor.py +296 -0
- yuchatlib-0.2.2/tests/test_reliability.py +249 -0
- yuchatlib-0.2.2/tests/test_security_boundaries.py +117 -0
- yuchatlib-0.2.2/tests/test_sqlite_fsm.py +194 -0
- yuchatlib-0.2.2/tests/test_stand_probe.py +133 -0
- yuchatlib-0.2.2/tests/test_webhook.py +152 -0
yuchatlib-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yuchatlib
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Асинхронная библиотека для ботов YuChat
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx<1,>=0.27
|
|
8
|
+
Requires-Dist: pydantic<3,>=2
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: pytest<10,>=8; extra == "test"
|
|
11
|
+
Requires-Dist: pytest-asyncio<2,>=0.23; extra == "test"
|
|
12
|
+
Requires-Dist: python-dotenv<2,>=1; extra == "test"
|
|
13
|
+
Requires-Dist: aiosqlite<1,>=0.20; extra == "test"
|
|
14
|
+
Provides-Extra: stand
|
|
15
|
+
Requires-Dist: python-dotenv<2,>=1; extra == "stand"
|
|
16
|
+
Provides-Extra: env
|
|
17
|
+
Requires-Dist: python-dotenv<2,>=1; extra == "env"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: ruff<1,>=0.11; extra == "dev"
|
|
20
|
+
Provides-Extra: sqlite
|
|
21
|
+
Requires-Dist: aiosqlite<1,>=0.20; extra == "sqlite"
|
|
22
|
+
Provides-Extra: webhook
|
|
23
|
+
Requires-Dist: aiohttp<4,>=3.10; extra == "webhook"
|
|
24
|
+
|
|
25
|
+
# yuchatlib
|
|
26
|
+
|
|
27
|
+
Асинхронная Python-библиотека для создания ботов YuChat. Она предоставляет обработчики команд и сообщений, роутеры, фильтры, middleware и управление состояниями диалога.
|
|
28
|
+
|
|
29
|
+
Библиотека поддерживает **Python 3.11 и новее**, работу с YuChat Cloud и On-Premises.
|
|
30
|
+
|
|
31
|
+
## Возможности
|
|
32
|
+
|
|
33
|
+
- Получение событий в режимах **polling** и **webhook**.
|
|
34
|
+
- Последовательная обработка событий через `Dispatcher`.
|
|
35
|
+
- Вложенные роутеры и асинхронные обработчики.
|
|
36
|
+
- Команды с префиксом `/`, фильтры и middleware.
|
|
37
|
+
- Командные кнопки и обработка нажатий.
|
|
38
|
+
- FSM для хранения состояния и данных диалога.
|
|
39
|
+
- Хранение FSM и курсора polling в памяти или SQLite.
|
|
40
|
+
- Отправка, чтение и редактирование сообщений.
|
|
41
|
+
|
|
42
|
+
## Настройки
|
|
43
|
+
|
|
44
|
+
Параметры можно указать в файле `.env` или в переменных окружения. Значения переменных окружения имеют приоритет над значениями из файла.
|
|
45
|
+
|
|
46
|
+
| Переменная | Назначение | Значение по умолчанию |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `YUCHAT_TOKEN` | Токен бота. | Не задан |
|
|
49
|
+
| `YUCHAT_BASE_URL` | Базовый адрес API YuChat Cloud или вашего On-Premises-стенда. | Не задан |
|
|
50
|
+
| `YUCHAT_WEBHOOK_SECRET` | Секрет для проверки входящих webhook-запросов. | Не задан |
|
|
51
|
+
| `YUCHAT_WORKSPACE_ID` | Идентификатор рабочего пространства для сценариев, которым он необходим. | Не задан |
|
|
52
|
+
| `YUCHAT_CHAT_ID` | Идентификатор чата для сценариев, которым он необходим. | Не задан |
|
|
53
|
+
| `YUCHAT_FSM_NAMESPACE` | Постоянная идентичность бота для SQLite FSM и курсора. | Не задан |
|
|
54
|
+
| `YUCHAT_FSM_SQLITE_PATH` | Путь к файлу SQLite для FSM и курсора. | Не задан |
|
|
55
|
+
| `YUCHAT_HTTP_TIMEOUT` | Таймаут HTTP-запросов в секундах. | `30.0` |
|
|
56
|
+
| `YUCHAT_POLLING_INTERVAL` | Задержка между запросами polling в секундах. | `1.0` |
|
|
57
|
+
| `YUCHAT_POLLING_LIMIT` | Максимальное число событий в одном запросе. | `20` |
|
|
58
|
+
| `YUCHAT_POLLING_MAX_RETRIES` | Максимальное число повторных попыток при временных ошибках. | `5` |
|
|
59
|
+
|
|
60
|
+
## Быстрый старт: polling и команда
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
|
|
65
|
+
from yuchatlib import Bot, Dispatcher, Router
|
|
66
|
+
from yuchatlib.api import MessageDto
|
|
67
|
+
from yuchatlib.config import Settings
|
|
68
|
+
|
|
69
|
+
router = Router()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.command("start")
|
|
73
|
+
async def start(message: MessageDto, bot: Bot) -> None:
|
|
74
|
+
await bot.send_message(
|
|
75
|
+
workspace_id=message.workspace_id,
|
|
76
|
+
chat_id=message.chat_id,
|
|
77
|
+
text="Привет! Я бот YuChat.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def main() -> None:
|
|
82
|
+
settings = Settings.from_env()
|
|
83
|
+
|
|
84
|
+
dispatcher = Dispatcher()
|
|
85
|
+
dispatcher.include_router(router)
|
|
86
|
+
|
|
87
|
+
async with Bot(
|
|
88
|
+
settings.require_token(),
|
|
89
|
+
base_url=settings.require_base_url(),
|
|
90
|
+
timeout=settings.http_timeout,
|
|
91
|
+
) as bot:
|
|
92
|
+
await dispatcher.start_polling(
|
|
93
|
+
bot,
|
|
94
|
+
interval=settings.polling_interval,
|
|
95
|
+
limit=settings.polling_limit,
|
|
96
|
+
max_retries=settings.polling_max_retries,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
После запуска отправьте боту команду `/start`. В декораторе имя команды указывается **без** символа `/`.
|
|
105
|
+
|
|
106
|
+
## Состояния диалога
|
|
107
|
+
|
|
108
|
+
FSM позволяет сохранять состояние и данные отдельно для каждого участника чата. Обработчики ниже можно добавить в пример с polling.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from yuchatlib.api import MessageDto
|
|
112
|
+
from yuchatlib.fsm import FSMContext, State, StatesGroup
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Form(StatesGroup):
|
|
116
|
+
waiting_for_name = State()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.command("name")
|
|
120
|
+
async def ask_name(message: MessageDto, state: FSMContext, bot: Bot) -> None:
|
|
121
|
+
await state.set_state(Form.waiting_for_name)
|
|
122
|
+
await bot.send_message(
|
|
123
|
+
workspace_id=message.workspace_id,
|
|
124
|
+
chat_id=message.chat_id,
|
|
125
|
+
text="Как вас зовут?",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.message(Form.waiting_for_name)
|
|
130
|
+
async def save_name(message: MessageDto, state: FSMContext, bot: Bot) -> None:
|
|
131
|
+
await state.update_data(name=message.content.text)
|
|
132
|
+
await bot.send_message(
|
|
133
|
+
workspace_id=message.workspace_id,
|
|
134
|
+
chat_id=message.chat_id,
|
|
135
|
+
text="Имя сохранено.",
|
|
136
|
+
)
|
|
137
|
+
await state.clear()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
По умолчанию FSM хранится в памяти процесса и очищается при его завершении. `clear()` удаляет состояние и данные текущего контекста.
|
|
141
|
+
|
|
142
|
+
## Командные кнопки
|
|
143
|
+
|
|
144
|
+
Бот может отправить сообщение с кнопкой и обработать её нажатие.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from yuchatlib.api import (
|
|
148
|
+
ButtonBarDto,
|
|
149
|
+
ButtonDto,
|
|
150
|
+
ButtonGroupDto,
|
|
151
|
+
CommandButtonDto,
|
|
152
|
+
MessageActionDto,
|
|
153
|
+
MessageDto,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.command("buttons")
|
|
158
|
+
async def show_buttons(message: MessageDto, bot: Bot) -> None:
|
|
159
|
+
button_bar = ButtonBarDto(
|
|
160
|
+
button_groups=[
|
|
161
|
+
ButtonGroupDto(
|
|
162
|
+
buttons=[
|
|
163
|
+
ButtonDto(
|
|
164
|
+
command_button=CommandButtonDto(
|
|
165
|
+
display_text="Подтвердить",
|
|
166
|
+
command_key="/confirm",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
await bot.send_message(
|
|
174
|
+
workspace_id=message.workspace_id,
|
|
175
|
+
chat_id=message.chat_id,
|
|
176
|
+
text="Выберите действие:",
|
|
177
|
+
button_bar=button_bar,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.callback_query(command_key="/confirm")
|
|
182
|
+
async def confirm(action: MessageActionDto, bot: Bot) -> None:
|
|
183
|
+
await bot.send_message(
|
|
184
|
+
workspace_id=action.workspace_id,
|
|
185
|
+
chat_id=action.chat_id,
|
|
186
|
+
text="Действие подтверждено.",
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Для получения нажатий у бота должен быть включён тип обновлений `MESSAGE_ACTION`. Идентификатор нажавшего пользователя в модели действия кнопки не предусмотрен, поэтому персональный FSM-контекст для такого действия автоматически не создаётся.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# yuchatlib
|
|
2
|
+
|
|
3
|
+
Асинхронная Python-библиотека для создания ботов YuChat. Она предоставляет обработчики команд и сообщений, роутеры, фильтры, middleware и управление состояниями диалога.
|
|
4
|
+
|
|
5
|
+
Библиотека поддерживает **Python 3.11 и новее**, работу с YuChat Cloud и On-Premises.
|
|
6
|
+
|
|
7
|
+
## Возможности
|
|
8
|
+
|
|
9
|
+
- Получение событий в режимах **polling** и **webhook**.
|
|
10
|
+
- Последовательная обработка событий через `Dispatcher`.
|
|
11
|
+
- Вложенные роутеры и асинхронные обработчики.
|
|
12
|
+
- Команды с префиксом `/`, фильтры и middleware.
|
|
13
|
+
- Командные кнопки и обработка нажатий.
|
|
14
|
+
- FSM для хранения состояния и данных диалога.
|
|
15
|
+
- Хранение FSM и курсора polling в памяти или SQLite.
|
|
16
|
+
- Отправка, чтение и редактирование сообщений.
|
|
17
|
+
|
|
18
|
+
## Настройки
|
|
19
|
+
|
|
20
|
+
Параметры можно указать в файле `.env` или в переменных окружения. Значения переменных окружения имеют приоритет над значениями из файла.
|
|
21
|
+
|
|
22
|
+
| Переменная | Назначение | Значение по умолчанию |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `YUCHAT_TOKEN` | Токен бота. | Не задан |
|
|
25
|
+
| `YUCHAT_BASE_URL` | Базовый адрес API YuChat Cloud или вашего On-Premises-стенда. | Не задан |
|
|
26
|
+
| `YUCHAT_WEBHOOK_SECRET` | Секрет для проверки входящих webhook-запросов. | Не задан |
|
|
27
|
+
| `YUCHAT_WORKSPACE_ID` | Идентификатор рабочего пространства для сценариев, которым он необходим. | Не задан |
|
|
28
|
+
| `YUCHAT_CHAT_ID` | Идентификатор чата для сценариев, которым он необходим. | Не задан |
|
|
29
|
+
| `YUCHAT_FSM_NAMESPACE` | Постоянная идентичность бота для SQLite FSM и курсора. | Не задан |
|
|
30
|
+
| `YUCHAT_FSM_SQLITE_PATH` | Путь к файлу SQLite для FSM и курсора. | Не задан |
|
|
31
|
+
| `YUCHAT_HTTP_TIMEOUT` | Таймаут HTTP-запросов в секундах. | `30.0` |
|
|
32
|
+
| `YUCHAT_POLLING_INTERVAL` | Задержка между запросами polling в секундах. | `1.0` |
|
|
33
|
+
| `YUCHAT_POLLING_LIMIT` | Максимальное число событий в одном запросе. | `20` |
|
|
34
|
+
| `YUCHAT_POLLING_MAX_RETRIES` | Максимальное число повторных попыток при временных ошибках. | `5` |
|
|
35
|
+
|
|
36
|
+
## Быстрый старт: polling и команда
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
from yuchatlib import Bot, Dispatcher, Router
|
|
42
|
+
from yuchatlib.api import MessageDto
|
|
43
|
+
from yuchatlib.config import Settings
|
|
44
|
+
|
|
45
|
+
router = Router()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.command("start")
|
|
49
|
+
async def start(message: MessageDto, bot: Bot) -> None:
|
|
50
|
+
await bot.send_message(
|
|
51
|
+
workspace_id=message.workspace_id,
|
|
52
|
+
chat_id=message.chat_id,
|
|
53
|
+
text="Привет! Я бот YuChat.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def main() -> None:
|
|
58
|
+
settings = Settings.from_env()
|
|
59
|
+
|
|
60
|
+
dispatcher = Dispatcher()
|
|
61
|
+
dispatcher.include_router(router)
|
|
62
|
+
|
|
63
|
+
async with Bot(
|
|
64
|
+
settings.require_token(),
|
|
65
|
+
base_url=settings.require_base_url(),
|
|
66
|
+
timeout=settings.http_timeout,
|
|
67
|
+
) as bot:
|
|
68
|
+
await dispatcher.start_polling(
|
|
69
|
+
bot,
|
|
70
|
+
interval=settings.polling_interval,
|
|
71
|
+
limit=settings.polling_limit,
|
|
72
|
+
max_retries=settings.polling_max_retries,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
После запуска отправьте боту команду `/start`. В декораторе имя команды указывается **без** символа `/`.
|
|
81
|
+
|
|
82
|
+
## Состояния диалога
|
|
83
|
+
|
|
84
|
+
FSM позволяет сохранять состояние и данные отдельно для каждого участника чата. Обработчики ниже можно добавить в пример с polling.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from yuchatlib.api import MessageDto
|
|
88
|
+
from yuchatlib.fsm import FSMContext, State, StatesGroup
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Form(StatesGroup):
|
|
92
|
+
waiting_for_name = State()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.command("name")
|
|
96
|
+
async def ask_name(message: MessageDto, state: FSMContext, bot: Bot) -> None:
|
|
97
|
+
await state.set_state(Form.waiting_for_name)
|
|
98
|
+
await bot.send_message(
|
|
99
|
+
workspace_id=message.workspace_id,
|
|
100
|
+
chat_id=message.chat_id,
|
|
101
|
+
text="Как вас зовут?",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.message(Form.waiting_for_name)
|
|
106
|
+
async def save_name(message: MessageDto, state: FSMContext, bot: Bot) -> None:
|
|
107
|
+
await state.update_data(name=message.content.text)
|
|
108
|
+
await bot.send_message(
|
|
109
|
+
workspace_id=message.workspace_id,
|
|
110
|
+
chat_id=message.chat_id,
|
|
111
|
+
text="Имя сохранено.",
|
|
112
|
+
)
|
|
113
|
+
await state.clear()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
По умолчанию FSM хранится в памяти процесса и очищается при его завершении. `clear()` удаляет состояние и данные текущего контекста.
|
|
117
|
+
|
|
118
|
+
## Командные кнопки
|
|
119
|
+
|
|
120
|
+
Бот может отправить сообщение с кнопкой и обработать её нажатие.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from yuchatlib.api import (
|
|
124
|
+
ButtonBarDto,
|
|
125
|
+
ButtonDto,
|
|
126
|
+
ButtonGroupDto,
|
|
127
|
+
CommandButtonDto,
|
|
128
|
+
MessageActionDto,
|
|
129
|
+
MessageDto,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.command("buttons")
|
|
134
|
+
async def show_buttons(message: MessageDto, bot: Bot) -> None:
|
|
135
|
+
button_bar = ButtonBarDto(
|
|
136
|
+
button_groups=[
|
|
137
|
+
ButtonGroupDto(
|
|
138
|
+
buttons=[
|
|
139
|
+
ButtonDto(
|
|
140
|
+
command_button=CommandButtonDto(
|
|
141
|
+
display_text="Подтвердить",
|
|
142
|
+
command_key="/confirm",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
await bot.send_message(
|
|
150
|
+
workspace_id=message.workspace_id,
|
|
151
|
+
chat_id=message.chat_id,
|
|
152
|
+
text="Выберите действие:",
|
|
153
|
+
button_bar=button_bar,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.callback_query(command_key="/confirm")
|
|
158
|
+
async def confirm(action: MessageActionDto, bot: Bot) -> None:
|
|
159
|
+
await bot.send_message(
|
|
160
|
+
workspace_id=action.workspace_id,
|
|
161
|
+
chat_id=action.chat_id,
|
|
162
|
+
text="Действие подтверждено.",
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Для получения нажатий у бота должен быть включён тип обновлений `MESSAGE_ACTION`. Идентификатор нажавшего пользователя в модели действия кнопки не предусмотрен, поэтому персональный FSM-контекст для такого действия автоматически не создаётся.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yuchatlib"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Асинхронная библиотека для ботов YuChat"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["httpx>=0.27,<1", "pydantic>=2,<3"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
test = ["pytest>=8,<10", "pytest-asyncio>=0.23,<2", "python-dotenv>=1,<2", "aiosqlite>=0.20,<1"]
|
|
15
|
+
stand = ["python-dotenv>=1,<2"]
|
|
16
|
+
env = ["python-dotenv>=1,<2"]
|
|
17
|
+
dev = ["ruff>=0.11,<1"]
|
|
18
|
+
sqlite = ["aiosqlite>=0.20,<1"]
|
|
19
|
+
webhook = ["aiohttp>=3.10,<4"]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
asyncio_mode = "auto"
|
|
27
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 120
|
|
31
|
+
target-version = "py311"
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "I"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Интеграция с публичным API YuChat."""
|
|
2
|
+
|
|
3
|
+
from .dto.message.button import ButtonDto
|
|
4
|
+
from .dto.message.button_bar import ButtonBarDto
|
|
5
|
+
from .dto.message.button_group import ButtonGroupDto
|
|
6
|
+
from .dto.message.command_button import CommandButtonDto
|
|
7
|
+
from .dto.message.get_messages_response import GetMessagesResponseDto
|
|
8
|
+
from .dto.update.message import MessageDto
|
|
9
|
+
from .dto.update.message_action import MessageActionDto
|
|
10
|
+
from .dto.update.update import UpdateDto
|
|
11
|
+
from .dto.webhook_info import WebhookInfoDto
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ButtonDto",
|
|
15
|
+
"ButtonBarDto",
|
|
16
|
+
"ButtonGroupDto",
|
|
17
|
+
"CommandButtonDto",
|
|
18
|
+
"GetMessagesResponseDto",
|
|
19
|
+
"MessageDto",
|
|
20
|
+
"MessageActionDto",
|
|
21
|
+
"UpdateDto",
|
|
22
|
+
"WebhookInfoDto",
|
|
23
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Внутренний HTTP-транспорт публичного API YuChat."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from yuchatlib.exceptions import ApiError, AuthenticationError, InvalidResponseError, NetworkError, RateLimitError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _YuChatHttpTransport:
|
|
11
|
+
"""Отправляет запросы и преобразует ответы без раскрытия данных."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: httpx.AsyncClient, base_url: str, token: str) -> None:
|
|
14
|
+
self._client = client
|
|
15
|
+
self._base_url = base_url
|
|
16
|
+
self._token = token
|
|
17
|
+
|
|
18
|
+
async def post(self, method: str, body: dict[str, Any] | None = None) -> Any:
|
|
19
|
+
try:
|
|
20
|
+
response = await self._client.post(
|
|
21
|
+
f"{self._base_url}/public/v2/{method}",
|
|
22
|
+
headers={"Authorization": f"Bearer {self._token}"},
|
|
23
|
+
json=body,
|
|
24
|
+
)
|
|
25
|
+
except httpx.RequestError:
|
|
26
|
+
pass
|
|
27
|
+
else:
|
|
28
|
+
return self.parse_response(response)
|
|
29
|
+
raise NetworkError("Ошибка соединения с YuChat")
|
|
30
|
+
|
|
31
|
+
async def request(self, http_method: str, method: str) -> httpx.Response:
|
|
32
|
+
try:
|
|
33
|
+
response = await self._client.request(
|
|
34
|
+
http_method,
|
|
35
|
+
f"{self._base_url}/public/v2/{method}",
|
|
36
|
+
headers={"Authorization": f"Bearer {self._token}"},
|
|
37
|
+
)
|
|
38
|
+
except httpx.RequestError:
|
|
39
|
+
pass
|
|
40
|
+
else:
|
|
41
|
+
return response
|
|
42
|
+
raise NetworkError("Ошибка соединения с YuChat")
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def parse_response(response: httpx.Response) -> Any:
|
|
46
|
+
"""Возвращает JSON или преобразует ошибки API в исключения SDK."""
|
|
47
|
+
if response.status_code in (401, 403):
|
|
48
|
+
raise AuthenticationError(response.status_code)
|
|
49
|
+
if response.status_code == 429:
|
|
50
|
+
raise RateLimitError(429, response.headers.get("Retry-After"))
|
|
51
|
+
if not response.is_success:
|
|
52
|
+
raise ApiError(response.status_code)
|
|
53
|
+
if not response.content:
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
return response.json()
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass
|
|
59
|
+
raise InvalidResponseError("Ответ YuChat не содержит корректный JSON")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Контракты публичного API YuChat."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Модели сведений о боте."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from .profile import ProfileDto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MeInfoDto(BaseModel):
|
|
9
|
+
"""Сведения о боте и настройках обновлений."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
12
|
+
profile: ProfileDto = Field(description="Профиль бота.")
|
|
13
|
+
workspaces: list[str] = Field(description="Рабочие пространства бота.")
|
|
14
|
+
update_api_version: int = Field(alias="updateApiVersion", description="Версия API обновлений.")
|
|
15
|
+
update_settings: list[str] = Field(alias="updateSettings", description="Включённые типы обновлений.")
|
|
16
|
+
auto_accept_workspace_invites: bool = Field(
|
|
17
|
+
alias="autoAcceptWorkspaceInvites", description="Автоматическое принятие приглашений."
|
|
18
|
+
)
|
|
19
|
+
scope: dict[str, Any] = Field(description="Область действия бота.")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProfileDto(BaseModel):
|
|
7
|
+
"""Профиль аккаунта бота."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
10
|
+
account_id: str = Field(alias="accountId", description="Идентификатор аккаунта.")
|
|
11
|
+
created_at: datetime = Field(alias="createdAt", description="Время создания аккаунта.")
|
|
12
|
+
updated_at: datetime = Field(alias="updatedAt", description="Время изменения аккаунта.")
|
|
13
|
+
account_type: str = Field(alias="accountType", description="Тип аккаунта.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Модели операций с сообщениями."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Контракт кнопки сообщения."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from .command_button import CommandButtonDto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ButtonDto(BaseModel):
|
|
9
|
+
"""Командная кнопка сообщения."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
12
|
+
|
|
13
|
+
command_button: CommandButtonDto = Field(alias="commandButton", description="Командная кнопка.")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Контракт панели кнопок."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from .button_group import ButtonGroupDto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ButtonBarDto(BaseModel):
|
|
9
|
+
"""Панель кнопок сообщения."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
12
|
+
|
|
13
|
+
button_groups: list[ButtonGroupDto] = Field(alias="buttonGroups", description="Ряды кнопок.")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Контракт командной кнопки."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommandButtonDto(BaseModel):
|
|
7
|
+
"""Команда и отображаемый текст кнопки."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
display_text: str = Field(alias="displayText", description="Текст кнопки.")
|
|
12
|
+
command_key: str = Field(alias="commandKey", description="Команда нажатой кнопки.")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from yuchatlib.api.dto.update.message import MessageDto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GetMessagesResponseDto(BaseModel):
|
|
7
|
+
"""Список сообщений, полученных из чата."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
messages: list[MessageDto] = Field(description="Сообщения чата.")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SendMessageResponseDto(BaseModel):
|
|
5
|
+
"""Результат отправки сообщения."""
|
|
6
|
+
|
|
7
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
8
|
+
message_id: str = Field(alias="messageId", description="Идентификатор отправленного сообщения.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Модели получаемых обновлений."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from .message_content import MessageContentDto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessageDto(BaseModel):
|
|
10
|
+
"""Сообщение в чате YuChat."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
13
|
+
workspace_id: str = Field(alias="workspaceId", description="Идентификатор рабочего пространства.")
|
|
14
|
+
chat_id: str = Field(alias="chatId", description="Идентификатор чата.")
|
|
15
|
+
message_id: str = Field(alias="messageId", description="Идентификатор сообщения.")
|
|
16
|
+
message_type: Literal["USER", "SYSTEM"] = Field(alias="messageType", description="Тип сообщения.")
|
|
17
|
+
membership_id: str = Field(alias="membershipId", description="Идентификатор автора в рабочем пространстве.")
|
|
18
|
+
created_at: datetime = Field(alias="createdAt", description="Время создания сообщения.")
|
|
19
|
+
updated_at: datetime = Field(alias="updatedAt", description="Время изменения сообщения.")
|
|
20
|
+
content: MessageContentDto = Field(description="Содержимое сообщения.")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from .pressed_button_command import PressedButtonCommandDto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MessageActionDto(BaseModel):
|
|
7
|
+
"""Действие с кнопкой сообщения."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
10
|
+
workspace_id: str = Field(alias="workspaceId", description="Идентификатор рабочего пространства.")
|
|
11
|
+
chat_id: str = Field(alias="chatId", description="Идентификатор чата.")
|
|
12
|
+
message_id: str = Field(alias="messageId", description="Идентификатор сообщения.")
|
|
13
|
+
pressed_button_command: PressedButtonCommandDto | None = Field(
|
|
14
|
+
default=None, alias="pressedButtonCommand", description="Команда нажатой кнопки, если передана."
|
|
15
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MessageContentDto(BaseModel):
|
|
5
|
+
"""Содержимое сообщения YuChat."""
|
|
6
|
+
|
|
7
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
8
|
+
text: str | None = Field(default=None, description="Текст сообщения в формате Markdown.")
|
|
9
|
+
file_ids: list[str] | None = Field(
|
|
10
|
+
default=None, alias="fileIds", description="Идентификаторы прикреплённых файлов."
|
|
11
|
+
)
|