Compare commits

...

126 Commits

Author SHA1 Message Date
e3c17ab60f Переход на Google Style Docstring 2025-08-06 02:38:34 +03:00
af4c9dc487 Переход на Google Style Docstring 2025-08-06 02:35:33 +03:00
1400f72cd0 Добавлен HandlerException, BaseException -> Exception 2025-08-06 02:34:48 +03:00
e0569de1c5 Добавлен пример на CallbackPayload 2025-08-05 00:54:00 +03:00
ff4575fe84 Добавлен фильтр-обработка CallbackPayload 2025-08-05 00:53:45 +03:00
e922132319 Поправлено преобразование вложений для ответа на callback 2025-08-05 00:53:17 +03:00
b59d97da8a 0.9.5 2025-08-03 14:02:59 +03:00
fe68e41b7a Поправлен импорт GetMessage 2025-08-03 13:58:14 +03:00
50980dfc77 Импорт BaseFilter 2025-08-03 13:57:31 +03:00
036c92d072 Переделана основа для фильтров Command, CommandStart 2025-08-03 13:57:19 +03:00
cb2226eee5 Добавлен BaseFilter 2025-08-03 13:56:56 +03:00
3855f93862 Правки по mypy 2025-08-03 13:56:12 +03:00
01e9cdd2fd Добавлен пример с BaseFilter 2025-08-03 13:55:53 +03:00
338d9c4089 Переделано добавление Middleware 2025-08-03 13:55:31 +03:00
3fa34079ae Доработано преобразование вложений в Pydantic модели 2025-08-02 23:22:36 +03:00
5bc5fb45c8 Мелкие правки 2025-08-02 23:21:57 +03:00
95313ad3dc Переработан метод для получения одного сообщения 2025-07-31 23:38:47 +03:00
42690d24ee Добавлена обработка события dialog_removed 2025-07-31 23:30:30 +03:00
6ad3df5829 0.9.4 2025-07-30 13:29:44 +03:00
622d3a3eb3 Изменена версия aiohttp на 3.12.14 2025-07-30 13:16:42 +03:00
67de8aae1f Правки в тексте 2025-07-30 13:16:14 +03:00
1b0b118239 0.9.3 2025-07-30 09:25:08 +03:00
8d62d0d20a Обновлены примеры использования Middleware 2025-07-30 09:25:00 +03:00
a7173b4371 Добавлены доп. настройки в пример запуска 2025-07-30 08:57:58 +03:00
b25b0c0a5e Переделано оформление 2025-07-30 08:57:41 +03:00
af6b1f1a74 Добавлен PhotoAttachmentRequestPayload для легкого импорта 2025-07-29 22:31:23 +03:00
b1f8fb91cb Поправлена аннотация photo в __init__ ChangeInfo 2025-07-29 22:30:57 +03:00
9c0567d858 Поправлена аннотация photo в change_info 2025-07-29 22:30:10 +03:00
9ab960ebe4 Добавлен метод close_session() 2025-07-29 14:17:35 +03:00
a653ed6792 Изменен фильтр на State. Вместо == теперь in. Работает если в хендлере указано несколько фильтров на State 2025-07-28 15:13:52 +03:00
103535d3ba Объект подписки на Webhook 2025-07-28 01:17:40 +03:00
82dc8d255a Объект ответа подписки на Webhook 2025-07-28 01:16:20 +03:00
35b60a1b44 Объект ответа отписки от Webhook 2025-07-28 01:14:41 +03:00
e07aeef726 Объект ответа подписки на Webhook 2025-07-28 01:14:34 +03:00
03a60014d4 Объект ответа полученных подписок Webhook 2025-07-28 01:14:25 +03:00
b69713f996 Класс на отписку от Webhook 2025-07-28 01:14:07 +03:00
9010c697f4 Класс для подписки на Webhook 2025-07-28 01:13:56 +03:00
b994717f64 Класс для получения подписок Webhook 2025-07-28 01:13:46 +03:00
44dcf39f42 Добавлен API путь /subscriptions 2025-07-28 01:13:12 +03:00
0bd59ddb4a Добавлена проверка на подписки Webhook при start_polling 2025-07-28 01:12:55 +03:00
f497760f5b Добавлены методы: get_subscriptions, subscribe_webhook, unsubscribe_webhook, delete_webhook. Добавлен auto_check_subscriptions к Bot. 2025-07-28 01:12:11 +03:00
5c64c3d040 Правки информации по user_removed 2025-07-27 16:59:12 +03:00
c8dd896691 Переработана система Middleware 2025-07-27 16:58:54 +03:00
e0e7bb53b6 0.9.2 Опциональные fastapi uvicorn 2025-07-27 14:20:08 +03:00
adbea8e55f Поправлены докстринги у Dispatcher и Router 2025-07-27 14:19:42 +03:00
3ff9907a58 0.9.2 fastapi, uvicorn опциональны 2025-07-27 02:15:03 +03:00
6f86d15de4 Добавлен filename к InputMediaBuffer 2025-07-27 02:12:58 +03:00
7ea24fe2af Изменен пример запуска 2025-07-27 02:12:30 +03:00
39fb0c5823 Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted 2025-07-27 02:12:22 +03:00
af84301e4f Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted. Правки с auto_requests 2025-07-27 02:12:16 +03:00
ec432fe8ce Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted 2025-07-27 02:11:48 +03:00
1bfd93f2ea chat_title обязателен 2025-07-27 02:11:30 +03:00
7925087ac7 Добавлено в User @property full_name 2025-07-27 02:11:04 +03:00
7ed540683c Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted 2025-07-27 02:10:34 +03:00
30350c8521 Откат преобразования Recipient, одного из полей может не быть 2025-07-27 02:10:25 +03:00
54683256ce Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted 2025-07-27 02:09:50 +03:00
be7f98976e Поправлена логика задержки отправки сообщения с InputMedia, InputMediaBuffer 2025-07-27 02:09:34 +03:00
54c073ab76 Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted 2025-07-27 02:09:04 +03:00
29b319768b Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted. Изменена система запуска вебхуков 2025-07-27 02:08:45 +03:00
5e98e540ea Изменена система запуска вебхука, fastapi с uvicorn опциональны 2025-07-27 02:08:27 +03:00
1df293f44d Маленькие правки 2025-07-27 02:07:22 +03:00
c667b82a6c Добавлены типы событий: bot_stopped, dialog_cleared, dialog_muted, dialog_unmuted, message_chat_created 2025-07-27 02:07:06 +03:00
62523c1eb2 Правки по преобразованию 2025-07-25 00:54:57 +03:00
b0b7040206 Добавлен аттрибут is_channel 2025-07-25 00:54:35 +03:00
29d3d7c042 Рефактор для получения обновлений 2025-07-25 00:53:35 +03:00
fd048e8544 ignore 2025-07-25 00:53:08 +03:00
32c0ca7647 Правки по ruff + mypy 2025-07-25 00:52:58 +03:00
02b4e2d39a Создан отдельный файл для MemoryContext 2025-07-25 00:52:38 +03:00
354c296fed Правки по ruff + mypy 2025-07-25 00:52:16 +03:00
8f93cf36e4 Удален импорт UpdateUnion 2025-07-25 00:49:20 +03:00
e1064761e4 Поправлены импорты 2025-07-25 00:48:53 +03:00
2420e4232e Добавлена обработка Error 2025-07-24 19:38:08 +03:00
fd9986b02e Доработана загрузка медиа 2025-07-24 19:37:43 +03:00
48b480ff9e Поправлена загрузка медиа 2025-07-24 19:37:30 +03:00
69e6274f42 Перенесены константы из send_message.py и добавлена AFTER_MEDIA_INPUT_DELAY 2025-07-24 19:37:11 +03:00
0daa9d508d Поправлен "спам" общей ошибка таймаута 2025-07-24 19:35:59 +03:00
0ae0758bc1 Добавлен after_input_media_delay в Bot, поправлены типы возвращаемых данных 2025-07-24 19:33:33 +03:00
7d2826c4b5 Правки по mypy 2025-07-24 18:54:41 +03:00
2cd3d64bb8 Небольшая оптимизация 2025-07-24 01:50:25 +03:00
1fb2fbd654 Добавлен файл для будущих исключений MAX 2025-07-24 01:50:06 +03:00
7534ae60a8 Убран лишний импорт 2025-07-24 01:48:03 +03:00
5ad37b8adf Заменен [] на Field(default_factory=list) для списков в pydantic-моделях, чтобы избежать shared mutable default 2025-07-24 01:30:28 +03:00
d731e5e905 0.9.1 2025-07-20 22:56:53 +03:00
7155f974a2 Добавлен README.md для вебхуков 2025-07-20 22:53:19 +03:00
cd85a1b7fb Добавлен README.md для примеров 2025-07-20 22:53:07 +03:00
bdc51a32c5 Изменен блок "Примеры" 2025-07-20 22:52:43 +03:00
c88b5228b7 Добавлена документация по вебхукам 2025-07-20 22:45:08 +03:00
611114ec09 Удален заголовок 2025-07-20 22:44:57 +03:00
52d69904c6 Пример низкоуровневого вебхука 2025-07-20 21:56:54 +03:00
7e9163adc9 0.9.0 2025-07-20 21:48:11 +03:00
6a0406f476 Изменены аннотации attachments 2025-07-20 21:47:31 +03:00
9241917bb1 Изменены аннотации attachments 2025-07-20 21:47:27 +03:00
0354fbc5fd Переработан вебхук 2025-07-20 21:47:22 +03:00
f600decf2b Изменены аннотации attachments 2025-07-20 21:46:47 +03:00
dd6c8ff9ea Пример низклуровневого вебхука 2025-07-20 21:46:14 +03:00
c844d2b2e6 Пример высокоуровневого вебхука 2025-07-20 21:46:06 +03:00
7336b2ebb9 Поправлен Command 2025-07-20 13:08:26 +03:00
85e4c086d4 Клавиатуры и кнопки 2025-07-19 17:29:35 +03:00
964fba7c32 GitIgnore 2025-07-19 17:08:26 +03:00
78976c5393 Лицензия 2025-07-19 17:06:59 +03:00
e8b7c71d25 Поправлены Wiki 2025-07-19 17:03:14 +03:00
aab87e16b2 Поправлен RequestGeoLocationButton 2025-07-19 17:02:59 +03:00
df383665dc Доработан RequestContact 2025-07-19 17:02:47 +03:00
bd06b33343 Добавлен OpenAppButton 2025-07-19 17:02:37 +03:00
7b70d1de18 Добавлен MessageButton 2025-07-19 17:02:21 +03:00
5f2c908da4 Поправлен LinkButton 2025-07-19 17:02:05 +03:00
1abbc16cc8 Изменены импорты 2025-07-19 17:01:50 +03:00
7b61ceaa58 Добавлен .pack() в ButtonsPayload для удобства 2025-07-19 17:01:42 +03:00
37f7907398 Добавлен CommandStart 2025-07-19 17:01:08 +03:00
9dab5f97fb Изменены импорты 2025-07-19 17:00:45 +03:00
0a3d1ca327 Добавлен CommandStart 2025-07-19 17:00:29 +03:00
93043835d1 Добавлены типы кнопок: message, open_app 2025-07-19 17:00:19 +03:00
3548d0558f Добавлено присваивание @property .me для bot 2025-07-19 16:59:57 +03:00
5ae4de6816 Добавлен @property .me (присваивается при запуске бота) 2025-07-19 16:59:25 +03:00
d77288ea07 Пример со всеми кнопками для клавиатуры 2025-07-19 16:58:42 +03:00
30cf778504 0.8.9 2025-07-19 13:43:34 +03:00
dd1bdb5e37 Добавлена загрузка файлов из буфера, InputMediaBuffer 2025-07-19 13:41:03 +03:00
b20a46de24 Добавлена загрузка файлов из буфера, InputMediaBuffer 2025-07-19 13:39:07 +03:00
de05e7931a 0.8.8 2025-07-17 02:05:48 +03:00
a8727c71e9 Обновлен блок "Документация" 2025-07-15 18:32:54 +03:00
b6c11cd28a Добавлена вики 2025-07-15 18:29:54 +03:00
12f64f0805 Поправлены докстринги 2025-07-15 12:11:00 +03:00
3df4dd21b4 Поправлены докстринги 2025-07-15 12:09:31 +03:00
b60d8571d3 backoff 2025-07-04 15:22:58 +03:00
c9a334a615 Небольшая оптимизация 2025-07-04 13:44:00 +03:00
222ca919fc Добавлена заглушка 2025-07-01 21:31:46 +03:00
124 changed files with 4333 additions and 1405 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# Byte-compiled / optimized / DLL files
test
test.py
__pycache__/
*.py[cod]

View File

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

112
README.md
View File

