Compare commits

...

25 Commits

Author SHA1 Message Date
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
70 changed files with 792 additions and 389 deletions

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='Тест чат'
)
)
@@ -80,7 +86,7 @@ async def bot_started(event: BotStarted):
async def chat_title_changed(event: ChatTitleChanged):
await event.bot.send_message(
chat_id=event.chat_id,
text=f'Крутое новое название "{event.chat.title}!"'
text=f'Крутое новое название "{event.chat.title}"!'
)
@@ -112,6 +118,34 @@ async def user_added(event: UserAdded):
chat_id=event.chat_id,
text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!'
)
@dp.bot_stopped()
async def bot_stopped(event: BotStopped):
print(event.from_user.full_name, 'остановил бота') # type: ignore
@dp.dialog_cleared()
async def dialog_cleared(event: DialogCleared):
print(event.from_user.full_name, 'очистил историю чата с ботом') # type: ignore
@dp.dialog_muted()
async def dialog_muted(event: DialogMuted):
print(event.from_user.full_name, 'отключил оповещения от чата бота до ', event.muted_until_datetime) # type: ignore
@dp.dialog_unmuted()
async def dialog_unmuted(event: DialogUnmuted):
print(event.from_user.full_name, 'включил оповещения от чата бота') # type: ignore
@dp.message_chat_created()
async def message_chat_created(event: MessageChatCreated):
await event.bot.send_message(
chat_id=event.chat.chat_id,
text=f'Чат создан! Ссылка: {event.chat.link}'
)
async def main():

View File