@@ -1,23 +1,53 @@
# Асинхронный MAX API
<p align="center">
<a href="https://github.com/love-apples/maxapi"><img src="https://s.iimg.su/s/29/DCvw4dx2HZgFdTcpqGAs6xdnJnvD44r9zLga2GGe.png" alt="MaxAPI"></a>
</p>
[![PyPI version](https://img.shields.io/pypi/v/maxapi.svg)](https://pypi.org/project/maxapi/)
[![Python Version](https://img.shields.io/pypi/pyversions/maxapi.svg)](https://pypi.org/project/maxapi/)
[![License](https://img.shields.io/github/license/love-apples/maxapi.svg)](https://love-apples/maxapi/blob/main/LICENSE)
---
<p align="center">
<a href='https://github.com/love-apples/maxapi/wiki'>Документация</a>
<a href='https://github.com/love-apples/maxapi/tree/main/examples'>Примеры</a>
<a href='https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8'>MAX Чат</a>
<a href='https://t.me/maxapi_github'>TG Чат</a>
</p>
## 📦 Установка
<p align="center">
<a href='https://pypi.org/project/maxapi/'>
<img src='https://img.shields.io/pypi/v/maxapi.svg' alt='PyPI version'>
</a>
<a href='https://pypi.org/project/maxapi/'>
<img src='https://img.shields.io/pypi/pyversions/maxapi.svg' alt='Python Version'>
</a>
<a href='https://love-apples/maxapi/blob/main/LICENSE'>
<img src='https://img.shields.io/github/license/love-apples/maxapi.svg' alt='License'>
</a>
</p>
## 📦 Установка из PyPi
Стабильная версия
```bash
pip install maxapi
```
---
## 🐱‍👤 Установка из GitHub
Свежая версия, возможны баги. Рекомендуется только для ознакомления с новыми коммитами.
```bash
pip install git+https://github.com/love-apples/maxapi.git
```
## 🚀 Быстрый старт
Если вы тестируете бота в чате - не забудьте дать ему права администратора!
### Запуск Polling
Если у бота установлены подписки на Webhook - события не будут приходить при методе `start_polling`. При таком случае удалите подписки на Webhook через [MasterBot](https://web.max.ru/masterbot) или через `await bot.delete_webhook()` перед `start_polling`.
```python
import asyncio
import logging
@@ -30,7 +60,7 @@ logging.basicConfig(level=logging.INFO)
bot = Bot(ут_ваш_токен')
dp = Dispatcher()
# Ответ бота при нажатии на кнопку "Начать"
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
@@ -38,7 +68,7 @@ async def bot_started(event: BotStarted):
text='Привет! Отправь мне /start'
)
# Ответ бота на команду /start
@dp.message_created(Command('start'))
async def hello(event: MessageCreated):
await event.message.answer(f"Пример чат-бота для MAX 💙")
@@ -52,50 +82,42 @@ if __name__ == '__main__':
asyncio.run(main())
```
---
### Запуск Webhook
## 📚 Документация
Перед запуском бота через Webhook, вам нужно установить дополнительные зависимости (fastapi, uvicorn). Можно это сделать через команду:
```bash
pip install maxapi[webhook]
```
В разработке...
Указан пример простого запуска, для более низкого уровня можете рассмотреть [этот пример](https://github.com/love-apples/maxapi/blob/main/examples/webhook/low_level.py).
```python
import asyncio
import logging
---
from maxapi import Bot, Dispatcher
from maxapi.types import BotStarted, Command, MessageCreated
## ⭐️ Примеры
logging.basicConfig(level=logging.INFO)
- [Эхо бот](https://github.com/love-apples/maxapi/blob/main/examples/echo/main.py)
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py)
- [Обработчики с MagicFilter](https://github.com/love-apples/maxapi/blob/main/examples/magic_filters/main.py)
- [Демонстрация роутинга, InputMedia и механика контекста](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media) (audio.mp3 для команды /media)
- [Получение ID](https://github.com/love-apples/maxapi/tree/main/examples/get_ids/main.py)
- [Миддлварь в хендлерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_in_handlers/main.py)
- [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/main.py)
---
bot = Bot(ут_ваш_токен')
dp = Dispatcher()
## 🧩 Возможности
- ✅ Middleware
- ✅ Роутеры
- ✅ Билдер инлайн клавиатур
- ✅ Простая загрузка медиафайлов
- ✅ MagicFilter
- ✅ Внутренние функции моделей
- ✅ Контекстный менеджер
- ✅ Поллинг
- ✅ Вебхук
- ✅ Логгирование
---
# Команда /start боту
@dp.message_created(Command('start'))
async def hello(event: MessageCreated):
await event.message.answer(f"Привет из вебхука!")
## 💬 Обратная связь и поддержка
async def main():
await dp.handle_webhook(
bot=bot,
host='localhost',
port=8080,
log_level='critical' # Можно убрать для подробного логгирования
)
- MAX: [Чат](https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8)
- Telegram: [@loveappless](https://t.me/loveappless)
- Telegram чат: [MAXApi | Обсуждение](https://t.me/maxapi_github)
---
## 📄 Лицензия
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](https://github.com/love-apples/maxapi/blob/main/LICENSE) для подробностей.
if __name__ == '__main__':
asyncio.run(main())
```

13
examples/README.md Normal file
View File

@@ -0,0 +1,13 @@
## ⭐️ Примеры
- [Эхо бот](https://github.com/love-apples/maxapi/blob/main/examples/echo/main.py)
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py)
- [Обработчики с MagicFilter](https://github.com/love-apples/maxapi/blob/main/examples/magic_filters/main.py)
- [Демонстрация роутинга, InputMedia и механика контекста](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media) (audio.mp3 для команды /media)
- [Получение ID](https://github.com/love-apples/maxapi/tree/main/examples/get_ids/main.py)
- [Миддлварь в хендлерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_in_handlers/main.py)
- [Вебхуки](https://github.com/love-apples/maxapi/tree/main/examples/webhook)
- [Клавиатуры](https://github.com/love-apples/maxapi/tree/main/examples/keyboard/main.py)
- [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/main.py)
- [Свой фильтр на BaseFilter](https://github.com/love-apples/maxapi/tree/main/examples/base_filter/main.py)
- [Фильтр callback payload](https://github.com/love-apples/maxapi/tree/main/examples/callback_payload/main.py)

View File

@@ -0,0 +1,38 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.types import MessageCreated, CommandStart, UpdateUnion
from maxapi.filters import BaseFilter
logging.basicConfig(level=logging.INFO)
bot = Bot(token='тут_ваш_токен')
dp = Dispatcher()
class FilterChat(BaseFilter):
"""
Фильтр, который срабатывает только в чате с названием `Test`
"""
async def __call__(self, event: UpdateUnion):
if not event.chat:
return False
return event.chat == 'Test'
@dp.message_created(CommandStart(), FilterChat())
async def custom_data(event: MessageCreated):
await event.message.answer('Привет!')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,61 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher, F
from maxapi.filters.callback_payload import CallbackPayload
from maxapi.filters.command import CommandStart
from maxapi.types import (
CallbackButton,
MessageCreated,
MessageCallback,
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
class MyPayload(CallbackPayload, prefix='mypayload'):
foo: str
action: str
class AnotherPayload(CallbackPayload, prefix='another'):
bar: str
value: int
@dp.message_created(CommandStart())
async def show_keyboard(event: MessageCreated):
kb = InlineKeyboardBuilder()
kb.row(
CallbackButton(
text='Первая кнопка',
payload=MyPayload(foo='123', action='edit').pack(),
),
CallbackButton(
text='Вторая кнопка',
payload=AnotherPayload(bar='abc', value=42).pack(),
),
)
await event.message.answer('Нажми кнопку!', attachments=[kb.as_markup()])
@dp.message_callback(MyPayload.filter(F.foo == '123'))
async def on_first_callback(event: MessageCallback, payload: MyPayload):
await event.answer(new_text=f'Первая кнопка: foo={payload.foo}, action={payload.action}')
@dp.message_callback(AnotherPayload.filter())
async def on_second_callback(event: MessageCallback, payload: AnotherPayload):
await event.answer(new_text=f'Вторая кнопка: bar={payload.bar}, value={payload.value}')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -13,7 +13,13 @@ from maxapi.types import (
MessageEdited,
MessageRemoved,
UserAdded,
UserRemoved
UserRemoved,
BotStopped,
DialogCleared,
DialogMuted,
DialogUnmuted,
ChatButton,
MessageChatCreated
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
@@ -38,9 +44,9 @@ async def hello(event: MessageCreated):
)
)
builder.add(
CallbackButton(
text='Кнопка 3',
payload='btn_3',
ChatButton(
text='Создать чат',
chat_title='Тест чат'
)
)
@@ -49,12 +55,17 @@ async def hello(event: MessageCreated):
attachments=[
builder.as_markup(),
] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений
) # поэтому она в attachments
@dp.bot_added()
async def bot_added(event: BotAdded):
await event.bot.send_message(
if not event.chat:
logging.info('Не удалось получить chat, возможно отключен auto_requests!')
return
await bot.send_message(
chat_id=event.chat.id,
text=f'Привет чат {event.chat.title}!'
)
@@ -62,7 +73,7 @@ async def bot_added(event: BotAdded):
@dp.message_removed()
async def message_removed(event: MessageRemoved):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat_id,
text='Я всё видел!'
)
@@ -70,7 +81,7 @@ async def message_removed(event: MessageRemoved):
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat_id,
text='Привет! Отправь мне /start'
)
@@ -78,9 +89,9 @@ async def bot_started(event: BotStarted):
@dp.chat_title_changed()
async def chat_title_changed(event: ChatTitleChanged):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat_id,
text=f'Крутое новое название "{event.chat.title}!"'
text=f'Крутое новое название "{event.title}"!'
)
@@ -100,7 +111,14 @@ async def message_edited(event: MessageEdited):
@dp.user_removed()
async def user_removed(event: UserRemoved):
await event.bot.send_message(
if not event.from_user:
return await bot.send_message(
chat_id=event.chat_id,
text=f'Неизвестный кикнул {event.user.first_name} 😢'
)
await bot.send_message(
chat_id=event.chat_id,
text=f'{event.from_user.first_name} кикнул {event.user.first_name} 😢'
)
@@ -108,10 +126,50 @@ async def user_removed(event: UserRemoved):
@dp.user_added()
async def user_added(event: UserAdded):
await event.bot.send_message(
if not event.chat:
return await bot.send_message(
chat_id=event.chat_id,
text=f'Чат приветствует вас, {event.user.first_name}!'
)
await bot.send_message(
chat_id=event.chat_id,
text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!'
)
@dp.bot_stopped()
async def bot_stopped(event: BotStopped):
logging.info(event.from_user.full_name, 'остановил бота') # type: ignore
@dp.dialog_cleared()
async def dialog_cleared(event: DialogCleared):
logging.info(event.from_user.full_name, 'очистил историю чата с ботом') # type: ignore
@dp.dialog_muted()
async def dialog_muted(event: DialogMuted):
logging.info(event.from_user.full_name, 'отключил оповещения от чата бота до ', event.muted_until_datetime) # type: ignore
@dp.dialog_unmuted()
async def dialog_unmuted(event: DialogUnmuted):
logging.info(event.from_user.full_name, 'включил оповещения от чата бота') # type: ignore
@dp.dialog_unmuted()
async def dialog_removed(event: DialogUnmuted):
logging.info(event.from_user.full_name, 'удалил диалог с ботом') # type: ignore
@dp.message_chat_created()
async def message_chat_created(event: MessageChatCreated):
await bot.send_message(
chat_id=event.chat.chat_id,
text=f'Чат создан! Ссылка: {event.chat.link}'
)
async def main():

153
examples/keyboard/main.py Normal file
View File

@@ -0,0 +1,153 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
# Кнопки
from maxapi.types import (
ChatButton,
LinkButton,
CallbackButton,
RequestGeoLocationButton,
MessageButton,
ButtonsPayload, # Для постройки клавиатуры без InlineKeyboardBuilder
RequestContactButton,
OpenAppButton,
)
from maxapi.types import (
MessageCreated,
MessageCallback,
MessageChatCreated,
CommandStart,
Command
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
@dp.message_created(CommandStart())
async def echo(event: MessageCreated):
await event.message.answer(
(
'Привет! Мои команды:\n\n'
'/builder - Клавиатура из InlineKeyboardBuilder\n'
'/pyaload - Клавиатура из pydantic моделей\n'
)
)
@dp.message_created(Command('builder'))
async def builder(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
ChatButton(
text="Создать чат",
chat_title='Test',
chat_description='Test desc'
),
LinkButton(
text="Документация MAX",
url="https://dev.max.ru/docs"
),
)
builder.row(
RequestGeoLocationButton(text="Геолокация"),
MessageButton(text="Сообщение"),
)
builder.row(
RequestContactButton(text="Контакт"),
OpenAppButton(
text="Приложение",
web_app=event.bot.me.username,
contact_id=event.bot.me.user_id
),
)
builder.row(
CallbackButton(
text='Callback',
payload='test',
)
)
await event.message.answer(
text='Клавиатура из InlineKeyboardBuilder',
attachments=[
builder.as_markup()
])
@dp.message_created(Command('payload'))
async def payload(event: MessageCreated):
buttons = [
[
# кнопку типа "chat" убрали из документации,
# возможны баги
ChatButton(
text="Создать чат",
chat_title='Test',
chat_description='Test desc'
),
LinkButton(
text="Документация MAX",
url="https://dev.max.ru/docs"
),
],
[
RequestGeoLocationButton(text="Геолокация"),
MessageButton(text="Сообщение"),
],
[
RequestContactButton(text="Контакт"),
OpenAppButton(
text="Приложение",
web_app=event.bot.me.username,
contact_id=event.bot.me.user_id
),
],
[
CallbackButton(
text='Callback',
payload='test',
)
]
]
buttons_payload = ButtonsPayload(buttons=buttons).pack()
await event.message.answer(
text='Клавиатура из pydantic моделей',
attachments=[
buttons_payload
])
@dp.message_chat_created()
async def message_chat_created(obj: MessageChatCreated):
await obj.bot.send_message(
chat_id=obj.chat.chat_id,
text=f'Чат создан! Ссылка: {obj.chat.link}'
)
@dp.message_callback()
async def message_callback(callback: MessageCallback):
await callback.message.answer('Вы нажали на Callback!')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -1,7 +1,7 @@
import asyncio
import logging
from typing import Any, Dict
from typing import Any, Awaitable, Callable, Dict
from maxapi import Bot, Dispatcher
from maxapi.types import MessageCreated, Command, UpdateUnion
@@ -15,14 +15,15 @@ dp = Dispatcher()
class CustomDataForRouterMiddleware(BaseMiddleware):
async def __call__(
self,
event: UpdateUnion,
data: Dict[str, Any]
):
self,
handler: Callable[[Any, Dict[str, Any]], Awaitable[Any]],
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
data['custom_data'] = f'Это ID того кто вызвал команду: {event.from_user.user_id}'
return data
data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}'
result = await handler(event_object, data)
return result
@dp.message_created(Command('custom_data'))
@@ -31,9 +32,8 @@ async def custom_data(event: MessageCreated, custom_data: str):
async def main():
dp.middlewares = [
CustomDataForRouterMiddleware()
]
dp.middleware(CustomDataForRouterMiddleware())
await dp.start_polling(bot)

View File

@@ -1,12 +1,11 @@
import asyncio
import logging
from typing import Any, Dict
from typing import Any, Awaitable, Callable, Dict
from maxapi import Bot, Dispatcher
from maxapi.filters.middleware import BaseMiddleware
from maxapi.types import MessageCreated, Command, UpdateUnion
from maxapi.types.command import Command
logging.basicConfig(level=logging.INFO)
@@ -16,30 +15,34 @@ dp = Dispatcher()
class CheckChatTitleMiddleware(BaseMiddleware):
async def __call__(
self,
event: UpdateUnion,
):
self,
handler: Callable[[Any, Dict[str, Any]], Awaitable[Any]],
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
return event.chat.title == 'MAXApi'
if event_object.chat.title == 'MAXApi':
return await handler(event_object, data)
class CustomDataMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[Any, Dict[str, Any]], Awaitable[Any]],
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}'
await handler(event_object, data)
@dp.message_created(Command('start'), CheckChatTitleMiddleware())
async def start(event: MessageCreated):
await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!')
class CustomDataMiddleware(BaseMiddleware):
async def __call__(
self,
event: UpdateUnion,
data: Dict[str, Any]
):
data['custom_data'] = f'Это ID того кто вызвал команду: {event.from_user.user_id}'
return data
@dp.message_created(Command('custom_data'), CustomDataMiddleware())
async def custom_data(event: MessageCreated, custom_data: str):
await event.message.answer(custom_data)

View File

@@ -0,0 +1,4 @@
## Вебхуки
- [Высокоуровневый](https://github.com/love-apples/maxapi/tree/main/examples/webhook/high_level.py)
- [Низкоуровневый](https://github.com/love-apples/maxapi/tree/main/examples/webhook/low_level.py)

View File

@@ -0,0 +1,28 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.types import MessageCreated
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
@dp.message_created()
async def handle_message(event: MessageCreated):
await event.message.answer('Бот работает через вебхук!')
async def main():
await dp.handle_webhook(
bot=bot,
host='localhost',
port=8080,
log_level='critical' # Можно убрать для подробного логгирования
)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,63 @@
import asyncio
import logging
try:
from fastapi import Request
from fastapi.responses import JSONResponse
except ImportError:
raise ImportError(
'\n\t Не установлен fastapi!'
'\n\t Выполните команду для установки fastapi: '
'\n\t pip install fastapi>=0.68.0'
'\n\t Или сразу все зависимости для работы вебхука:'
'\n\t pip install maxapi[webhook]'
)
from maxapi import Bot, Dispatcher
from maxapi.methods.types.getted_updates import process_update_webhook
from maxapi.types import MessageCreated
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
@dp.message_created()
async def handle_message(event: MessageCreated):
await event.message.answer('Бот работает через вебхук!')
# Регистрация обработчика
# для вебхука
@dp.webhook_post('/')
async def _(request: Request):
# Сериализация полученного запроса
event_json = await request.json()
# Десериализация полученного запроса
# в pydantic
event_object = await process_update_webhook(
event_json=event_json,
bot=bot
)
# ...свой код
print(f'Информация из вебхука: {event_json}')
# ...свой код
# Окончательная обработка запроса
await dp.handle(event_object)
# Ответ вебхука
return JSONResponse(content={'ok': True}, status_code=200)
async def main():
# Запуск сервера
await dp.init_serve(bot, log_level='critical')
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -3,8 +3,8 @@ from .dispatcher import Dispatcher, Router
from .filters import F
__all__ = [
Bot,
Dispatcher,
F,
Router
'Bot',
'Dispatcher',
'F',
'Router'
]

File diff suppressed because it is too large Load Diff

27
maxapi/client/default.py Normal file
View File

@@ -0,0 +1,27 @@
from aiohttp import ClientTimeout
class DefaultConnectionProperties:
'''
Класс для хранения параметров соединения по умолчанию для aiohttp-клиента.
Args:
timeout (int): Таймаут всего соединения в секундах (по умолчанию 5 * 30).
sock_connect (int): Таймаут установки TCP-соединения в секундах (по умолчанию 30).
**kwargs: Дополнительные параметры, которые будут сохранены как есть.
Attributes:
timeout (ClientTimeout): Экземпляр aiohttp.ClientTimeout с заданными параметрами.
kwargs (dict): Дополнительные параметры.
'''
def __init__(self, timeout: int = 5 * 30, sock_connect: int = 30, **kwargs):
'''
Инициализация параметров соединения.
Args:
timeout (int): Таймаут всего соединения в секундах.
sock_connect (int): Таймаут установки TCP-соединения в секундах.
**kwargs: Дополнительные параметры.
'''
self.timeout = ClientTimeout(total=timeout, sock_connect=sock_connect)
self.kwargs = kwargs

View File

@@ -1,20 +1,25 @@
import os
from __future__ import annotations
from typing import TYPE_CHECKING
import os
import mimetypes
from typing import TYPE_CHECKING, Any, Optional
import aiofiles
import aiohttp
import puremagic
from pydantic import BaseModel
from aiohttp import ClientSession, ClientConnectionError, FormData
from ..exceptions.invalid_token import InvalidToken
from ..exceptions.max import MaxConnection
from ..types.errors import Error
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..enums.upload_type import UploadType
from ..loggers import logger_bot, logger_connection
from ..loggers import logger_bot
if TYPE_CHECKING:
from ..bot import Bot
@@ -25,54 +30,78 @@ class BaseConnection:
"""
Базовый класс для всех методов API.
Содержит общую логику выполнения запроса (например, сериализацию, отправку HTTP-запроса, обработку ответа).
Метод request() может быть переопределён в потомках при необходимости.
Содержит общую логику выполнения запроса (сериализация, отправка HTTP-запроса, обработка ответа).
"""
API_URL = 'https://botapi.max.ru'
RETRY_DELAY = 2
ATTEMPTS_COUNT = 5
AFTER_MEDIA_INPUT_DELAY = 2.0
def __init__(self):
self.bot: 'Bot' = None
self.session: aiohttp.ClientSession = None
def __init__(self) -> None:
"""
Инициализация BaseConnection.
Атрибуты:
bot (Optional[Bot]): Экземпляр бота.
session (Optional[ClientSession]): aiohttp-сессия.
after_input_media_delay (float): Задержка после ввода медиа.
"""
self.bot: Optional[Bot] = None
self.session: Optional[ClientSession] = None
self.after_input_media_delay: float = self.AFTER_MEDIA_INPUT_DELAY
async def request(
self,
method: HTTPMethod,
path: ApiPath,
model: BaseModel = None,
is_return_raw: bool = False,
**kwargs
):
self,
method: HTTPMethod,
path: ApiPath | str,
model: BaseModel | Any = None,
is_return_raw: bool = False,
**kwargs
):
"""
Выполняет HTTP-запрос к API, используя указанные параметры.
Выполняет HTTP-запрос к API.
:param method: HTTP-метод запроса (GET, POST и т.д.)
:param path: Путь к конечной точке API
:param model: Pydantic-модель, в которую будет десериализован ответ (если is_return_raw=False)
:param is_return_raw: Если True — вернуть "сырое" тело ответа, иначе — результат десериализации в model
:param kwargs: Дополнительные параметры (например, query, headers, json)
Args:
method (HTTPMethod): HTTP-метод (GET, POST и т.д.).
path (ApiPath | str): Путь до конечной точки.
model (BaseModel | Any, optional): Pydantic-модель для десериализации ответа, если is_return_raw=False.
is_return_raw (bool, optional): Если True — вернуть сырой ответ, иначе — результат десериализации.
**kwargs: Дополнительные параметры (query, headers, json).
:return:
- Объект model (если is_return_raw=False и model задан)
- dict (если is_return_raw=True)
Returns:
model | dict | Error: Объект модели, dict или ошибка.
Raises:
RuntimeError: Если бот не инициализирован.
MaxConnection: Ошибка соединения.
InvalidToken: Ошибка авторизации (401).
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
if not self.bot.session:
self.bot.session = aiohttp.ClientSession(self.bot.API_URL)
self.bot.session = ClientSession(
base_url=self.bot.API_URL,
timeout=self.bot.default_connection.timeout,
**self.bot.default_connection.kwargs
)
try:
r = await self.bot.session.request(
method=method.value,
url=path.value if isinstance(path, ApiPath) else path,
method=method.value,
url=path.value if isinstance(path, ApiPath) else path,
**kwargs
)
except aiohttp.ClientConnectorDNSError as e:
return logger_connection.error(f'Ошибка при отправке запроса: {e}')
except ClientConnectionError as e:
raise MaxConnection(f'Ошибка при отправке запроса: {e}')
if r.status == 401:
await self.bot.session.close()
raise InvalidToken('Неверный токен!')
if not r.ok:
@@ -80,37 +109,41 @@ class BaseConnection:
error = Error(code=r.status, raw=raw)
logger_bot.error(error)
return error
raw = await r.json()
if is_return_raw: return raw
if is_return_raw:
return raw
model = model(**raw) # type: ignore
model = model(**raw)
if hasattr(model, 'message'):
attr = getattr(model, 'message')
if hasattr(attr, 'bot'):
attr.bot = self.bot
if hasattr(model, 'bot'):
model.bot = self.bot
return model
async def upload_file(
self,
url: str,
path: str,
type: UploadType
self,
url: str,
path: str,
type: UploadType
):
"""
Загружает файл на указанный URL.
Загружает файл на сервер.
:param url: Конечная точка загрузки файла
:param path: Путь к локальному файлу
:param type: Тип файла (video, image, audio, file)
Args:
url (str): URL загрузки.
path (str): Путь к файлу.
type (UploadType): Тип файла.
:return: Сырой .text() ответ от сервера после загрузки файла
Returns:
str: Сырой .text() ответ от сервера.
"""
async with aiofiles.open(path, 'rb') as f:
@@ -119,7 +152,7 @@ class BaseConnection:
basename = os.path.basename(path)
_, ext = os.path.splitext(basename)
form = aiohttp.FormData()
form = FormData()
form.add_field(
name='data',
value=file_data,
@@ -127,39 +160,60 @@ class BaseConnection:
content_type=f"{type.value}/{ext.lstrip('.')}"
)
async with aiohttp.ClientSession() as session:
async with ClientSession() as session:
response = await session.post(
url=url,
url=url,
data=form
)
return await response.text()
async def download_file(
self,
path: str,
url: str,
token: str,
async def upload_file_buffer(
self,
filename: str,
url: str,
buffer: bytes,
type: UploadType
):
"""
Скачивает медиа с указанной ссылки по токену, сохраняя по определенному пути
Загружает файл из буфера.
:param path: Путь сохранения медиа
:param url: Ссылка на медиа
:param token: Токен медиа
Args:
filename (str): Имя файла.
url (str): URL загрузки.
buffer (bytes): Буфер данных.
type (UploadType): Тип файла.
:return: Числовой статус
Returns:
str: Сырой .text() ответ от сервера.
"""
headers = {
'Authorization': f'Bearer {token}'
}
try:
matches = puremagic.magic_string(buffer[:4096])
if matches:
mime_type = matches[0][1]
ext = mimetypes.guess_extension(mime_type) or ''
else:
mime_type = f"{type.value}/*"
ext = ''
except Exception:
mime_type = f"{type.value}/*"
ext = ''
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
async with aiofiles.open(path, 'wb') as f:
await f.write(await response.read())
return response.status
basename = f'{filename}{ext}'
form = FormData()
form.add_field(
name='data',
value=buffer,
filename=basename,
content_type=mime_type
)
async with ClientSession() as session:
response = await session.post(
url=url,
data=form
)
return await response.text()

View File

@@ -1,93 +1,9 @@
import asyncio
from typing import Any, Dict
from ..context.state_machine import State, StatesGroup
from .context import MemoryContext
class MemoryContext:
"""
Контекст хранения данных пользователя с блокировками.
Args:
chat_id (int): Идентификатор чата
user_id (int): Идентификатор пользователя
"""
def __init__(self, chat_id: int, user_id: int):
self.chat_id = chat_id
self.user_id = user_id
self._context: Dict[str, Any] = {}
self._state: State | None = None
self._lock = asyncio.Lock()
async def get_data(self) -> dict[str, Any]:
"""
Возвращает текущий контекст данных.
Returns:
Словарь с данными контекста
"""
async with self._lock:
return self._context
async def set_data(self, data: dict[str, Any]):
"""
Полностью заменяет контекст данных.
Args:
data: Новый словарь контекста
"""
async with self._lock:
self._context = data
async def update_data(self, **kwargs):
"""
Обновляет контекст данных новыми значениями.
Args:
**kwargs: Пары ключ-значение для обновления
"""
async with self._lock:
self._context.update(kwargs)
async def set_state(self, state: State | str = None):
"""
Устанавливает новое состояние.
Args:
state: Новое состояние или None для сброса
"""
async with self._lock:
self._state = state
async def get_state(self):
"""
Возвращает текущее состояние.
Returns:
Текущее состояние или None
"""
async with self._lock:
return self._state
async def clear(self):
"""
Очищает контекст и сбрасывает состояние.
"""
async with self._lock:
self._state = None
self._context = {}
__all__ = [
'State',
'StatesGroup',
'MemoryContext'
]

93
maxapi/context/context.py Normal file
View File

@@ -0,0 +1,93 @@
import asyncio
from typing import Any, Dict, Optional, Union
from ..context.state_machine import State
class MemoryContext:
"""
Контекст хранения данных пользователя с блокировками.
Args:
chat_id (int): Идентификатор чата
user_id (int): Идентификатор пользователя
"""
def __init__(self, chat_id: int, user_id: int):
self.chat_id = chat_id
self.user_id = user_id
self._context: Dict[str, Any] = {}
self._state: State | str | None = None
self._lock = asyncio.Lock()
async def get_data(self) -> dict[str, Any]:
"""
Возвращает текущий контекст данных.
Returns:
Словарь с данными контекста
"""
async with self._lock:
return self._context
async def set_data(self, data: dict[str, Any]):
"""
Полностью заменяет контекст данных.
Args:
data: Новый словарь контекста
"""
async with self._lock:
self._context = data
async def update_data(self, **kwargs):
"""
Обновляет контекст данных новыми значениями.
Args:
**kwargs: Пары ключ-значение для обновления
"""
async with self._lock:
self._context.update(kwargs)
async def set_state(self, state: Optional[Union[State, str]] = None):
"""
Устанавливает новое состояние.
Args:
state: Новое состояние или None для сброса
"""
async with self._lock:
self._state = state
async def get_state(self):
"""
Возвращает текущее состояние.
Returns:
Текущее состояние или None
"""
async with self._lock:
return self._state
async def clear(self):
"""
Очищает контекст и сбрасывает состояние.
"""
async with self._lock:
self._state = None
self._context = {}

View File

@@ -1,4 +1,8 @@
from typing import List
class State:
"""
Представляет отдельное состояние в FSM-группе.
@@ -16,6 +20,7 @@ class State:
class StatesGroup:
"""
Базовый класс для описания группы состояний FSM.
@@ -23,11 +28,13 @@ class StatesGroup:
"""
@classmethod
def states(cls) -> list[str]:
def states(cls) -> List[str]:
"""
Получить список всех состояний в формате 'ИмяКласса:имя_состояния'.
:return: Список строковых представлений состояний
Returns:
Список строковых представлений состояний
"""
return [str(getattr(cls, attr)) for attr in dir(cls)

View File

@@ -2,13 +2,15 @@ from __future__ import annotations
import asyncio
from typing import Any, Callable, Dict, List, TYPE_CHECKING
import functools
from typing import Any, Awaitable, Callable, Dict, List, TYPE_CHECKING, Literal, Optional
from asyncio.exceptions import TimeoutError as AsyncioTimeoutError
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from uvicorn import Config, Server
from aiohttp import ClientConnectorError
from maxapi.exceptions.dispatcher import HandlerException
from .filters.filter import BaseFilter
from .filters.middleware import BaseMiddleware
from .filters.handler import Handler
@@ -16,7 +18,7 @@ from .context import MemoryContext
from .types.updates import UpdateUnion
from .types.errors import Error
from .methods.types.getted_updates import process_update_webhook, process_update_request
from .methods.types.getted_updates import process_update_request, process_update_webhook
from .filters import filter_attrs
@@ -24,35 +26,70 @@ from .bot import Bot
from .enums.update import UpdateType
from .loggers import logger_dp
try:
from fastapi import FastAPI, Request # type: ignore
from fastapi.responses import JSONResponse # type: ignore
FASTAPI_INSTALLED = True
except ImportError:
FASTAPI_INSTALLED = False
try:
from uvicorn import Config, Server # type: ignore
UVICORN_INSTALLED = True
except ImportError:
UVICORN_INSTALLED = False
if TYPE_CHECKING:
from magic_filter import MagicFilter
webhook_app = FastAPI()
CONNECTION_RETRY_DELAY = 30
GET_UPDATES_RETRY_DELAY = 5
class Dispatcher:
"""Основной класс для обработки событий бота.
"""
Основной класс для обработки событий бота.
Обеспечивает работу с вебхуком и поллингом, управляет обработчиками событий.
Обеспечивает запуск поллинга и вебхука, маршрутизацию событий,
применение middleware, фильтров и вызов соответствующих обработчиков.
"""
def __init__(self):
def __init__(self, router_id: str | None = None) -> None:
"""
Инициализация диспетчера.
Args:
router_id (str | None): Идентификатор роутера для логов.
"""
self.router_id = router_id
self.event_handlers: List[Handler] = []
self.contexts: List[MemoryContext] = []
self.routers: List[Router] = []
self.routers: List[Router | Dispatcher] = []
self.filters: List[MagicFilter] = []
self.base_filters: List[BaseFilter] = []
self.middlewares: List[BaseMiddleware] = []
self.bot: Bot = None
self.on_started_func: Callable = None
self.bot: Optional[Bot] = None
self.webhook_app: Optional[FastAPI] = None
self.on_started_func: Optional[Callable] = None
self.polling = False
self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self)
self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self)
self.bot_removed = Event(update_type=UpdateType.BOT_REMOVED, router=self)
self.bot_started = Event(update_type=UpdateType.BOT_STARTED, router=self)
self.bot_stopped = Event(update_type=UpdateType.BOT_STOPPED, router=self)
self.dialog_cleared = Event(update_type=UpdateType.DIALOG_CLEARED, router=self)
self.dialog_muted = Event(update_type=UpdateType.DIALOG_MUTED, router=self)
self.dialog_unmuted = Event(update_type=UpdateType.DIALOG_UNMUTED, router=self)
self.dialog_removed = Event(update_type=UpdateType.DIALOG_REMOVED, router=self)
self.chat_title_changed = Event(update_type=UpdateType.CHAT_TITLE_CHANGED, router=self)
self.message_callback = Event(update_type=UpdateType.MESSAGE_CALLBACK, router=self)
self.message_chat_created = Event(update_type=UpdateType.MESSAGE_CHAT_CREATED, router=self)
@@ -62,34 +99,124 @@ class Dispatcher:
self.user_removed = Event(update_type=UpdateType.USER_REMOVED, router=self)
self.on_started = Event(update_type=UpdateType.ON_STARTED, router=self)
def webhook_post(self, path: str):
def decorator(func):
if self.webhook_app is None:
try:
from fastapi import FastAPI # type: ignore
except ImportError:
raise ImportError(
'\n\t Не установлен fastapi!'
'\n\t Выполните команду для установки fastapi: '
'\n\t pip install fastapi>=0.68.0'
'\n\t Или сразу все зависимости для работы вебхука:'
'\n\t pip install maxapi[webhook]'
)
self.webhook_app = FastAPI()
return self.webhook_app.post(path)(func)
return decorator
async def check_me(self):
"""Проверяет и логирует информацию о боте."""
"""
Проверяет и логирует информацию о боте.
"""
me = await self.bot.get_me()
self.bot._me = me
logger_dp.info(f'Бот: @{me.username} first_name={me.first_name} id={me.user_id}')
def build_middleware_chain(
self,
middlewares: list[BaseMiddleware],
handler: Callable[[Any, dict[str, Any]], Awaitable[Any]]
) -> Callable[[Any, dict[str, Any]], Awaitable[Any]]:
"""
Формирует цепочку вызова middleware вокруг хендлера.
Args:
middlewares (list[BaseMiddleware]): Список middleware.
handler (Callable): Финальный обработчик.
Returns:
Callable: Обёрнутый обработчик.
"""
for mw in reversed(middlewares):
handler = functools.partial(mw, handler)
return handler
def include_routers(self, *routers: 'Router'):
"""Добавляет обработчики из роутеров.
"""
Добавляет указанные роутеры в диспетчер.
Args:
*routers: Роутеры для включения
*routers (Router): Роутеры для добавления.
"""
for router in routers:
self.routers.append(router)
self.routers += [r for r in routers]
def outer_middleware(self, middleware: BaseMiddleware) -> None:
"""
Добавляет Middleware на первое место в списке.
Args:
middleware (BaseMiddleware): Middleware.
"""
self.middlewares.insert(0, middleware)
def middleware(self, middleware: BaseMiddleware) -> None:
"""
Добавляет Middleware в конец списка.
Args:
middleware (BaseMiddleware): Middleware.
"""
self.middlewares.append(middleware)
def filter(self, base_filter: BaseFilter) -> None:
"""
Добавляет фильтр в список.
Args:
base_filter (BaseFilter): Фильтр.
"""
self.base_filters.append(base_filter)
async def __ready(self, bot: Bot):
"""
Подготавливает диспетчер: сохраняет бота, регистрирует обработчики, вызывает on_started.
Args:
bot (Bot): Экземпляр бота.
"""
self.bot = bot
if self.polling and self.bot.auto_check_subscriptions:
response = await self.bot.get_subscriptions()
if response.subscriptions:
logger_subscriptions_text = ', '.join([s.url for s in response.subscriptions])
logger_dp.warning('БОТ ИГНОРИРУЕТ POLLING! Обнаружены установленные подписки: %s', logger_subscriptions_text)
await self.check_me()
self.routers += [self]
handlers_count = 0
for router in self.routers:
for _ in router.event_handlers:
handlers_count += 1
handlers_count = sum(len(router.event_handlers) for router in self.routers)
logger_dp.info(f'{handlers_count} событий на обработку')
@@ -98,14 +225,15 @@ class Dispatcher:
def __get_memory_context(self, chat_id: int, user_id: int):
"""Возвращает или создает контекст для чата и пользователя.
"""
Возвращает существующий или создаёт новый MemoryContext по chat_id и user_id.
Args:
chat_id: ID чата
user_id: ID пользователя
chat_id (int): Идентификатор чата.
user_id (int): Идентификатор пользователя.
Returns:
Существующий или новый контекст
MemoryContext: Контекст.
"""
for ctx in self.contexts:
@@ -115,60 +243,106 @@ class Dispatcher:
new_ctx = MemoryContext(chat_id, user_id)
self.contexts.append(new_ctx)
return new_ctx
async def call_handler(
self,
handler: Callable[[Any, dict[str, Any]], Awaitable[Any]],
event_object: UpdateType,
data: Dict[str, Any]
):
async def process_middlewares(
self,
middlewares: List[BaseMiddleware],
event_object: UpdateUnion,
result_data_kwargs: Dict[str, Any]
):
"""
Вызывает хендлер с нужными аргументами.
Args:
handler: Handler.
event_object: Объект события.
data: Данные для хендлера.
Returns:
None
"""
for middleware in middlewares:
result = await middleware.process_middleware(
event_object=event_object,
result_data_kwargs=result_data_kwargs
)
if result == None or result == False:
return
elif result == True:
result = {}
for key, value in result.items():
result_data_kwargs[key] = value
func_args = handler.func_event.__annotations__.keys()
kwargs_filtered = {k: v for k, v in data.items() if k in func_args}
return result_data_kwargs
if kwargs_filtered:
await handler.func_event(event_object, **kwargs_filtered)
else:
await handler.func_event(event_object)
async def process_base_filters(
self,
event: UpdateUnion,
filters: List[BaseFilter]
) -> Optional[Dict[str, Any]] | Literal[False]:
"""
Асинхронно применяет фильтры к событию.
Args:
event (UpdateUnion): Событие.
filters (List[BaseFilter]): Список фильтров.
Returns:
Optional[Dict[str, Any]] | Literal[False]: Словарь с результатом или False.
"""
data = {}
for _filter in filters:
result = await _filter(event)
if isinstance(result, dict):
data.update(result)
elif not result:
return result
return data
async def handle(self, event_object: UpdateUnion):
"""Обрабатывает событие.
Args:
event_object: Объект события для обработки
"""
Основной обработчик события. Применяет фильтры, middleware и вызывает нужный handler.
Args:
event_object (UpdateUnion): Событие.
"""
try:
ids = event_object.get_ids()
memory_context = self.__get_memory_context(*ids)
current_state = await memory_context.get_state()
kwargs = {'context': memory_context}
router_id = None
process_info = f'{event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}'
is_handled = False
for router in self.routers:
for index, router in enumerate(self.routers):
if is_handled:
break
router_id = router.router_id or index
if router.filters:
if not filter_attrs(event_object, *router.filters):
continue
kwargs = await self.process_middlewares(
middlewares=router.middlewares,
event_object=event_object,
result_data_kwargs=kwargs
result_router_filter = await self.process_base_filters(
event=event_object,
filters=router.base_filters
)
if isinstance(result_router_filter, dict):
kwargs.update(result_router_filter)
elif not result_router_filter:
continue
for handler in router.event_handlers:
if not handler.update_type == event_object.update_type:
@@ -177,58 +351,91 @@ class Dispatcher:
if handler.filters:
if not filter_attrs(event_object, *handler.filters):
continue
if not handler.state == await memory_context.get_state() \
and handler.state:
continue
if handler.states:
if current_state not in handler.states:
continue
func_args = handler.func_event.__annotations__.keys()
if handler.base_filters:
result_filter = await self.process_base_filters(
event=event_object,
filters=handler.base_filters
)
if isinstance(result_filter, dict):
kwargs.update(result_filter)
elif not result_filter:
continue
if isinstance(router, Router):
full_middlewares = self.middlewares + router.middlewares + handler.middlewares
elif isinstance(router, Dispatcher):
full_middlewares = self.middlewares + handler.middlewares
kwargs = await self.process_middlewares(
middlewares=handler.middlewares,
event_object=event_object,
result_data_kwargs=kwargs
handler_chain = self.build_middleware_chain(
full_middlewares,
functools.partial(self.call_handler, handler)
)
if not kwargs:
continue
for key in kwargs.copy().keys():
if not key in func_args:
del kwargs[key]
await handler.func_event(event_object, **kwargs)
kwargs_filtered = {k: v for k, v in kwargs.items() if k in func_args}
try:
await handler_chain(event_object, kwargs_filtered)
except:
raise HandlerException(
handler_title=handler.func_event.__name__,
memory_context={
'data': await memory_context.get_data(),
'state': current_state
}
)
logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
logger_dp.info(f'Обработано: router_id: {router_id} | {process_info}')
is_handled = True
break
if not is_handled:
logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
logger_dp.info(f'Проигнорировано: router_id: {router_id} | {process_info}')
except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]} | {e} ")
logger_dp.error(f"Ошибка при обработке события: router_id: {router_id} | {process_info} | {e} ")
async def start_polling(self, bot: Bot):
"""Запускает поллинг обновлений.
Args:
bot: Экземпляр бота
"""
await self.__ready(bot)
Запускает цикл получения обновлений (long polling).
while True:
Args:
bot (Bot): Экземпляр бота.
"""
self.polling = True
await self.__ready(bot)
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
while self.polling:
try:
events: Dict = await self.bot.get_updates()
except AsyncioTimeoutError:
continue
try:
events = await self.bot.get_updates()
if isinstance(events, Error):
logger_dp.info(f'Ошибка при получении обновлений: {events}')
logger_dp.info(f'Ошибка при получении обновлений: {events}, жду {GET_UPDATES_RETRY_DELAY} секунд')
await asyncio.sleep(GET_UPDATES_RETRY_DELAY)
continue
self.bot.marker_updates = events.get('marker')
processed_events = await process_update_request(
events=events,
bot=self.bot
@@ -236,63 +443,138 @@ class Dispatcher:
for event in processed_events:
await self.handle(event)
except ClientConnectorError:
logger_dp.error(f'Ошибка подключения, жду {CONNECTION_RETRY_DELAY} секунд')
await asyncio.sleep(CONNECTION_RETRY_DELAY)
except Exception as e:
logger_dp.error(f'Общая ошибка при обработке событий: {e}')
logger_dp.error(f'Общая ошибка при обработке событий: {e.__class__} - {e}')
async def handle_webhook(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080):
"""Запускает вебхук сервер.
async def handle_webhook(self, bot: Bot, host: str = 'localhost', port: int = 8080, **kwargs):
"""
Запускает FastAPI-приложение для приёма обновлений через вебхук.
Args:
bot: Экземпляр бота
host: Хост для сервера
port: Порт для сервера
bot (Bot): Экземпляр бота.
host (str): Хост сервера.
port (int): Порт сервера.
"""
await self.__ready(bot)
@webhook_app.post('/')
if not FASTAPI_INSTALLED:
raise ImportError(
'\n\t Не установлен fastapi!'
'\n\t Выполните команду для установки fastapi: '
'\n\t pip install fastapi>=0.68.0'
'\n\t Или сразу все зависимости для работы вебхука:'
'\n\t pip install maxapi[webhook]'
)
elif not UVICORN_INSTALLED:
raise ImportError(
'\n\t Не установлен uvicorn!'
'\n\t Выполните команду для установки uvicorn: '
'\n\t pip install uvicorn>=0.15.0'
'\n\t Или сразу все зависимости для работы вебхука:'
'\n\t pip install maxapi[webhook]'
)
@self.webhook_post('/')
async def _(request: Request):
try:
event_json = await request.json()
event_json = await request.json()
event_object = await process_update_webhook(
event_json=event_json,
bot=bot
)
await self.handle(event_object)
return JSONResponse(content={'ok': True}, status_code=200)
await self.init_serve(
bot=bot,
host=host,
port=port,
**kwargs
)
async def init_serve(self, bot: Bot, host: str = 'localhost', port: int = 8080, **kwargs):
"""
Запускает сервер для обработки вебхуков.
event_object = await process_update_webhook(
event_json=event_json,
bot=self.bot
)
await self.handle(event_object)
return JSONResponse(content={'ok': True}, status_code=200)
except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {event_json['update_type']}: {e}")
config = Config(app=webhook_app, host=host, port=port, log_level="critical")
Args:
bot (Bot): Экземпляр бота.
host (str): Хост.
port (int): Порт.
"""
if not UVICORN_INSTALLED:
raise ImportError(
'\n\t Не установлен uvicorn!'
'\n\t Выполните команду для установки uvicorn: '
'\n\t pip install uvicorn>=0.15.0'
'\n\t Или сразу все зависимости для работы вебхука:'
'\n\t pip install maxapi[webhook]'
)
if self.webhook_app is None:
raise RuntimeError('webhook_app не инициализирован')
config = Config(app=self.webhook_app, host=host, port=port, **kwargs)
server = Server(config)
await self.__ready(bot)
await server.serve()
class Router(Dispatcher):
"""Роутер для группировки обработчиков событий."""
"""
Роутер для группировки обработчиков событий.
"""
def __init__(self):
super().__init__()
def __init__(self, router_id: str | None = None):
"""
Инициализация роутера.
Args:
router_id (str | None): Идентификатор роутера для логов.
"""
super().__init__(router_id)
class Event:
"""Декоратор для регистрации обработчиков событий."""
"""
Декоратор для регистрации обработчиков событий.
"""
def __init__(self, update_type: UpdateType, router: Dispatcher | Router):
"""
Инициализирует событие-декоратор.
Args:
update_type (UpdateType): Тип события.
router (Dispatcher | Router): Экземпляр роутера или диспетчера.
"""
self.update_type = update_type
self.router = router
def __call__(self, *args, **kwargs):
"""
Регистрирует функцию как обработчик события.
Returns:
Callable: Исходная функция.
"""
def decorator(func_event: Callable):
if self.update_type == UpdateType.ON_STARTED:

View File

@@ -19,4 +19,5 @@ class ApiPath(str, Enum):
PIN = '/pin'
MEMBERS = '/members'
ADMINS = '/admins'
UPLOADS = '/uploads'
UPLOADS = '/uploads'
SUBSCRIPTIONS = '/subscriptions'

View File

@@ -16,4 +16,5 @@ class AttachmentType(str, Enum):
STICKER = 'sticker'
CONTACT = 'contact'
INLINE_KEYBOARD = 'inline_keyboard'
LOCATION = 'location'
LOCATION = 'location'
SHARE = 'share'

View File

@@ -14,3 +14,5 @@ class ButtonType(str, Enum):
LINK = 'link'
REQUEST_GEO_LOCATION = 'request_geo_location'
CHAT = 'chat'
MESSAGE = 'message'
OPEN_APP = 'open_app'

View File

@@ -20,6 +20,11 @@ class UpdateType(str, Enum):
MESSAGE_REMOVED = 'message_removed'
USER_ADDED = 'user_added'
USER_REMOVED = 'user_removed'
BOT_STOPPED = 'bot_stopped'
DIALOG_CLEARED = 'dialog_cleared'
DIALOG_MUTED = 'dialog_muted'
DIALOG_UNMUTED = 'dialog_unmuted'
DIALOG_REMOVED = 'dialog_removed'
# Для начинки диспатчера
ON_STARTED = 'on_started'

View File

@@ -0,0 +1,17 @@
class HandlerException(Exception):
def __init__(self, handler_title: str, *args, **kwargs):
self.handler_title = handler_title
self.extra = kwargs
message = f'Обработчик: {handler_title!r}'
if args:
message += f', детали: {args}'
if kwargs:
message += f', другое: {kwargs}'
super().__init__(message)

View File

@@ -1,4 +1,4 @@
class NotAvailableForDownload(BaseException):
class NotAvailableForDownload(Exception):
...

View File

@@ -1,4 +1,4 @@
class InvalidToken(BaseException):
class InvalidToken(Exception):
...

11
maxapi/exceptions/max.py Normal file
View File

@@ -0,0 +1,11 @@
class MaxConnection(Exception):
...
class MaxUploadFileFailed(Exception):
...
class MaxIconParamsException(Exception):
...

View File

@@ -1,17 +1,27 @@
from magic_filter import MagicFilter
from .filter import BaseFilter
F = MagicFilter()
__all__ = [
'BaseFilter'
]
def filter_attrs(obj: object, *filters: MagicFilter) -> bool:
"""
Применяет один или несколько фильтров MagicFilter к объекту.
:param obj: Любой объект с атрибутами (например, event/message)
:param filters: Один или несколько MagicFilter выражений
:return: True, если все фильтры возвращают True, иначе False
Args:
obj (object): Объект, к которому применяются фильтры (например, event или message).
*filters (MagicFilter): Один или несколько выражений MagicFilter.
Returns:
bool: True, если все фильтры возвращают True, иначе False.
"""
try:
return all(f.resolve(obj) for f in filters)
except Exception:
return False
return False

View File

@@ -0,0 +1,175 @@
from __future__ import annotations
from typing import Any, ClassVar, List, Optional, Type, TYPE_CHECKING
from magic_filter import MagicFilter
from pydantic import BaseModel
from ..types.updates.message_callback import MessageCallback
from ..types.updates import UpdateUnion
from .filter import BaseFilter
PAYLOAD_MAX = 1024
class CallbackPayload(BaseModel):
"""
Базовый класс для сериализации/десериализации callback payload.
Атрибуты:
prefix (str): Префикс для payload (используется при pack/unpack) (по умолчанию название класса).
separator (str): Разделитель между значениями (по умолчанию '|').
"""
if TYPE_CHECKING:
prefix: ClassVar[str]
separator: ClassVar[str]
def __init_subclass__(cls, **kwargs: Any) -> None:
"""
Автоматически проставляет prefix и separator при наследовании.
"""
cls.prefix = kwargs.get('prefix', str(cls.__name__))
cls.separator = kwargs.get('separator', '|')
def pack(self) -> str:
"""
Собирает данные payload в строку для передачи в callback payload.
Raises:
ValueError: Если в значении встречается разделитель или payload слишком длинный.
Returns:
str: Сериализованный payload.
"""
values = [self.prefix]
for name in self.attrs():
value = getattr(self, name)
str_value = '' if value is None else str(value)
if self.separator in str_value:
raise ValueError(
f'Символ разделителя "{self.separator}" не должен встречаться в значении поля {name}'
)
values.append(str_value)
data = self.separator.join(values)
if len(data.encode()) > PAYLOAD_MAX:
raise ValueError(
f'Payload слишком длинный! Максимум: {PAYLOAD_MAX} байт'
)
return data
@classmethod
def unpack(cls, data: str):
"""
Десериализует payload из строки.
Args:
data (str): Строка payload (из callback payload).
Raises:
ValueError: Некорректный prefix или количество аргументов.
Returns:
CallbackPayload: Экземпляр payload с заполненными полями.
"""
parts = data.split(cls.separator)
if not parts[0] == cls.prefix:
raise ValueError('Некорректный prefix')
field_names = cls.attrs()
if not len(parts) - 1 == len(field_names):
raise ValueError(
f'Ожидалось {len(field_names)} аргументов, получено {len(parts) - 1}'
)
kwargs = dict(zip(field_names, parts[1:]))
return cls(**kwargs)
@classmethod
def attrs(cls) -> List[str]:
"""
Возвращает список полей для сериализации/десериализации (исключая prefix и separator).
Returns:
List[str]: Имена полей модели.
"""
return [
k for k in cls.model_fields.keys()
if k not in ('prefix', 'separator')
]
@classmethod
def filter(cls, rule: Optional[MagicFilter] = None) -> PayloadFilter:
"""
Создаёт PayloadFilter для фильтрации callback-ивентов по payload.
Args:
rule (Optional[MagicFilter]): Фильтр на payload.
Returns:
PayloadFilter: Экземпляр фильтра для хэндлера.
"""
return PayloadFilter(model=cls, rule=rule)
class PayloadFilter(BaseFilter):
"""
Фильтр для MessageCallback по payload.
"""
def __init__(self, model: Type[CallbackPayload], rule: Optional[MagicFilter]):
"""
Args:
model (Type[CallbackPayload]): Класс payload для распаковки.
rule (Optional[MagicFilter]): Фильтр (условие) для payload.
"""
self.model = model
self.rule = rule
async def __call__(self, event: UpdateUnion):
"""
Проверяет event на MessageCallback и применяет фильтр к payload.
Args:
event (UpdateUnion): Обновление/событие.
Returns:
dict | bool: dict с payload при совпадении, иначе False.
"""
if not isinstance(event, MessageCallback):
return False
if not event.callback.payload:
return False
try:
payload = self.model.unpack(event.callback.payload)
except Exception:
return False
if not self.rule or self.rule.resolve(payload):
return {'payload': payload}
return False

116
maxapi/filters/command.py Normal file
View File

@@ -0,0 +1,116 @@
from typing import List, Tuple
from ..types.updates import UpdateUnion
from ..filters.filter import BaseFilter
from ..types.updates.message_created import MessageCreated
class Command(BaseFilter):
"""
Фильтр сообщений на соответствие команде.
Args:
commands (str | List[str]): Ожидаемая команда или список команд без префикса.
prefix (str, optional): Префикс команды (по умолчанию '/').
check_case (bool, optional): Учитывать регистр при сравнении (по умолчанию False).
Attributes:
commands (List[str]): Список команд без префикса.
prefix (str): Префикс команды.
check_case (bool): Флаг чувствительности к регистру.
"""
def __init__(self, commands: str | List[str], prefix: str = '/', check_case: bool = False):
"""
Инициализация фильтра команд.
"""
if isinstance(commands, str):
self.commands = [commands]
else:
self.commands = commands
self.prefix = prefix
self.check_case = check_case
if not check_case:
self.commands = [cmd.lower() for cmd in self.commands]
def parse_command(self, text: str) -> Tuple[str, List[str]]:
"""
Извлекает команду из текста.
Args:
text (str): Текст сообщения.
Returns:
Optional[str]: Найденная команда с префиксом, либо None.
"""
args = text.split()
first = args[0]
if not first.startswith(self.prefix):
return '', []
return first[len(self.prefix):], args
async def __call__(self, event: UpdateUnion):
"""
Проверяет, соответствует ли сообщение заданной(ым) команде(ам).
Args:
event (MessageCreated): Событие сообщения.
Returns:
bool: True, если команда совпадает, иначе False.
"""
if not isinstance(event, MessageCreated):
return False
text = event.message.body.text
if not text:
return False
parsed_command, args = self.parse_command(text)
if not parsed_command:
return False
if not self.check_case:
if parsed_command.lower() in [commands.lower() for commands in self.commands]:
return {'args': args}
else:
return False
if parsed_command in self.commands:
return {'args': args}
return False
class CommandStart(Command):
"""
Фильтр для команды /start.
Args:
prefix (str, optional): Префикс команды (по умолчанию '/').
check_case (bool, optional): Учитывать регистр (по умолчанию False).
"""
def __init__(self, prefix = '/', check_case = False):
super().__init__(
'start',
prefix,
check_case
)
async def __call__(self, event):
return await super().__call__(event)

21
maxapi/filters/filter.py Normal file
View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..types.updates import UpdateUnion
class BaseFilter:
"""
Базовый класс для фильтров.
Определяет интерфейс фильтрации событий.
Потомки должны переопределять метод __call__.
Methods:
__call__(event): Асинхронная проверка события на соответствие фильтру.
"""
async def __call__(self, event: UpdateUnion) -> bool | dict:
return True

View File

@@ -1,11 +1,10 @@
from typing import Callable, List
from typing import Callable, List, Optional
from magic_filter import F, MagicFilter
from magic_filter import MagicFilter
from ..filters.filter import BaseFilter
from ..filters.middleware import BaseMiddleware
from ..types.command import Command
from ..context.state_machine import State
from ..enums.update import UpdateType
@@ -18,44 +17,43 @@ class Handler:
"""
Обработчик события.
Позволяет связать функцию-обработчик с типом обновления, состоянием и набором фильтров.
Связывает функцию-обработчик с типом события, состояниями и фильтрами.
"""
def __init__(
self,
*args,
func_event: Callable,
update_type: UpdateType,
**kwargs
):
self,
*args,
func_event: Callable,
update_type: UpdateType,
**kwargs
):
"""
Инициализация обработчика.
Создаёт обработчик события.
:param args: Список фильтров и состояний, в том числе:
- MagicFilter — фильтр события,
- State — состояние FSM,
- Command — команда для фильтрации по началу текста сообщения.
:param func_event: Функция-обработчик события
:param update_type: Тип обновления (события), на которое подписан обработчик
:param kwargs: Дополнительные параметры (не используются)
Args:
*args: Список фильтров (MagicFilter, State, Command, BaseFilter, BaseMiddleware).
func_event (Callable): Функция-обработчик.
update_type (UpdateType): Тип обновления.
"""
self.func_event: Callable = func_event
self.update_type: UpdateType = update_type
self.filters = []
self.state: State = None
self.filters: Optional[List[MagicFilter]] = []
self.base_filters: Optional[List[BaseFilter]] = []
self.states: Optional[List[State]] = []
self.middlewares: List[BaseMiddleware] = []
for arg in args:
if isinstance(arg, MagicFilter):
self.filters.append(arg)
elif isinstance(arg, State):
self.state = arg
elif isinstance(arg, Command):
self.filters.insert(0, F.message.body.text.startswith(arg.command))
self.states.append(arg)
elif isinstance(arg, BaseMiddleware):
self.middlewares.append(arg)
elif isinstance(arg, BaseFilter):
self.base_filters.append(arg)
else:
logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при '
f'регистрации функции `{func_event.__name__}`')
logger_dp.info(
f'Неизвестный фильтр `{arg}` при регистрации `{func_event.__name__}`'
)

View File

@@ -1,23 +1,30 @@
from typing import Any, Dict
from ..types.updates import UpdateUnion
from typing import Any, Callable, Awaitable
class BaseMiddleware:
def __init__(self):
...
async def process_middleware(
self,
result_data_kwargs: Dict[str, Any],
event_object: UpdateUnion
):
"""
Базовый класс для мидлварей.
Используется для обработки события до и после вызова хендлера.
"""
async def __call__(
self,
handler: Callable[[Any, dict[str, Any]], Awaitable[Any]],
event_object: Any,
data: dict[str, Any]
) -> Any:
kwargs_temp = {'data': result_data_kwargs.copy()}
"""
Вызывает хендлер с переданным событием и данными.
Args:
handler (Callable): Хендлер события.
event_object (Any): Событие.
data (dict): Дополнительные данные.
Returns:
Any: Результат работы хендлера.
"""
for key in kwargs_temp.copy().keys():
if not key in self.__call__.__annotations__.keys():
del kwargs_temp[key]
result: Dict[str, Any] = await self(event_object, **kwargs_temp)
return result
return await handler(event_object, data)

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from .types.added_admin_chat import AddedListAdminChat
from ..types.users import ChatAdmin
@@ -30,14 +30,14 @@ class AddAdminChat(BaseConnection):
bot: 'Bot',
chat_id: int,
admins: List[ChatAdmin],
marker: int = None
marker: Optional[int] = None
):
self.bot = bot
self.chat_id = chat_id
self.admins = admins
self.marker = marker
async def request(self) -> AddedListAdminChat:
async def fetch(self) -> AddedListAdminChat:
"""
Выполняет HTTP POST запрос для добавления администраторов в чат.
@@ -48,7 +48,10 @@ class AddAdminChat(BaseConnection):
AddedListAdminChat: Результат операции с информацией об успешности.
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['admins'] = [admin.model_dump() for admin in self.admins]
json['marker'] = self.marker

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, Any, Dict, List
from ..methods.types.added_members_chat import AddedMembersChat
@@ -34,7 +34,7 @@ class AddMembersChat(BaseConnection):
self.chat_id = chat_id
self.user_ids = user_ids
async def request(self) -> AddedMembersChat:
async def fetch(self) -> AddedMembersChat:
"""
Отправляет POST-запрос на добавление пользователей в чат.
@@ -45,7 +45,10 @@ class AddMembersChat(BaseConnection):
AddedMembersChat: Результат операции с информацией об успешности добавления.
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['user_ids'] = self.user_ids

View File

@@ -1,4 +1,6 @@
from typing import Any, Dict, List, TYPE_CHECKING
from typing import Any, Dict, List, TYPE_CHECKING, Optional
from ..types.attachments.image import PhotoAttachmentRequestPayload
from ..types.users import User
from ..types.command import BotCommand
@@ -23,16 +25,16 @@ class ChangeInfo(BaseConnection):
name (str, optional): Новое имя бота
description (str, optional): Новое описание
commands (List[BotCommand], optional): Список команд
photo (Dict[str, Any], optional): Данные фото
photo (PhotoAttachmentRequestPayload, optional): Данные фото
"""
def __init__(
self,
bot: 'Bot',
name: str = None,
description: str = None,
commands: List[BotCommand] = None,
photo: Dict[str, Any] = None
name: Optional[str] = None,
description: Optional[str] = None,
commands: Optional[List[BotCommand]] = None,
photo: Optional[PhotoAttachmentRequestPayload] = None
):
self.bot = bot
self.name = name
@@ -40,7 +42,7 @@ class ChangeInfo(BaseConnection):
self.commands = commands
self.photo = photo
async def request(self) -> User:
async def fetch(self) -> User:
"""Отправляет запрос на изменение информации о боте.
@@ -48,12 +50,19 @@ class ChangeInfo(BaseConnection):
User: Объект с обновленными данными бота
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
if self.name: json['name'] = self.name
if self.description: json['description'] = self.description
if self.commands: json['commands'] = [command.model_dump() for command in self.commands]
if self.photo: json['photo'] = self.photo
if self.name:
json['name'] = self.name
if self.description:
json['description'] = self.description
if self.commands:
json['commands'] = [command.model_dump() for command in self.commands]
if self.photo:
json['photo'] = self.photo.model_dump()
return await super().request(
method=HTTPMethod.PATCH,

View File

@@ -30,7 +30,7 @@ class DeleteMeFromMessage(BaseConnection):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> DeletedBotFromChat:
async def fetch(self) -> DeletedBotFromChat:
"""
Отправляет DELETE-запрос для удаления бота из чата.
@@ -39,6 +39,8 @@ class DeleteMeFromMessage(BaseConnection):
DeletedBotFromChat: Результат операции удаления.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ME,

View File

@@ -29,7 +29,7 @@ class DeleteChat(BaseConnection):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> DeletedChat:
async def fetch(self) -> DeletedChat:
"""
Отправляет DELETE-запрос для удаления указанного чата.
@@ -38,6 +38,9 @@ class DeleteChat(BaseConnection):
DeletedChat: Результат операции удаления чата.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.CHATS.value + '/' + str(self.chat_id),

View File

@@ -29,7 +29,7 @@ class DeleteMessage(BaseConnection):
self.bot = bot
self.message_id = message_id
async def request(self) -> DeletedMessage:
async def fetch(self) -> DeletedMessage:
"""
Выполняет DELETE-запрос для удаления сообщения.
@@ -40,6 +40,9 @@ class DeleteMessage(BaseConnection):
DeletedMessage: Результат операции удаления сообщения.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['message_id'] = self.message_id

View File

@@ -19,18 +19,18 @@ class DeletePinMessage(BaseConnection):
Args:
bot (Bot): Экземпляр бота для выполнения запроса.
chat_id (str): Идентификатор чата, из которого нужно удалить закреплённое сообщение.
chat_id (int): Идентификатор чата, из которого нужно удалить закреплённое сообщение.
"""
def __init__(
self,
bot: 'Bot',
chat_id: str,
chat_id: int,
):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> DeletedPinMessage:
async def fetch(self) -> DeletedPinMessage:
"""
Выполняет DELETE-запрос для удаления закреплённого сообщения.
@@ -39,6 +39,9 @@ class DeletePinMessage(BaseConnection):
DeletedPinMessage: Результат операции удаления закреплённого сообщения.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.PIN,

View File

@@ -1,11 +1,5 @@
from typing import TYPE_CHECKING
from ..methods.types.deleted_pin_message import DeletedPinMessage
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..enums.upload_type import UploadType
from ..connection.base import BaseConnection
@@ -36,7 +30,7 @@ class DownloadMedia(BaseConnection):
self.media_url = media_url
self.media_token = media_token
async def request(self) -> int:
async def fetch(self) -> int:
"""
Выполняет GET-запрос для скачивания медиафайла

View File

@@ -1,9 +1,11 @@
from logging import getLogger
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Optional
from collections import Counter
from ..exceptions.max import MaxIconParamsException
from ..types.attachments.image import PhotoAttachmentRequestPayload
from ..types.chats import Chat
@@ -37,10 +39,10 @@ class EditChat(BaseConnection):
self,
bot: 'Bot',
chat_id: int,
icon: PhotoAttachmentRequestPayload = None,
title: str = None,
pin: str = None,
notify: bool = True,
icon: Optional[PhotoAttachmentRequestPayload] = None,
title: Optional[str] = None,
pin: Optional[str] = None,
notify: Optional[bool] = None,
):
self.bot = bot
self.chat_id = chat_id
@@ -49,7 +51,7 @@ class EditChat(BaseConnection):
self.pin = pin
self.notify = notify
async def request(self) -> Chat:
async def fetch(self) -> Chat:
"""
Выполняет PATCH-запрос для обновления параметров чата.
@@ -62,24 +64,31 @@ class EditChat(BaseConnection):
Chat: Обновлённый объект чата.
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
if self.icon:
dump = self.icon.model_dump()
counter = Counter(dump.values())
if not None in counter or \
if None not in counter or \
not counter[None] == 2:
return logger.error(
raise MaxIconParamsException(
'Все атрибуты модели Icon являются взаимоисключающими | '
'https://dev.max.ru/docs-api/methods/PATCH/chats/-chatId-'
)
json['icon'] = dump
if self.title: json['title'] = self.title
if self.pin: json['pin'] = self.pin
if self.notify: json['notify'] = self.notify
if self.title:
json['title'] = self.title
if self.pin:
json['pin'] = self.pin
if self.notify:
json['notify'] = self.notify
return await super().request(
method=HTTPMethod.PATCH,

View File

@@ -1,14 +1,21 @@
from typing import List, TYPE_CHECKING, Optional
from __future__ import annotations
import asyncio
from ..types.errors import Error
from typing import Any, Dict, List, TYPE_CHECKING, Optional
from ..utils.message import process_input_media
from .types.edited_message import EditedMessage
from ..types.message import NewMessageLink
from ..types.attachments.attachment import Attachment
from ..types.input_media import InputMedia, InputMediaBuffer
from ..enums.parse_mode import ParseMode
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
from ..loggers import logger_bot
if TYPE_CHECKING:
@@ -24,7 +31,7 @@ class EditMessage(BaseConnection):
bot (Bot): Экземпляр бота для выполнения запроса.
message_id (str): Идентификатор сообщения для редактирования.
text (str, optional): Новый текст сообщения.
attachments (List[Attachment], optional): Список вложений для сообщения.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений для сообщения.
link (NewMessageLink, optional): Связь с другим сообщением (ответ или пересылка).
notify (bool, optional): Отправлять ли уведомление о сообщении (по умолчанию True).
parse_mode (ParseMode, optional): Формат разметки текста (markdown, html и т.д.).
@@ -32,12 +39,12 @@ class EditMessage(BaseConnection):
def __init__(
self,
bot: 'Bot',
bot: Bot,
message_id: str,
text: str = None,
attachments: List['Attachment'] = None,
link: 'NewMessageLink' = None,
notify: bool = True,
text: Optional[str] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
link: Optional[NewMessageLink] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
self.bot = bot
@@ -48,7 +55,7 @@ class EditMessage(BaseConnection):
self.notify = notify
self.parse_mode = parse_mode
async def request(self) -> EditedMessage:
async def fetch(self) -> Optional[EditedMessage | Error]:
"""
Выполняет PUT-запрос для обновления сообщения.
@@ -59,23 +66,58 @@ class EditMessage(BaseConnection):
EditedMessage: Обновлённое сообщение.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
json = {}
json: Dict[str, Any] = {'attachments': []}
params['message_id'] = self.message_id
if not self.text is None: json['text'] = self.text
if self.attachments: json['attachments'] = \
[att.model_dump() for att in self.attachments]
if not self.link is None: json['link'] = self.link.model_dump()
if not self.notify is None: json['notify'] = self.notify
if not self.parse_mode is None: json['format'] = self.parse_mode.value
if self.text is not None:
json['text'] = self.text
if self.attachments:
for att in self.attachments:
return await super().request(
method=HTTPMethod.PUT,
path=ApiPath.MESSAGES,
model=EditedMessage,
params=params,
json=json
)
if isinstance(att, InputMedia) or isinstance(att, InputMediaBuffer):
input_media = await process_input_media(
base_connection=self,
bot=self.bot,
att=att
)
json['attachments'].append(
input_media.model_dump()
)
else:
json['attachments'].append(att.model_dump())
if self.link is not None:
json['link'] = self.link.model_dump()
if self.notify is not None:
json['notify'] = self.notify
if self.parse_mode is not None:
json['format'] = self.parse_mode.value
await asyncio.sleep(self.bot.after_input_media_delay)
response = None
for attempt in range(self.ATTEMPTS_COUNT):
response = await super().request(
method=HTTPMethod.PUT,
path=ApiPath.MESSAGES,
model=EditedMessage,
params=params,
json=json
)
if isinstance(response, Error):
if response.raw.get('code') == 'attachment.not.ready':
logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду {self.RETRY_DELAY} секунды')
await asyncio.sleep(self.RETRY_DELAY)
continue
return response
return response

View File

@@ -30,7 +30,7 @@ class GetChatById(BaseConnection):
self.bot = bot
self.id = id
async def request(self) -> Chat:
async def fetch(self) -> Chat:
"""
Выполняет GET-запрос для получения данных чата.
@@ -39,6 +39,9 @@ class GetChatById(BaseConnection):
Chat: Объект чата с полной информацией.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS.value + '/' + str(self.id),

View File

@@ -40,7 +40,7 @@ class GetChatByLink(BaseConnection):
if not self.link:
return
async def request(self) -> Chat:
async def fetch(self) -> Chat:
"""
Выполняет GET-запрос для получения данных чата по ссылке.
@@ -49,6 +49,9 @@ class GetChatByLink(BaseConnection):
Chat: Объект с информацией о чате.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS.value + '/' + self.link[-1],

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from ..types.chats import Chats
@@ -32,13 +32,13 @@ class GetChats(BaseConnection):
self,
bot: 'Bot',
count: int = 50,
marker: int = None
marker: Optional[int] = None
):
self.bot = bot
self.count = count
self.marker = marker
async def request(self) -> Chats:
async def fetch(self) -> Chats:
"""
Выполняет GET-запрос для получения списка чатов.
@@ -47,6 +47,9 @@ class GetChats(BaseConnection):
Chats: Объект с данными по списку чатов.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['count'] = self.count

View File

@@ -34,7 +34,7 @@ class GetListAdminChat(BaseConnection):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> GettedListAdminChat:
async def fetch(self) -> GettedListAdminChat:
"""
Выполняет GET-запрос для получения списка администраторов указанного чата.
@@ -43,6 +43,9 @@ class GetListAdminChat(BaseConnection):
GettedListAdminChat: Объект с информацией о администраторах чата.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS.value + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ADMINS,

View File

@@ -24,7 +24,7 @@ class GetMe(BaseConnection):
def __init__(self, bot: 'Bot'):
self.bot = bot
async def request(self) -> User:
async def fetch(self) -> User:
"""
Выполняет GET-запрос для получения данных о боте.
@@ -33,6 +33,9 @@ class GetMe(BaseConnection):
User: Объект пользователя с полной информацией.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.ME,

View File

@@ -34,7 +34,7 @@ class GetMeFromChat(BaseConnection):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> ChatMember:
async def fetch(self) -> ChatMember:
"""
Выполняет GET-запрос для получения информации о боте в указанном чате.
@@ -43,6 +43,9 @@ class GetMeFromChat(BaseConnection):
ChatMember: Информация о боте как участнике чата.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ME,

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, List, Optional
from ..methods.types.getted_members_chat import GettedMembersChat
@@ -27,7 +27,7 @@ class GetMembersChat(BaseConnection):
Attributes:
bot (Bot): Экземпляр бота.
chat_id (int): Идентификатор чата.
user_ids (List[str] | None): Список ID пользователей для фильтра.
user_ids (List[int] | None): Список ID пользователей для фильтра.
marker (int | None): Позиция для пагинации.
count (int | None): Максимальное количество участников.
"""
@@ -36,9 +36,9 @@ class GetMembersChat(BaseConnection):
self,
bot: 'Bot',
chat_id: int,
user_ids: List[str] = None,
marker: int = None,
count: int = None,
user_ids: Optional[List[int]] = None,
marker: Optional[int] = None,
count: Optional[int] = None,
):
self.bot = bot
@@ -47,7 +47,7 @@ class GetMembersChat(BaseConnection):
self.marker = marker
self.count = count
async def request(self) -> GettedMembersChat:
async def fetch(self) -> GettedMembersChat:
"""
Выполняет GET-запрос для получения участников чата с опциональной фильтрацией.
@@ -58,13 +58,18 @@ class GetMembersChat(BaseConnection):
GettedMembersChat: Объект с данными по участникам чата.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
if self.user_ids:
self.user_ids = [str(user_id) for user_id in self.user_ids]
params['user_ids'] = ','.join(self.user_ids)
if self.marker: params['marker'] = self.marker
if self.count: params['marker'] = self.count
params['user_ids'] = ','.join([str(user_id) for user_id in self.user_ids])
if self.marker:
params['marker'] = self.marker
if self.count:
params['marker'] = self.count
return await super().request(
method=HTTPMethod.GET,

View File

@@ -0,0 +1,49 @@
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional, Union
from ..types.message import Message
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetMessage(BaseConnection):
"""
Класс для получения сообщения.
Args:
bot (Bot): Экземпляр бота для выполнения запроса.
message_id (str, optional): ID сообщения (mid), чтобы получить одно сообщение в чате.
"""
def __init__(
self,
bot: 'Bot',
message_id: Optional[str] = None,
):
self.bot = bot
self.message_id = message_id
async def fetch(self) -> Message:
"""
Выполняет GET-запрос для получения сообщения.
Returns:
Message: Объект с полученным сообщением.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.MESSAGES + '/' + self.message_id,
model=Message,
params=self.bot.params
)

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, List, Optional, Union
from ..types.message import Messages
from ..enums.http_method import HTTPMethod
@@ -36,10 +36,10 @@ class GetMessages(BaseConnection):
def __init__(
self,
bot: 'Bot',
chat_id: int,
message_ids: List[str] = None,
from_time: datetime | int = None,
to_time: datetime | int = None,
chat_id: Optional[int] = None,
message_ids: Optional[List[str]] = None,
from_time: Optional[Union[datetime, int]] = None,
to_time: Optional[Union[datetime, int]] = None,
count: int = 50,
):
self.bot = bot
@@ -49,7 +49,7 @@ class GetMessages(BaseConnection):
self.to_time = to_time
self.count = count
async def request(self) -> Messages:
async def fetch(self) -> Messages:
"""
Выполняет GET-запрос для получения сообщений с учётом параметров фильтрации.
@@ -60,9 +60,13 @@ class GetMessages(BaseConnection):
Messages: Объект с полученными сообщениями.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
if self.chat_id: params['chat_id'] = self.chat_id
if self.chat_id:
params['chat_id'] = self.chat_id
if self.message_ids:
params['message_ids'] = ','.join(self.message_ids)

View File

@@ -29,7 +29,7 @@ class GetPinnedMessage(BaseConnection):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> GettedPin:
async def fetch(self) -> GettedPin:
"""
Выполняет GET-запрос для получения закреплённого сообщения.
@@ -38,6 +38,9 @@ class GetPinnedMessage(BaseConnection):
GettedPin: Объект с информацией о закреплённом сообщении.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.PIN,

View File

@@ -0,0 +1,44 @@
from typing import TYPE_CHECKING
from ..methods.types.getted_subscriptions import GettedSubscriptions
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetSubscriptions(BaseConnection):
"""
Если ваш бот получает данные через WebHook, этот класс возвращает список всех подписок.
"""
def __init__(
self,
bot: 'Bot',
):
self.bot = bot
async def fetch(self) -> GettedSubscriptions:
"""
Отправляет запрос на получение списка всех подписок.
Returns:
GettedSubscriptions: Объект со списком подписок
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.SUBSCRIPTIONS,
model=GettedSubscriptions,
params=self.bot.params
)

View File

@@ -1,6 +1,5 @@
from typing import TYPE_CHECKING
from ..types.updates import UpdateUnion
from __future__ import annotations
from typing import TYPE_CHECKING, Dict
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
@@ -28,13 +27,13 @@ class GetUpdates(BaseConnection):
def __init__(
self,
bot: 'Bot',
bot: Bot,
limit: int = 100,
):
self.bot = bot
self.limit = limit
async def request(self) -> UpdateUnion:
async def fetch(self) -> Dict:
"""
Выполняет GET-запрос для получения обновлений с указанным лимитом.
@@ -45,6 +44,9 @@ class GetUpdates(BaseConnection):
UpdateUnion: Объединённый тип данных обновлений.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['limit'] = self.limit

View File

@@ -33,7 +33,7 @@ class GetUploadURL(BaseConnection):
self.bot = bot
self.type = type
async def request(self) -> GettedUploadUrl:
async def fetch(self) -> GettedUploadUrl:
"""
Выполняет POST-запрос для получения URL загрузки файла.
@@ -44,6 +44,9 @@ class GetUploadURL(BaseConnection):
GettedUploadUrl: Результат с URL для загрузки.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['type'] = self.type.value

View File

@@ -30,7 +30,7 @@ class GetVideo(BaseConnection):
self.bot = bot
self.video_token = video_token
async def request(self) -> Video:
async def fetch(self) -> Video:
"""
Выполняет GET-запрос для получения данных видео по токену.
@@ -38,7 +38,10 @@ class GetVideo(BaseConnection):
Returns:
Video: Объект с информацией о видео.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.VIDEOS.value + '/' + self.video_token,

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Optional
from .types.pinned_message import PinnedMessage
@@ -35,14 +35,14 @@ class PinMessage(BaseConnection):
bot: 'Bot',
chat_id: int,
message_id: str,
notify: bool = True
notify: Optional[bool] = None
):
self.bot = bot
self.chat_id = chat_id
self.message_id = message_id
self.notify = notify
async def request(self) -> PinnedMessage:
async def fetch(self) -> PinnedMessage:
"""
Выполняет PUT-запрос для закрепления сообщения в чате.
@@ -53,7 +53,10 @@ class PinMessage(BaseConnection):
PinnedMessage: Объект с информацией о закреплённом сообщении.
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['message_id'] = self.message_id
json['notify'] = self.notify

View File

@@ -38,7 +38,7 @@ class RemoveAdmin(BaseConnection):
self.chat_id = chat_id
self.user_id = user_id
async def request(self) -> RemovedAdmin:
async def fetch(self) -> RemovedAdmin:
"""
Выполняет DELETE-запрос для отмены прав администратора в чате.
@@ -47,10 +47,13 @@ class RemoveAdmin(BaseConnection):
RemovedAdmin: Объект с результатом отмены прав администратора.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.CHATS + '/' + str(self.chat_id) + \
ApiPath.MEMBERS + ApiPath.ADMINS + '/' + str(self.user_id),
model=RemovedAdmin,
params=self.bot.params,
)
)

View File

@@ -43,7 +43,7 @@ class RemoveMemberChat(BaseConnection):
self.user_id = user_id
self.block = block
async def request(self) -> RemovedMemberChat:
async def fetch(self) -> RemovedMemberChat:
"""
Выполняет DELETE-запрос для удаления пользователя из чата.
@@ -54,6 +54,9 @@ class RemoveMemberChat(BaseConnection):
RemovedMemberChat: Результат удаления участника.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['chat_id'] = self.chat_id

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Optional
from ..methods.types.sended_action import SendedAction
@@ -34,14 +34,14 @@ class SendAction(BaseConnection):
def __init__(
self,
bot: 'Bot',
chat_id: int = None,
chat_id: Optional[int] = None,
action: SenderAction = SenderAction.TYPING_ON
):
self.bot = bot
self.chat_id = chat_id
self.action = action
async def request(self) -> SendedAction:
async def fetch(self) -> SendedAction:
"""
Выполняет POST-запрос для отправки действия в указанный чат.
@@ -50,7 +50,10 @@ class SendAction(BaseConnection):
SendedAction: Результат выполнения запроса.
"""
json = {}
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['action'] = self.action.value

View File

@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Optional
from ..methods.types.sended_callback import SendedCallback
@@ -36,15 +36,15 @@ class SendCallback(BaseConnection):
self,
bot: 'Bot',
callback_id: str,
message: Message = None,
notification: str = None
message: Optional[Message] = None,
notification: Optional[str] = None
):
self.bot = bot
self.callback_id = callback_id
self.message = message
self.notification = notification
async def request(self) -> SendedCallback:
async def fetch(self) -> SendedCallback:
"""
Выполняет POST-запрос для отправки callback-ответа.
@@ -55,14 +55,19 @@ class SendCallback(BaseConnection):
SendedCallback: Объект с результатом отправки callback.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['callback_id'] = self.callback_id
json = {}
json: Dict[str, Any] = {}
if self.message: json['message'] = self.message.model_dump()
if self.notification: json['notification'] = self.notification
if self.message:
json['message'] = self.message.model_dump()
if self.notification:
json['notification'] = self.notification
return await super().request(
method=HTTPMethod.POST,

View File

@@ -1,18 +1,16 @@
import asyncio
from typing import List, TYPE_CHECKING, Optional
from typing import Any, Dict, List, TYPE_CHECKING, Optional
from json import loads as json_loads
from ..utils.message import process_input_media
from .types.sended_message import SendedMessage
from ..types.attachments.upload import AttachmentPayload, AttachmentUpload
from ..types.errors import Error
from ..types.message import NewMessageLink
from ..types.input_media import InputMedia
from ..types.input_media import InputMedia, InputMediaBuffer
from ..types.attachments.attachment import Attachment
from ..enums.upload_type import UploadType
from ..enums.parse_mode import ParseMode
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
@@ -25,10 +23,6 @@ if TYPE_CHECKING:
from ..bot import Bot
RETRY_DELAY = 2
ATTEMPTS_COUNT = 5
class SendMessage(BaseConnection):
"""
@@ -39,7 +33,7 @@ class SendMessage(BaseConnection):
chat_id (int, optional): Идентификатор чата, куда отправлять сообщение.
user_id (int, optional): Идентификатор пользователя, если нужно отправить личное сообщение.
text (str, optional): Текст сообщения.
attachments (List[Attachment | InputMedia], optional): Список вложений к сообщению.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений к сообщению.
link (NewMessageLink, optional): Связь с другим сообщением (например, ответ или пересылка).
notify (bool, optional): Отправлять ли уведомление о сообщении. По умолчанию True.
parse_mode (ParseMode, optional): Режим разбора текста (например, Markdown, HTML).
@@ -48,12 +42,12 @@ class SendMessage(BaseConnection):
def __init__(
self,
bot: 'Bot',
chat_id: int = None,
user_id: int = None,
text: str = None,
attachments: List[Attachment | InputMedia] = None,
link: NewMessageLink = None,
notify: bool = True,
chat_id: Optional[int] = None,
user_id: Optional[int] = None,
text: Optional[str] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
link: Optional[NewMessageLink] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
self.bot = bot
@@ -65,52 +59,7 @@ class SendMessage(BaseConnection):
self.notify = notify
self.parse_mode = parse_mode
async def __process_input_media(
self,
att: InputMedia
):
# очень нестабильный метод независящий от модуля
# ждем обновлений MAX API
"""
Загружает файл вложения и формирует объект AttachmentUpload.
Args:
att (InputMedia): Объект вложения для загрузки.
Returns:
AttachmentUpload: Загруженное вложение с токеном.
"""
upload = await self.bot.get_upload_url(att.type)
upload_file_response = await self.upload_file(
url=upload.url,
path=att.path,
type=att.type
)
if att.type in (UploadType.VIDEO, UploadType.AUDIO):
token = upload.token
elif att.type == UploadType.FILE:
json_r = json_loads(upload_file_response)
token = json_r['token']
elif att.type == UploadType.IMAGE:
json_r = json_loads(upload_file_response)
json_r_keys = list(json_r['photos'].keys())
token = json_r['photos'][json_r_keys[0]]['token']
return AttachmentUpload(
type=att.type,
payload=AttachmentPayload(
token=token
)
)
async def request(self) -> SendedMessage:
async def fetch(self) -> Optional[SendedMessage | Error]:
"""
Отправляет сообщение с вложениями (если есть), с обработкой задержки готовности вложений.
@@ -121,33 +70,53 @@ class SendMessage(BaseConnection):
SendedMessage или Error
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
json = {'attachments': []}
json: Dict[str, Any] = {'attachments': []}
if self.chat_id: params['chat_id'] = self.chat_id
elif self.user_id: params['user_id'] = self.user_id
if self.chat_id:
params['chat_id'] = self.chat_id
elif self.user_id:
params['user_id'] = self.user_id
json['text'] = self.text
HAS_INPUT_MEDIA = False
if self.attachments:
for att in self.attachments:
if isinstance(att, InputMedia):
input_media = await self.__process_input_media(att)
if isinstance(att, (InputMedia, InputMediaBuffer)):
HAS_INPUT_MEDIA = True
input_media = await process_input_media(
base_connection=self,
bot=self.bot,
att=att
)
json['attachments'].append(
input_media.model_dump()
)
else:
json['attachments'].append(att.model_dump())
if not self.link is None: json['link'] = self.link.model_dump()
if self.link is not None:
json['link'] = self.link.model_dump()
json['notify'] = self.notify
if not self.parse_mode is None: json['format'] = self.parse_mode.value
if self.parse_mode is not None:
json['format'] = self.parse_mode.value
if HAS_INPUT_MEDIA:
await asyncio.sleep(self.bot.after_input_media_delay)
response = None
for attempt in range(ATTEMPTS_COUNT):
for attempt in range(self.ATTEMPTS_COUNT):
response = await super().request(
method=HTTPMethod.POST,
path=ApiPath.MESSAGES,
@@ -158,8 +127,8 @@ class SendMessage(BaseConnection):
if isinstance(response, Error):
if response.raw.get('code') == 'attachment.not.ready':
logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду {RETRY_DELAY} секунды')
await asyncio.sleep(RETRY_DELAY)
logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду {self.RETRY_DELAY} секунды')
await asyncio.sleep(self.RETRY_DELAY)
continue
return response

View File

@@ -0,0 +1,70 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from ..methods.types.subscribed import Subscribed
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..enums.update import UpdateType
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class SubscribeWebhook(BaseConnection):
"""
Подписывает бота на получение обновлений через WebHook.
После вызова этого метода бот будет получать уведомления о новых событиях в чатах на указанный URL.
Ваш сервер должен прослушивать один из следующих портов: `80`, `8080`, `443`, `8443`, `16384`-`32383`.
Args:
bot (Bot): Экземпляр бота для выполнения запроса.
url (str): URL HTTP(S)-эндпойнта вашего бота. Должен начинаться с http(s)://
update_types (Optional[List[str]]): Список типов обновлений, которые ваш бот хочет получать. Для полного списка типов см. объект
secret (str): От 5 до 256 символов. Cекрет, который должен быть отправлен в заголовке X-Max-Bot-Api-Secret в каждом запросе Webhook. Разрешены только символы A-Z, a-z, 0-9, и дефис. Заголовок рекомендован, чтобы запрос поступал из установленного веб-узла
"""
def __init__(
self,
bot: 'Bot',
url: str,
update_types: Optional[List[UpdateType]] = None,
secret: Optional[str] = None
):
self.bot = bot
self.url = url
self.update_types = update_types
self.secret = secret
async def fetch(self) -> Subscribed:
"""
Отправляет запрос на подписку бота на получение обновлений через WebHook
Returns:
Subscribed: Объект с информацией об операции
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['url'] = self.url
if self.update_types:
json['update_types'] = self.update_types
if self.secret:
json['secret'] = self.secret
return await super().request(
method=HTTPMethod.POST,
path=ApiPath.SUBSCRIPTIONS,
model=Subscribed,
params=self.bot.params,
json=json
)

View File

@@ -0,0 +1,16 @@
from typing import List
from pydantic import BaseModel
from ...types.subscription import Subscription
class GettedSubscriptions(BaseModel):
"""
Ответ API с отправленным сообщением.
Attributes:
message (Message): Объект отправленного сообщения.
"""
subscriptions: List[Subscription]

View File

@@ -1,9 +1,12 @@
from typing import TYPE_CHECKING
from ...utils.updates import enrich_event
from ...enums.update import UpdateType
from ...types.updates.bot_added import BotAdded
from ...types.updates.bot_removed import BotRemoved
from ...types.updates.bot_started import BotStarted
from ...types.updates.bot_stopped import BotStopped
from ...types.updates.chat_title_changed import ChatTitleChanged
from ...types.updates.message_callback import MessageCallback
from ...types.updates.message_chat_created import MessageChatCreated
@@ -12,125 +15,56 @@ from ...types.updates.message_edited import MessageEdited
from ...types.updates.message_removed import MessageRemoved
from ...types.updates.user_added import UserAdded
from ...types.updates.user_removed import UserRemoved
from ...types.updates.dialog_cleared import DialogCleared
from ...types.updates.dialog_muted import DialogMuted
from ...types.updates.dialog_unmuted import DialogUnmuted
from ...types.updates.dialog_removed import DialogRemoved
if TYPE_CHECKING:
from ...bot import Bot
UPDATE_MODEL_MAPPING = {
UpdateType.BOT_ADDED: BotAdded,
UpdateType.BOT_REMOVED: BotRemoved,
UpdateType.BOT_STARTED: BotStarted,
UpdateType.CHAT_TITLE_CHANGED: ChatTitleChanged,
UpdateType.MESSAGE_CALLBACK: MessageCallback,
UpdateType.MESSAGE_CHAT_CREATED: MessageChatCreated,
UpdateType.MESSAGE_CREATED: MessageCreated,
UpdateType.MESSAGE_EDITED: MessageEdited,
UpdateType.MESSAGE_REMOVED: MessageRemoved,
UpdateType.USER_ADDED: UserAdded,
UpdateType.USER_REMOVED: UserRemoved,
UpdateType.BOT_STOPPED: BotStopped,
UpdateType.DIALOG_CLEARED: DialogCleared,
UpdateType.DIALOG_MUTED: DialogMuted,
UpdateType.DIALOG_UNMUTED: DialogUnmuted,
UpdateType.DIALOG_REMOVED: DialogRemoved
}
async def get_update_model(event: dict, bot: 'Bot'):
event_object = None
match event['update_type']:
case UpdateType.BOT_ADDED:
event_object = BotAdded(**event)
case UpdateType.BOT_REMOVED:
event_object = BotRemoved(**event)
case UpdateType.BOT_STARTED:
event_object = BotStarted(**event)
update_type = event['update_type']
model_cls = UPDATE_MODEL_MAPPING.get(update_type)
case UpdateType.CHAT_TITLE_CHANGED:
event_object = ChatTitleChanged(**event)
case UpdateType.MESSAGE_CALLBACK:
event_object = MessageCallback(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.callback.user
case UpdateType.MESSAGE_CHAT_CREATED:
event_object = MessageChatCreated(**event)
event_object.chat = event_object.chat
case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.message.sender
case UpdateType.MESSAGE_EDITED:
event_object = MessageEdited(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.message.sender
case UpdateType.MESSAGE_REMOVED:
event_object = MessageRemoved(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id,
user_id=event_object.user_id
) if bot.auto_requests else None
case UpdateType.USER_ADDED:
event_object = UserAdded(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.user
case UpdateType.USER_REMOVED:
event_object = UserRemoved(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id,
user_id=event_object.admin_id
) if event_object.admin_id and \
bot.auto_requests else None
if event['update_type'] in (UpdateType.BOT_ADDED,
UpdateType.BOT_REMOVED,
UpdateType.BOT_STARTED,
UpdateType.CHAT_TITLE_CHANGED):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
if not model_cls:
raise ValueError(f'Unknown update type: {update_type}')
event_object.from_user = event_object.user
if hasattr(event_object, 'bot'):
event_object.bot = bot
if hasattr(event_object, 'message'):
event_object.message.bot = bot
for attachment in event_object.message.body.attachments:
if hasattr(attachment, 'bot'):
attachment.bot = bot
event_object = await enrich_event(
event_object=model_cls(**event),
bot=bot
)
return event_object
async def process_update_request(events: dict, bot: 'Bot'):
events = [event for event in events['updates']]
objects = []
for event in events:
objects.append(
await get_update_model(
bot=bot,
event=event
)
)
return objects
return [
await get_update_model(event, bot)
for event in events['updates']
]
async def process_update_webhook(event_json: dict, bot: 'Bot'):

View File

@@ -3,5 +3,5 @@ from pydantic import BaseModel
class GettedUploadUrl(BaseModel):
url: Optional[str] = None
url: str
token: Optional[str] = None

View File

@@ -21,4 +21,4 @@ class SendedCallback(BaseModel):
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
bot: Optional[Bot] # type: ignore

View File

@@ -0,0 +1,16 @@
from typing import Optional
from pydantic import BaseModel
class Subscribed(BaseModel):
"""
Результат подписки на обновления на Webhook
Attributes:
success (bool): Статус успешности операции.
message (Optional[str]): Дополнительное сообщение или ошибка.
"""
success: bool
message: Optional[str] = None

View File

@@ -0,0 +1,16 @@
from typing import Optional
from pydantic import BaseModel
class Unsubscribed(BaseModel):
"""
Результат отписки от обновлений на Webhook
Attributes:
success (bool): Статус успешности операции.
message (Optional[str]): Дополнительное сообщение или ошибка.
"""
success: bool
message: Optional[str] = None

View File

@@ -0,0 +1,54 @@
from typing import TYPE_CHECKING
from ..methods.types.unsubscribed import Unsubscribed
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class UnsubscribeWebhook(BaseConnection):
"""
Отписывает бота от получения обновлений через WebHook. После вызова этого метода бот перестает получать уведомления о новых событиях, и доступна доставка уведомлений через API с длительным опросом.
Args:
bot (Bot): Экземпляр бота для выполнения запроса.
url (str): URL, который нужно удалить из подписок на WebHook
"""
def __init__(
self,
bot: 'Bot',
url: str,
):
self.bot = bot
self.url = url
async def fetch(self) -> Unsubscribed:
"""
Отправляет запрос на подписку бота на получение обновлений через WebHook
Returns:
Unsubscribed: Объект с информацией об операции
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['url'] = self.url
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.SUBSCRIPTIONS,
model=Unsubscribed,
params=params,
)

View File

@@ -9,8 +9,13 @@ from ..types.updates.message_edited import MessageEdited
from ..types.updates.message_removed import MessageRemoved
from ..types.updates.user_added import UserAdded
from ..types.updates.user_removed import UserRemoved
from ..types.updates.bot_stopped import BotStopped
from ..types.updates.dialog_cleared import DialogCleared
from ..types.updates.dialog_muted import DialogMuted
from ..types.updates.dialog_unmuted import DialogUnmuted
from ..types.updates import UpdateUnion
from ..types.attachments.attachment import Attachment
from ..types.attachments.attachment import PhotoAttachmentPayload
from ..types.attachments.attachment import OtherAttachmentPayload
from ..types.attachments.attachment import ContactAttachmentPayload
@@ -19,38 +24,55 @@ from ..types.attachments.attachment import StickerAttachmentPayload
from ..types.attachments.buttons.callback_button import CallbackButton
from ..types.attachments.buttons.chat_button import ChatButton
from ..types.attachments.buttons.link_button import LinkButton
from ..types.attachments.buttons.request_contact import RequestContact
from ..types.attachments.buttons.request_contact import RequestContactButton
from ..types.attachments.buttons.open_app_button import OpenAppButton
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ..types.message import Message
from ..types.attachments.buttons.message_button import MessageButton
from ..types.attachments.image import PhotoAttachmentRequestPayload
from ..types.message import Message, NewMessageLink
from ..types.command import Command, BotCommand
from ..filters.command import Command, CommandStart
from ..types.command import BotCommand
from .input_media import InputMedia
from .input_media import InputMediaBuffer
__all__ = [
UpdateUnion,
InputMedia,
BotCommand,
CallbackButton,
ChatButton,
LinkButton,
RequestContact,
RequestGeoLocationButton,
Command,
PhotoAttachmentPayload,
OtherAttachmentPayload,
ContactAttachmentPayload,
ButtonsPayload,
StickerAttachmentPayload,
BotAdded,
BotRemoved,
BotStarted,
ChatTitleChanged,
MessageCallback,
MessageChatCreated,
MessageCreated,
MessageEdited,
MessageRemoved,
UserAdded,
UserRemoved
]
'NewMessageLink',
'PhotoAttachmentRequestPayload',
'DialogUnmuted',
'DialogMuted',
'DialogCleared',
'BotStopped',
'CommandStart',
'OpenAppButton',
'Message',
'Attachment',
'InputMediaBuffer',
'MessageButton',
'UpdateUnion',
'InputMedia',
'BotCommand',
'CallbackButton',
'ChatButton',
'LinkButton',
'RequestContactButton',
'RequestGeoLocationButton',
'Command',
'PhotoAttachmentPayload',
'OtherAttachmentPayload',
'ContactAttachmentPayload',
'ButtonsPayload',
'StickerAttachmentPayload',
'BotAdded',
'BotRemoved',
'BotStarted',
'ChatTitleChanged',
'MessageCallback',
'MessageChatCreated',
'MessageCreated',
'MessageEdited',
'MessageRemoved',
'UserAdded',
'UserRemoved',
]

View File

@@ -0,0 +1,25 @@
from typing import Annotated, Union
from pydantic import Field
from ..attachments.share import Share
from ..attachments.buttons.attachment_button import AttachmentButton
from ..attachments.sticker import Sticker
from ..attachments.file import File
from ..attachments.image import Image
from ..attachments.video import Video
from ..attachments.audio import Audio
from ..attachments.location import Location
from ..attachments.contact import Contact
Attachments = Annotated[Union[
Audio,
Video,
File,
Image,
Sticker,
Share,
Location,
AttachmentButton,
Contact
], Field(discriminator='type')]

View File

@@ -1,8 +1,6 @@
from typing import TYPE_CHECKING, Any, List, Optional, Union
from pydantic import BaseModel, Field
from ...exceptions.download_file import NotAvailableForDownload
from ...types.attachments.upload import AttachmentUpload
from ...types.attachments.buttons import InlineButtonUnion
from ...types.users import User
@@ -82,6 +80,12 @@ class ButtonsPayload(BaseModel):
"""
buttons: List[List[InlineButtonUnion]]
def pack(self):
return Attachment(
type=AttachmentType.INLINE_KEYBOARD,
payload=self
)
class Attachment(BaseModel):
@@ -106,33 +110,7 @@ class Attachment(BaseModel):
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
bot: Optional[Bot] # type: ignore
class Config:
use_enum_values = True
async def download(
self,
path: str
):
"""
Скачивает медиа, сохраняя по определенному пути
:param path: Путь сохранения медиа
:return: Числовой статус
"""
if not hasattr(self.payload, 'token') or \
not hasattr(self.payload, 'url'):
raise NotAvailableForDownload()
elif not self.payload.token or not self.payload.url:
raise NotAvailableForDownload(f'Медиа типа `{self.type}` недоступно для скачивания')
return await self.bot.download_file(
path=path,
url=self.payload.url,
token=self.payload.token,
)
use_enum_values = True

View File

@@ -1,5 +1,7 @@
from typing import Literal, Optional
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -9,9 +11,8 @@ class Audio(Attachment):
Вложение с типом аудио.
Attributes:
type (Literal['audio']): Тип вложения, всегда 'audio'.
transcription (Optional[str]): Транскрипция аудио (если есть).
"""
type: Literal['audio'] = 'audio'
type: Literal[AttachmentType.AUDIO]
transcription: Optional[str] = None

View File

@@ -3,13 +3,17 @@ from typing import Union
from .callback_button import CallbackButton
from .chat_button import ChatButton
from .link_button import LinkButton
from .request_contact import RequestContact
from .request_contact import RequestContactButton
from .request_geo_location_button import RequestGeoLocationButton
from .message_button import MessageButton
from .open_app_button import OpenAppButton
InlineButtonUnion = Union[
CallbackButton,
ChatButton,
LinkButton,
RequestContact,
RequestGeoLocationButton
RequestContactButton,
RequestGeoLocationButton,
MessageButton,
OpenAppButton
]

View File

@@ -1,10 +1,11 @@
from typing import Literal
from pydantic import BaseModel
from ..attachment import ButtonsPayload
from ....enums.attachment import AttachmentType
from ..attachment import Attachment
class AttachmentButton(BaseModel):
class AttachmentButton(Attachment):
"""
Модель кнопки вложения для сообщения.
@@ -14,5 +15,4 @@ class AttachmentButton(BaseModel):
payload: Полезная нагрузка кнопки (массив рядов кнопок)
"""
type: Literal['inline_keyboard'] = 'inline_keyboard'
payload: ButtonsPayload
type: Literal[AttachmentType.INLINE_KEYBOARD]

View File

@@ -17,8 +17,7 @@ class ChatButton(Button):
"""
type: ButtonType = ButtonType.CHAT
chat_title: Optional[str] = None
chat_title: str
chat_description: Optional[str] = None
start_payload: Optional[str] = None
chat_title: Optional[str] = None
uuid: Optional[int] = None
uuid: Optional[int] = None

View File

@@ -1,5 +1,7 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button
@@ -12,4 +14,5 @@ class LinkButton(Button):
url: Ссылка для перехода (должна содержать http/https)
"""
type: ButtonType = ButtonType.LINK
url: Optional[str] = None

View File

@@ -0,0 +1,17 @@
from ....enums.button_type import ButtonType
from .button import Button
class MessageButton(Button):
"""
Кнопка для отправки текста
Attributes:
type: Тип кнопки (определяет её поведение и функционал)
text: Отправляемый текст
"""
type: ButtonType = ButtonType.MESSAGE
text: str

View File

@@ -0,0 +1,22 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button
class OpenAppButton(Button):
"""
Кнопка для открытия приложения
Attributes:
text: Видимый текст кнопки
web_app: Публичное имя (username) бота или ссылка на него, чьё мини-приложение надо запустить
contact_id: Идентификатор бота, чьё мини-приложение надо запустить
"""
type: ButtonType = ButtonType.OPEN_APP
text: str
web_app: Optional[str] = None
contact_id: Optional[int] = None

View File

@@ -1,8 +1,16 @@
from ....enums.button_type import ButtonType
from .button import Button
class RequestContact(Button):
class RequestContactButton(Button):
"""
Кнопка с контактом.
Кнопка с контактом
Args:
text: Текст кнопки
"""
...
type: ButtonType = ButtonType.REQUEST_CONTACT
text: str

View File

@@ -1,13 +1,17 @@
from ....enums.button_type import ButtonType
from .button import Button
class RequestGeoLocationButton(Button):
"""Кнопка запроса геолокации пользователя.
"""
Кнопка запроса геолокации пользователя.
Attributes:
quick: Если True, запрашивает геолокацию без дополнительного
подтверждения пользователя (по умолчанию False)
"""
type: ButtonType = ButtonType.REQUEST_GEO_LOCATION
quick: bool = False

View File

@@ -1,4 +1,5 @@
from typing import Literal
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -7,9 +8,6 @@ class Contact(Attachment):
"""
Вложение с типом контакта.
Attributes:
type (Literal['contact']): Тип вложения, всегда 'contact'.
"""
type: Literal['contact'] = 'contact'
type: Literal[AttachmentType.CONTACT]

View File

@@ -1,5 +1,7 @@
from typing import Literal, Optional
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -9,11 +11,10 @@ class File(Attachment):
Вложение с типом файла.
Attributes:
type (Literal['file']): Тип вложения, всегда 'file'.
filename (Optional[str]): Имя файла.
size (Optional[int]): Размер файла в байтах.
"""
type: Literal['file'] = 'file'
type: Literal[AttachmentType.FILE]
filename: Optional[str] = None
size: Optional[int] = None

View File

@@ -1,7 +1,9 @@
from typing import Literal, Optional
from pydantic import BaseModel
from .attachment import Attachment
from ...enums.attachment import AttachmentType
class PhotoAttachmentRequestPayload(BaseModel):
@@ -29,4 +31,4 @@ class Image(Attachment):
type (Literal['image']): Тип вложения, всегда 'image'.
"""
type: Literal['image'] = 'image'
type: Literal[AttachmentType.IMAGE]

View File

@@ -1,5 +1,7 @@
from typing import Literal, Optional
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -9,11 +11,10 @@ class Location(Attachment):
Вложение с типом геолокации.
Attributes:
type (Literal['location']): Тип вложения, всегда 'location'.
latitude (Optional[float]): Широта.
longitude (Optional[float]): Долгота.
"""
type: Literal['location'] = 'location'
type: Literal[AttachmentType.LOCATION]
latitude: Optional[float] = None
longitude: Optional[float] = None

View File

@@ -1,5 +1,7 @@
from typing import Literal, Optional
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -9,13 +11,12 @@ class Share(Attachment):
Вложение с типом "share" (поделиться).
Attributes:
type (Literal['share']): Тип вложения, всегда 'share'.
title (Optional[str]): Заголовок для шаринга.
description (Optional[str]): Описание.
image_url (Optional[str]): URL изображения для предпросмотра.
"""
type: Literal['share'] = 'share'
type: Literal[AttachmentType.SHARE]
title: Optional[str] = None
description: Optional[str] = None
image_url: Optional[str] = None

View File

@@ -1,5 +1,7 @@
from typing import Literal, Optional
from ...enums.attachment import AttachmentType
from .attachment import Attachment
@@ -9,11 +11,10 @@ class Sticker(Attachment):
Вложение с типом стикера.
Attributes:
type (Literal['sticker']): Тип вложения, всегда 'sticker'.
width (Optional[int]): Ширина стикера в пикселях.
height (Optional[int]): Высота стикера в пикселях.
"""
type: Literal['sticker'] = 'sticker'
type: Literal[AttachmentType.STICKER]
width: Optional[int] = None
height: Optional[int] = None

View File

@@ -1,6 +1,8 @@
from typing import TYPE_CHECKING, Any, Literal, Optional
from pydantic import BaseModel, Field
from ...enums.attachment import AttachmentType
from .attachment import Attachment
if TYPE_CHECKING:
@@ -49,7 +51,6 @@ class Video(Attachment):
Вложение с типом видео.
Attributes:
type (Optional[Literal['video']]): Тип вложения, всегда 'video'.
token (Optional[str]): Токен видео.
urls (Optional[VideoUrl]): URLs видео разных разрешений.
thumbnail (VideoThumbnail): Миниатюра видео.
@@ -59,7 +60,7 @@ class Video(Attachment):
bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется.
"""
type: Optional[Literal['video']] = 'video'
type: Literal[AttachmentType.VIDEO]
token: Optional[str] = None
urls: Optional[VideoUrl] = None
thumbnail: VideoThumbnail
@@ -69,4 +70,4 @@ class Video(Attachment):
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional['Bot']
bot: Optional['Bot'] # type: ignore

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, field_validator
from pydantic import BaseModel, Field, field_validator
from typing import Dict, List, Optional
from datetime import datetime
@@ -96,7 +96,7 @@ class Chats(BaseModel):
marker (Optional[int]): Маркер для пагинации. Может быть None.
"""
chats: List[Chat] = []
chats: List[Chat] = Field(default_factory=list)
marker: Optional[int] = None

View File

@@ -1,32 +1,5 @@
from typing import Optional
from pydantic import BaseModel
class Command:
"""
Класс для представления команды бота.
Attributes:
text (str): Текст команды без префикса.
prefix (str): Префикс команды. По умолчанию '/'.
"""
def __init__(self, text: str, prefix: str = '/'):
self.text = text
self.prefix = prefix
@property
def command(self):
"""
Возвращает полную команду с префиксом.
Returns:
str: Команда, состоящая из префикса и текста.
"""
return self.prefix + self.text
class BotCommand(BaseModel):

View File

@@ -1,4 +1,6 @@
import mimetypes
from __future__ import annotations
import puremagic
from ..enums.upload_type import UploadType
@@ -10,9 +12,9 @@ class InputMedia:
Attributes:
path (str): Путь к файлу.
type (UploadType): Тип файла, определенный на основе MIME-типа.
type (UploadType): Тип файла, определенный на основе содержимого (MIME-типа).
"""
def __init__(self, path: str):
"""
@@ -28,7 +30,7 @@ class InputMedia:
def __detect_file_type(self, path: str) -> UploadType:
"""
Определяет тип файла на основе его MIME-типа.
Определяет тип файла на основе его содержимого (MIME-типа).
Args:
path (str): Путь к файлу.
@@ -37,11 +39,67 @@ class InputMedia:
UploadType: Тип файла (VIDEO, IMAGE, AUDIO или FILE).
"""
mime_type, _ = mimetypes.guess_type(path)
with open(path, 'rb') as f:
sample = f.read(4096)
try:
matches = puremagic.magic_string(sample)
if matches:
mime_type = matches[0].mime_type
else:
mime_type = None
except Exception:
mime_type = None
if mime_type is None:
return UploadType.FILE
if mime_type.startswith('video/'):
return UploadType.VIDEO
elif mime_type.startswith('image/'):
return UploadType.IMAGE
elif mime_type.startswith('audio/'):
return UploadType.AUDIO
else:
return UploadType.FILE
class InputMediaBuffer:
"""
Класс для представления медиафайла из буфера.
Attributes:
buffer (bytes): Буфер с содержимым файла.
type (UploadType): Тип файла, определенный по содержимому.
"""
def __init__(self, buffer: bytes, filename: str | None = None):
"""
Инициализирует объект медиафайла из буфера.
Args:
buffer (IO): Буфер с содержимым файла.
filename (str): Название файла (по умолчанию присваивается uuid4).
"""
self.filename = filename
self.buffer = buffer
self.type = self.__detect_file_type(buffer)
def __detect_file_type(self, buffer: bytes) -> UploadType:
try:
matches = puremagic.magic_string(buffer)
if matches:
mime_type = matches[0].mime_type
else:
mime_type = None
except Exception:
mime_type = None
if mime_type is None:
return UploadType.FILE
if mime_type.startswith('video/'):
return UploadType.VIDEO
elif mime_type.startswith('image/'):

View File

@@ -3,26 +3,21 @@ from __future__ import annotations
from pydantic import BaseModel, Field
from typing import Any, Optional, List, Union, TYPE_CHECKING
from ..types.attachments import Attachments
from ..enums.text_style import TextStyle
from ..enums.parse_mode import ParseMode
from ..enums.chat_type import ChatType
from ..enums.message_link_type import MessageLinkType
from .attachments.attachment import Attachment
from .attachments.share import Share
from .attachments.buttons.attachment_button import AttachmentButton
from .attachments.sticker import Sticker
from .attachments.file import File
from .attachments.image import Image
from .attachments.video import Video
from .attachments.audio import Audio
from .attachments.location import Location
from .users import User
if TYPE_CHECKING:
from ..bot import Bot
from ..types.input_media import InputMedia, InputMediaBuffer
class MarkupElement(BaseModel):
@@ -88,21 +83,10 @@ class MessageBody(BaseModel):
mid: str
seq: int
text: str = None
text: Optional[str] = None
attachments: Optional[
List[
Union[
AttachmentButton,
Audio,
Video,
File,
Image,
Sticker,
Share,
Location
]
]
] = []
List[Attachments]
] = Field(default_factory=list) # type: ignore
markup: Optional[
List[
@@ -110,7 +94,7 @@ class MessageBody(BaseModel):
MarkupLink, MarkupElement
]
]
] = []
] = Field(default_factory=list) # type: ignore
class MessageStat(BaseModel):
@@ -163,19 +147,19 @@ class Message(BaseModel):
recipient: Recipient
timestamp: int
link: Optional[LinkedMessage] = None
body: Optional[MessageBody] = None
body: MessageBody
stat: Optional[MessageStat] = None
url: Optional[str] = None
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
bot: Optional[Bot] # type: ignore
async def answer(
self,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
text: Optional[str] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
link: Optional[NewMessageLink] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
@@ -185,7 +169,7 @@ class Message(BaseModel):
Args:
text (str, optional): Текст ответа. Может быть None.
attachments (List[Attachment], optional): Список вложений. Может быть None.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений. Может быть None.
link (NewMessageLink, optional): Связь с другим сообщением. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
@@ -194,6 +178,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.send_message(
chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id,
@@ -206,8 +193,8 @@ class Message(BaseModel):
async def reply(
self,
text: str = None,
attachments: List[Attachment] = None,
text: Optional[str] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
@@ -217,7 +204,7 @@ class Message(BaseModel):
Args:
text (str, optional): Текст ответа. Может быть None.
attachments (List[Attachment], optional): Список вложений. Может быть None.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
@@ -225,6 +212,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.send_message(
chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id,
@@ -241,8 +231,8 @@ class Message(BaseModel):
async def forward(
self,
chat_id,
user_id: int = None,
attachments: List[Attachment] = None,
user_id: Optional[int] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
@@ -253,7 +243,7 @@ class Message(BaseModel):
Args:
chat_id (int): ID чата для отправки (обязателен, если не указан user_id)
user_id (int): ID пользователя для отправки (обязателен, если не указан chat_id). По умолчанию None
attachments (List[Attachment], optional): Список вложений. Может быть None.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
@@ -261,6 +251,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.send_message(
chat_id=chat_id,
user_id=user_id,
@@ -275,9 +268,9 @@ class Message(BaseModel):
async def edit(
self,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
text: Optional[str] = None,
attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None,
link: Optional[NewMessageLink] = None,
notify: bool = True,
parse_mode: Optional[ParseMode] = None
):
@@ -287,7 +280,7 @@ class Message(BaseModel):
Args:
text (str, optional): Новый текст сообщения. Может быть None.
attachments (List[Attachment], optional): Новые вложения. Может быть None.
attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Новые вложения. Может быть None.
link (NewMessageLink, optional): Новая связь с сообщением. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
@@ -296,6 +289,9 @@ class Message(BaseModel):
Any: Результат выполнения метода edit_message бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.edit_message(
message_id=self.body.mid,
text=text,
@@ -330,6 +326,9 @@ class Message(BaseModel):
Any: Результат выполнения метода pin_message бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.pin_message(
chat_id=self.recipient.chat_id,
message_id=self.body.mid,
@@ -351,7 +350,7 @@ class Messages(BaseModel):
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
bot: Optional[Bot] # type: ignore
class NewMessageLink(BaseModel):

View File

@@ -0,0 +1,18 @@
from typing import List, Optional
from pydantic import BaseModel
class Subscription(BaseModel):
"""
Подписка для вебхука
Attributes:
url (str): URL вебхука
time (int): Unix-время, когда была создана подписка
update_types (List[str]): Типы обновлений, на которые подписан бот
"""
url: str
time: int
update_types: Optional[List[str]] = None

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional
from .update import Update
@@ -14,12 +14,14 @@ class BotAdded(Update):
Обновление, сигнализирующее о добавлении бота в чат.
Attributes:
chat_id (Optional[int]): Идентификатор чата, куда добавлен бот.
chat_id (int): Идентификатор чата, куда добавлен бот.
user (User): Объект пользователя-бота.
is_channel (bool): Указывает, был ли бот добавлен в канал или нет
"""
chat_id: Optional[int] = None
chat_id: int
user: User
is_channel: bool
if TYPE_CHECKING:
bot: Optional[Bot]

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional
from .update import Update
@@ -14,12 +14,14 @@ class BotRemoved(Update):
Обновление, сигнализирующее об удалении бота из чата.
Attributes:
chat_id (Optional[int]): Идентификатор чата, из которого удалён бот.
chat_id (int): Идентификатор чата, из которого удалён бот.
user (User): Объект пользователя-бота.
is_channel (bool): Указывает, был ли пользователь добавлен в канал или нет
"""
chat_id: Optional[int] = None
chat_id: int
user: User
is_channel: bool
if TYPE_CHECKING:
bot: Optional[Bot]

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional
from .update import Update
@@ -14,13 +14,13 @@ class BotStarted(Update):
Обновление, сигнализирующее о первом старте бота.
Attributes:
chat_id (Optional[int]): Идентификатор чата.
chat_id (int): Идентификатор чата.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
payload (Optional[str]): Дополнительные данные.
"""
chat_id: Optional[int] = None
chat_id: int
user: User
user_locale: Optional[str] = None
payload: Optional[str] = None

Some files were not shown because too many files have changed in this diff Show More