@@ -44,7 +44,7 @@ async def echo(event: MessageCreated):
@dp.message_created(Command('builder'))
async def echo(event: MessageCreated):
async def builder(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
@@ -88,7 +88,7 @@ async def echo(event: MessageCreated):
@dp.message_created(Command('payload'))
async def echo(event: MessageCreated):
async def payload(event: MessageCreated):
buttons = [
[
# кнопку типа "chat" убрали из документации,
@@ -133,7 +133,7 @@ async def echo(event: MessageCreated):
@dp.message_chat_created()
async def callback(obj: MessageChatCreated):
async def message_chat_created(obj: MessageChatCreated):
await obj.bot.send_message(
chat_id=obj.chat.chat_id,
text=f'Чат создан! Ссылка: {obj.chat.link}'
@@ -141,7 +141,7 @@ async def callback(obj: MessageChatCreated):
@dp.message_callback()
async def callback(callback: MessageCallback):
async def message_callback(callback: MessageCallback):
await callback.message.answer('Вы нажали на Callback!')

View File

@@ -16,7 +16,7 @@ async def handle_message(event: MessageCreated):
async def main():
await dp.handle_webhook(bot)
await dp.handle_webhook(bot, log_level='critical')
if __name__ == '__main__':

View File

@@ -1,13 +1,21 @@
import asyncio
import logging
from fastapi import Request
from fastapi.responses import JSONResponse
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
from maxapi.dispatcher import webhook_app
logging.basicConfig(level=logging.INFO)
@@ -21,7 +29,7 @@ async def handle_message(event: MessageCreated):
# Регистрация обработчика
# для вебхука
@webhook_app.post('/')
@dp.webhook_post('/')
async def _(request: Request):
# Сериализация полученного запроса

View File

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

View File

@@ -49,7 +49,6 @@ if TYPE_CHECKING:
from .types.chats import Chat, ChatMember, Chats
from .types.command import BotCommand
from .types.message import Message, Messages, NewMessageLink
from .types.updates import UpdateUnion
from .types.users import ChatAdmin, User
from .methods.types.added_admin_chat import AddedListAdminChat

View File

@@ -4,7 +4,6 @@ import os
import mimetypes
from typing import TYPE_CHECKING, Any, Optional
from uuid import uuid4
import aiofiles
import puremagic
@@ -70,7 +69,8 @@ class BaseConnection:
- dict (если is_return_raw=True)
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
if not self.bot.session:
self.bot.session = ClientSession(
@@ -100,7 +100,8 @@ class BaseConnection:
raw = await r.json()
if is_return_raw: return raw
if is_return_raw:
return raw
model = model(**raw) # type: ignore
@@ -154,6 +155,7 @@ class BaseConnection:
async def upload_file_buffer(
self,
filename: str,
url: str,
buffer: bytes,
type: UploadType
@@ -180,7 +182,7 @@ class BaseConnection:
mime_type = f"{type.value}/*"
ext = ''
basename = f'{uuid4()}{ext}'
basename = f'{filename}{ext}'
form = FormData()
form.add_field(

View File

@@ -1,93 +1,9 @@
import asyncio
from typing import Any, Dict, Optional, Union
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 | 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 = {}
__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

@@ -5,9 +5,6 @@ import asyncio
from typing import Any, Callable, Dict, List, TYPE_CHECKING, 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 .filters.middleware import BaseMiddleware
@@ -17,7 +14,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
@@ -25,12 +22,25 @@ 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
@@ -44,12 +54,14 @@ class Dispatcher:
применение middleware, фильтров и вызов соответствующих обработчиков.
"""
def __init__(self) -> None:
def __init__(self, router_id: str | None = None) -> None:
"""
Инициализация диспетчера.
"""
self.router_id = router_id
self.event_handlers: List[Handler] = []
self.contexts: List[MemoryContext] = []
self.routers: List[Router | Dispatcher] = []
@@ -57,12 +69,17 @@ class Dispatcher:
self.middlewares: List[BaseMiddleware] = []
self.bot: Optional[Bot] = None
self.webhook_app: Optional[FastAPI] = None
self.on_started_func: Optional[Callable] = None
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.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)
@@ -72,6 +89,23 @@ 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):
"""
@@ -178,14 +212,19 @@ class Dispatcher:
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
@@ -220,21 +259,21 @@ class Dispatcher:
continue
for key in kwargs.copy().keys():
if not key in func_args:
if key not in func_args:
del kwargs[key]
await handler.func_event(event_object, **kwargs)
logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
logger_dp.info(f'Обработано: {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} | {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} | {process_info} | {e} ")
async def start_polling(self, bot: Bot):
@@ -247,9 +286,12 @@ class Dispatcher:
await self.__ready(bot)
while True:
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
try:
events: Dict = await self.bot.get_updates() # type: ignore
events: Dict = await self.bot.get_updates()
except AsyncioTimeoutError:
continue
@@ -260,11 +302,11 @@ class Dispatcher:
await asyncio.sleep(GET_UPDATES_RETRY_DELAY)
continue
self.bot.marker_updates = events.get('marker') # type: ignore
self.bot.marker_updates = events.get('marker')
processed_events = await process_update_request(
events=events,
bot=self.bot # type: ignore
bot=self.bot
)
for event in processed_events:
@@ -276,7 +318,7 @@ class Dispatcher:
except Exception as 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-приложение для приёма обновлений через вебхук.
@@ -285,30 +327,58 @@ class Dispatcher:
:param host: Хост, на котором запускается сервер.
:param port: Порт сервера.
"""
@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]'
)
# 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]'
# )
@self.webhook_post('/')
async def _(request: Request):
try:
event_json = await request.json()
event_object = await process_update_webhook(
event_json=event_json,
bot=self.bot # type: ignore
)
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}")
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
port=port,
**kwargs
)
async def init_serve(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080, **kwargs):
async def init_serve(self, bot: Bot, host: str = 'localhost', port: int = 8080, **kwargs):
"""
Запускает сервер для обработки входящих вебхуков.
@@ -318,7 +388,30 @@ class Dispatcher:
:param port: Порт сервера.
"""
config = Config(app=webhook_app, host=host, port=port, **kwargs)
# try:
# from uvicorn import Config, Server
# except ImportError:
# 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 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)
@@ -332,8 +425,8 @@ class Router(Dispatcher):
Роутер для группировки обработчиков событий.
"""
def __init__(self):
super().__init__()
def __init__(self, router_id: str | None = None):
super().__init__(router_id)
class Event:

View File

@@ -20,6 +20,10 @@ 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'
# Для начинки диспатчера
ON_STARTED = 'on_started'

View File

@@ -1,4 +1,4 @@
from typing import Callable, List
from typing import Callable, List, Optional
from magic_filter import F, MagicFilter
@@ -44,7 +44,7 @@ class Handler:
self.func_event: Callable = func_event
self.update_type: UpdateType = update_type
self.filters = []
self.state: State = None
self.state: Optional[State] = None
self.middlewares: List[BaseMiddleware] = []
for arg in args:

View File

@@ -19,9 +19,9 @@ class BaseMiddleware:
kwargs_temp = {'data': result_data_kwargs.copy()}
for key in kwargs_temp.copy().keys():
if not key in self.__call__.__annotations__.keys():
if key not in self.__call__.__annotations__.keys(): # type: ignore
del kwargs_temp[key]
result: Dict[str, Any] = await self(event_object, **kwargs_temp)
result: Dict[str, Any] = await self(event_object, **kwargs_temp) # type: ignore
return result

View File

@@ -48,7 +48,8 @@ class AddAdminChat(BaseConnection):
AddedListAdminChat: Результат операции с информацией об успешности.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}

View File

@@ -45,7 +45,8 @@ class AddMembersChat(BaseConnection):
AddedMembersChat: Результат операции с информацией об успешности добавления.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}

View File

@@ -48,14 +48,19 @@ class ChangeInfo(BaseConnection):
User: Объект с обновленными данными бота
"""
assert self.bot is not None
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
return await super().request(
method=HTTPMethod.PATCH,

View File

@@ -39,7 +39,8 @@ class DeleteMeFromMessage(BaseConnection):
DeletedBotFromChat: Результат операции удаления.
"""
assert self.bot is not None
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

@@ -38,7 +38,9 @@ class DeleteChat(BaseConnection):
DeletedChat: Результат операции удаления чата.
"""
assert self.bot is not None
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

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

View File

@@ -38,7 +38,10 @@ class DeletePinMessage(BaseConnection):
Returns:
DeletedPinMessage: Результат операции удаления закреплённого сообщения.
"""
assert self.bot is not None
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

View File

@@ -64,14 +64,16 @@ class EditChat(BaseConnection):
Chat: Обновлённый объект чата.
"""
assert self.bot is not None
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:
raise MaxIconParamsException(
@@ -81,9 +83,12 @@ class EditChat(BaseConnection):
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

@@ -66,14 +66,17 @@ class EditMessage(BaseConnection):
EditedMessage: Обновлённое сообщение.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
json: Dict[str, Any] = {'attachments': []}
params['message_id'] = self.message_id
if not self.text is None: json['text'] = self.text
if self.text is not None:
json['text'] = self.text
if self.attachments:
@@ -91,9 +94,12 @@ class EditMessage(BaseConnection):
else:
json['attachments'].append(att.model_dump())
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.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)

View File

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

View File

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

View File

@@ -46,7 +46,10 @@ class GetChats(BaseConnection):
Returns:
Chats: Объект с данными по списку чатов.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['count'] = self.count

View File

@@ -42,7 +42,10 @@ class GetListAdminChat(BaseConnection):
Returns:
GettedListAdminChat: Объект с информацией о администраторах чата.
"""
assert self.bot is not None
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

@@ -32,7 +32,10 @@ class GetMe(BaseConnection):
Returns:
User: Объект пользователя с полной информацией.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.ME,

View File

@@ -42,7 +42,10 @@ class GetMeFromChat(BaseConnection):
Returns:
ChatMember: Информация о боте как участнике чата.
"""
assert self.bot is not None
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

@@ -57,14 +57,19 @@ class GetMembersChat(BaseConnection):
Returns:
GettedMembersChat: Объект с данными по участникам чата.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
if self.user_ids:
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
if self.marker:
params['marker'] = self.marker
if self.count:
params['marker'] = self.count
return await super().request(
method=HTTPMethod.GET,

View File

@@ -59,10 +59,14 @@ class GetMessages(BaseConnection):
Returns:
Messages: Объект с полученными сообщениями.
"""
assert self.bot is not None
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

@@ -37,7 +37,10 @@ class GetPinnedMessage(BaseConnection):
Returns:
GettedPin: Объект с информацией о закреплённом сообщении.
"""
assert self.bot is not None
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

@@ -1,8 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Dict
from ..types.updates import UpdateUnion
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
@@ -45,7 +43,10 @@ class GetUpdates(BaseConnection):
Returns:
UpdateUnion: Объединённый тип данных обновлений.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['limit'] = self.limit

View File

@@ -43,7 +43,10 @@ class GetUploadURL(BaseConnection):
Returns:
GettedUploadUrl: Результат с URL для загрузки.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['type'] = self.type.value

View File

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

View File

@@ -52,7 +52,10 @@ class PinMessage(BaseConnection):
Returns:
PinnedMessage: Объект с информацией о закреплённом сообщении.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}
json['message_id'] = self.message_id

View File

@@ -46,7 +46,10 @@ class RemoveAdmin(BaseConnection):
Returns:
RemovedAdmin: Объект с результатом отмены прав администратора.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.DELETE,
path=ApiPath.CHATS + '/' + str(self.chat_id) + \

View File

@@ -54,7 +54,9 @@ class RemoveMemberChat(BaseConnection):
RemovedMemberChat: Результат удаления участника.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['chat_id'] = self.chat_id

View File

@@ -49,7 +49,9 @@ class SendAction(BaseConnection):
Returns:
SendedAction: Результат выполнения запроса.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
json: Dict[str, Any] = {}

View File

@@ -55,15 +55,19 @@ class SendCallback(BaseConnection):
SendedCallback: Объект с результатом отправки callback.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
params['callback_id'] = self.callback_id
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

@@ -70,21 +70,29 @@ class SendMessage(BaseConnection):
SendedMessage или Error
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
params = self.bot.params.copy()
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) or isinstance(att, InputMediaBuffer):
if isinstance(att, (InputMedia, InputMediaBuffer)):
HAS_INPUT_MEDIA = True
input_media = await process_input_media(
base_connection=self,
bot=self.bot,
@@ -96,11 +104,16 @@ class SendMessage(BaseConnection):
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
await asyncio.sleep(self.bot.after_input_media_delay)
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(self.ATTEMPTS_COUNT):

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,54 @@ 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
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
}
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

@@ -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

@@ -9,6 +9,10 @@ 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
@@ -32,6 +36,10 @@ from .input_media import InputMedia
from .input_media import InputMediaBuffer
__all__ = [
'DialogUnmuted',
'DialogMuted',
'DialogCleared',
'BotStopped',
'CommandStart',
'OpenAppButton',
'Message',

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

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,3 @@
from pydantic import BaseModel
from ....enums.button_type import ButtonType
from .button import Button

View File

@@ -1,5 +1,3 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Literal, Optional
from typing import TYPE_CHECKING, Any, Optional
from pydantic import BaseModel, Field
from ...enums.attachment import AttachmentType

View File

@@ -1,16 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import puremagic
from ..enums.upload_type import UploadType
if TYPE_CHECKING:
from io import BytesIO
class InputMedia:
"""
Класс для представления медиафайла.
@@ -74,13 +68,15 @@ class InputMediaBuffer:
type (UploadType): Тип файла, определенный по содержимому.
"""
def __init__(self, buffer: bytes):
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)

View File

@@ -195,7 +195,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
assert self.bot is not None
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,
@@ -227,7 +229,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
assert self.bot is not None
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,
@@ -264,7 +268,9 @@ class Message(BaseModel):
Any: Результат выполнения метода send_message бота.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.send_message(
chat_id=chat_id,
user_id=user_id,
@@ -300,7 +306,9 @@ class Message(BaseModel):
Any: Результат выполнения метода edit_message бота.
"""
assert self.bot is not None
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await self.bot.edit_message(
message_id=self.body.mid,
text=text,
@@ -335,7 +343,9 @@ class Message(BaseModel):
Any: Результат выполнения метода pin_message бота.
"""
assert self.bot is not None
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,

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

View File

@@ -0,0 +1,32 @@
from typing import TYPE_CHECKING, Optional
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class BotStopped(Update):
"""
Обновление, сигнализирующее об остановке бота.
Attributes:
chat_id (int): Идентификатор чата.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
payload (Optional[str]): Дополнительные данные.
"""
chat_id: int
user: User
user_locale: Optional[str] = None
payload: Optional[str] = None
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional
from .update import Update
@@ -16,12 +16,12 @@ class ChatTitleChanged(Update):
Attributes:
chat_id (Optional[int]): Идентификатор чата.
user (User): Пользователь, совершивший изменение.
title (Optional[str]): Новое название чата.
title (str): Новое название чата.
"""
chat_id: Optional[int] = None
chat_id: int
user: User
title: Optional[str] = None
title: str
if TYPE_CHECKING:
bot: Optional[Bot]

View File

@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING, Optional
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class DialogCleared(Update):
"""
Обновление, сигнализирующее об очистке диалога с ботом.
Attributes:
chat_id (int): Идентификатор чата.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
"""
chat_id: int
user: User
user_locale: Optional[str] = None
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING, Optional
from datetime import datetime
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class DialogMuted(Update):
"""
Обновление, сигнализирующее об отключении оповещений от бота.
Attributes:
chat_id (int): Идентификатор чата.
muted_until (int): Время до включения оповещений от бота.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
"""
chat_id: int
muted_until: int
user: User
user_locale: Optional[str] = None
if TYPE_CHECKING:
bot: Optional[Bot]
@property
def muted_until_datetime(self):
try:
return datetime.fromtimestamp(self.muted_until // 1000)
except (OverflowError, OSError):
return datetime.max
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING, Optional
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class DialogUnmuted(Update):
"""
Обновление, сигнализирующее о включении оповещений от бота.
Attributes:
chat_id (int): Идентификатор чата.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
"""
chat_id: int
user: User
user_locale: Optional[str] = None
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@@ -1,4 +1,4 @@
from typing import List, Optional, TYPE_CHECKING, Union
from typing import List, Optional, Union
from pydantic import BaseModel, Field
@@ -21,12 +21,6 @@ from ..attachments.video import Video
from ..attachments.audio import Audio
if TYPE_CHECKING:
from ...bot import Bot
from ...types.chats import Chat
from ...types.users import User
class MessageForCallback(BaseModel):
"""
@@ -110,6 +104,9 @@ class MessageCallback(Update):
Результат вызова send_callback бота.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
message = MessageForCallback()
message.text = new_text
@@ -117,8 +114,7 @@ class MessageCallback(Update):
message.link = link
message.notify = notify
message.format = format
assert self.bot is not None
return await self.bot.send_callback(
callback_id=self.callback.callback_id,
message=message,

View File

@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from typing import Optional
from .update import Update

View File

@@ -1,5 +1,3 @@
from typing import Optional
from .update import Update
@@ -9,14 +7,14 @@ class MessageRemoved(Update):
Класс для обработки события удаления сообщения в чате.
Attributes:
message_id (Optional[str]): Идентификатор удаленного сообщения. Может быть None.
chat_id (Optional[int]): Идентификатор чата. Может быть None.
user_id (Optional[int]): Идентификатор пользователя. Может быть None.
message_id (str): Идентификатор удаленного сообщения. Может быть None.
chat_id (int): Идентификатор чата. Может быть None.
user_id (int): Идентификатор пользователя. Может быть None.
"""
message_id: Optional[str] = None
chat_id: Optional[int] = None
user_id: Optional[int] = None
message_id: str
chat_id: int
user_id: int
def get_ids(self):

View File

@@ -28,9 +28,9 @@ class Update(BaseModel):
chat: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
from_user: Optional[User]
chat: Optional[Chat]
bot: Optional[Bot] # type: ignore
from_user: Optional[User] # type: ignore
chat: Optional[Chat] # type: ignore
class Config:
arbitrary_types_allowed=True

View File

@@ -11,14 +11,16 @@ class UserAdded(Update):
Класс для обработки события добавления пользователя в чат.
Attributes:
inviter_id (Optional[int]): Идентификатор пользователя, добавившего нового участника. Может быть None.
chat_id (Optional[int]): Идентификатор чата. Может быть None.
inviter_id (int): Идентификатор пользователя, добавившего нового участника. Может быть None.
chat_id (int): Идентификатор чата. Может быть None.
user (User): Объект пользователя, добавленного в чат.
is_channel (bool): Указывает, был ли пользователь добавлен в канал или нет
"""
inviter_id: Optional[int] = None
chat_id: Optional[int] = None
chat_id: int
user: User
is_channel: bool
def get_ids(self):

View File

@@ -12,13 +12,15 @@ class UserRemoved(Update):
Attributes:
admin_id (Optional[int]): Идентификатор администратора, удалившего пользователя. Может быть None.
chat_id (Optional[int]): Идентификатор чата. Может быть None.
chat_id (int): Идентификатор чата. Может быть None.
user (User): Объект пользователя, удаленного из чата.
is_channel (bool): Указывает, был ли пользователь удален из канала или нет
"""
admin_id: Optional[int] = None
chat_id: Optional[int] = None
chat_id: int
user: User
is_channel: bool
def get_ids(self):

View File

@@ -35,6 +35,13 @@ class User(BaseModel):
avatar_url: Optional[str] = None
full_avatar_url: Optional[str] = None
commands: Optional[List[BotCommand]] = None
@property
def full_name(self):
if self.last_name is None:
return self.first_name
return f'{self.first_name} {self.last_name}'
class Config:
json_encoders = {

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from json import loads
from uuid import uuid4
from ..types.input_media import InputMedia, InputMediaBuffer
from ..enums.upload_type import UploadType
@@ -46,6 +47,7 @@ async def process_input_media(
)
elif isinstance(att, InputMediaBuffer):
upload_file_response = await base_connection.upload_file_buffer(
filename=att.filename or str(uuid4()),
url=upload.url,
buffer=att.buffer,
type=att.type,
@@ -53,8 +55,10 @@ async def process_input_media(
if att.type in (UploadType.VIDEO, UploadType.AUDIO):
if upload.token is None:
assert bot.session is not None
await bot.session.close()
if bot.session is not None:
await bot.session.close()
raise MaxUploadFileFailed('По неизвестной причине token не был получен')
token = upload.token

77
maxapi/utils/updates.py Normal file
View File

@@ -0,0 +1,77 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
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_created import MessageCreated
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 ..enums.chat_type import ChatType
if TYPE_CHECKING:
from ..bot import Bot
async def enrich_event(event_object: Any, bot: Bot) -> Any:
if not bot.auto_requests:
return event_object
if hasattr(event_object, 'chat_id'):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
if isinstance(event_object, (MessageCreated, MessageEdited, MessageCallback)):
if event_object.message.recipient.chat_id is not None:
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id)
event_object.from_user = getattr(event_object.message, 'sender', None)
elif isinstance(event_object, MessageRemoved):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
if event_object.chat.type == ChatType.CHAT:
event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id,
user_id=event_object.user_id
)
elif event_object.chat.type == ChatType.DIALOG:
event_object.from_user = event_object.chat
elif isinstance(event_object, UserRemoved):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
if event_object.admin_id:
event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id,
user_id=event_object.admin_id
)
elif isinstance(event_object, UserAdded):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.from_user = event_object.user
elif isinstance(event_object, (BotAdded, BotRemoved, BotStarted, ChatTitleChanged, BotStopped, DialogCleared, DialogMuted, DialogUnmuted)):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.from_user = event_object.user
if hasattr(event_object, 'message'):
event_object.message.bot = bot
for att in event_object.message.body.attachments:
if hasattr(att, 'bot'):
att.bot = bot
if hasattr(event_object, 'bot'):
event_object.bot = bot
return event_object

View File

@@ -6,6 +6,10 @@
| `bot_added` | Бот добавлен в чат |
| `bot_removed` | Бот удалён из чата |
| `bot_started` | Пользователь запустил бота |
| `bot_stopped` | Пользователь остановил бота |
| `dialog_cleared` | Пользователь очистил историю диалога с ботом |
| `dialog_muted` | Пользователь отключил оповещения от чата бота |
| `dialog_unmuted` | Пользователь включил оповещения от чата бота |
| `chat_title_changed` | Изменено название чата |
| `message_callback` | Пользователь нажал на callback-кнопку (inline button) |
| `message_chat_created`| Срабатывает когда пользователь нажал на кнопку с действием "Создать чат" (работает некорректно со стороны API MAX, ждем исправлений) |

View File

@@ -81,13 +81,21 @@ async def init_serve(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080, **
import asyncio
import logging
from fastapi import Request
from fastapi.responses import JSONResponse
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
from maxapi.dispatcher import webhook_app
logging.basicConfig(level=logging.INFO)
@@ -101,7 +109,7 @@ async def handle_message(event: MessageCreated):
# Регистрация обработчика
# для вебхука
@webhook_app.post('/')
@dp.webhook_post('/')
async def _(request: Request):
# Сериализация полученного запроса
@@ -133,7 +141,6 @@ async def main():
if __name__ == '__main__':
asyncio.run(main())
```
---