Compare commits

...

13 Commits

13 changed files with 399 additions and 101 deletions

View File

@@ -9,9 +9,12 @@ from .types.errors import Error
from .types.input_media import InputMedia, InputMediaBuffer
from .connection.base import BaseConnection
from .loggers import logger_bot
from .enums.parse_mode import ParseMode
from .enums.sender_action import SenderAction
from .enums.upload_type import UploadType
from .enums.update import UpdateType
from .methods.add_admin_chat import AddAdminChat
from .methods.add_members_chat import AddMembersChat
@@ -20,7 +23,7 @@ from .methods.delete_bot_from_chat import DeleteMeFromMessage
from .methods.delete_chat import DeleteChat
from .methods.delete_message import DeleteMessage
from .methods.delete_pin_message import DeletePinMessage
from .methods.download_media import DownloadMedia
# from .methods.download_media import DownloadMedia
from .methods.edit_chat import EditChat
from .methods.edit_message import EditMessage
from .methods.get_chat_by_id import GetChatById
@@ -41,6 +44,12 @@ from .methods.remove_member_chat import RemoveMemberChat
from .methods.send_action import SendAction
from .methods.send_callback import SendCallback
from .methods.send_message import SendMessage
from .methods.get_subscriptions import GetSubscriptions
from .methods.types.getted_subscriptions import GettedSubscriptions
from .methods.subscribe_webhook import SubscribeWebhook
from .methods.types.subscribed import Subscribed
from .methods.types.unsubscribed import Unsubscribed
from .methods.unsubscribe_webhook import UnsubscribeWebhook
if TYPE_CHECKING:
from .types.attachments.attachment import Attachment
@@ -85,7 +94,8 @@ class Bot(BaseConnection):
notify: Optional[bool] = None,
auto_requests: bool = True,
default_connection: Optional[DefaultConnectionProperties] = None,
after_input_media_delay: Optional[float] = None
after_input_media_delay: Optional[float] = None,
auto_check_subscriptions: bool = True
):
"""
@@ -94,17 +104,18 @@ class Bot(BaseConnection):
:param token: Токен доступа к API бота
:param parse_mode: Форматирование по умолчанию
:param notify: Отключение уведомлений при отправке сообщений (по умолчанию игнорируется) (не работает на стороне MAX)
:param auto_requests: Автоматическое заполнение полей chat и from_user в Update
:param auto_requests: Автоматическое заполнение полей chat и from_user в Update с помощью API запросов если они не заложены как полноценные объекты в Update (по умолчанию True, при False chat и from_user в некоторых событиях будут выдавать None)
:param default_connection: Настройки aiohttp
:param after_input_media_delay: Задержка в секундах после загрузки файла на сервера MAX (без этого чаще всего MAX не успевает обработать вложение и выдает ошибку `errors.process.attachment.file.not.processed`)
с помощью API запросов если они не заложены как полноценные объекты в Update (по умолчанию True, при False chat и from_user в некоторых событиях будут выдавать None)
:param auto_check_subscriptions: Проверка на установленные подписки для метода start_polling (бот не работает в поллинге при установленных подписках)
"""
super().__init__()
self.bot = self
self.default_connection = default_connection or DefaultConnectionProperties()
self.after_input_media_delay = after_input_media_delay or 2.0
self.after_input_media_delay = after_input_media_delay or 2.0
self.auto_check_subscriptions = auto_check_subscriptions
self.__token = token
self.params: Dict[str, Any] = {'access_token': self.__token}
@@ -765,26 +776,100 @@ class Bot(BaseConnection):
commands=list(commands)
).fetch()
async def download_file(
self,
path: str,
url: str,
token: str
):
async def get_subscriptions(self) -> GettedSubscriptions:
"""
Скачивает медиа с указанной ссылки по токену, сохраняя по определенному пути
Получает список всех подписок.
:param path: Путь сохранения медиа
:param url: Ссылка на медиа
:param token: Токен медиа
:return: Числовой статус
:return: Объект со списком подписок
"""
return await DownloadMedia(
return await GetSubscriptions(bot=self).fetch()
async def subscribe_webhook(
self,
url: str,
update_types: Optional[List[UpdateType]] = None,
secret: Optional[str] = None
) -> Subscribed:
"""
Подписывает бота на получение обновлений через WebHook.
После вызова этого метода бот будет получать уведомления о новых событиях в чатах на указанный URL.
Ваш сервер должен прослушивать один из следующих портов: `80`, `8080`, `443`, `8443`, `16384`-`32383`.
:param url: URL HTTP(S)-эндпойнта вашего бота. Должен начинаться с http(s)://
:param update_types: Список типов обновлений, которые ваш бот хочет получать.
Для полного списка типов см. объект
:param secret: От 5 до 256 символов. Cекрет, который должен быть отправлен в заголовке X-Max-Bot-Api-Secret
в каждом запросе Webhook. Разрешены только символы A-Z, a-z, 0-9, и дефис.
Заголовок рекомендован, чтобы запрос поступал из установленного веб-узла
:return: Обновленная информация о боте
"""
return await SubscribeWebhook(
bot=self,
path=path,
media_url=url,
media_token=token
).fetch()
url=url,
update_types=update_types,
secret=secret
).fetch()
async def unsubscribe_webhook(
self,
url: str,
) -> Unsubscribed:
"""
Отписывает бота от получения обновлений через WebHook.
После вызова этого метода бот перестает получать уведомления о новых событиях,
и доступна доставка уведомлений через API с длительным опросом.
:param url: URL HTTP(S)-эндпойнта вашего бота. Должен начинаться с http(s)://
:return: Обновленная информация о боте
"""
return await UnsubscribeWebhook(
bot=self,
url=url,
).fetch()
async def delete_webhook(self):
"""
Удаление всех подписок на Webhook
"""
subs = await self.get_subscriptions()
if subs.subscriptions:
for sub in subs.subscriptions:
await self.unsubscribe_webhook(sub.url)
logger_bot.info('Удалена подписка на Webhook: %s', sub.url)
# async def download_file(
# self,
# path: str,
# url: str,
# token: str
# ):
# """
# Скачивает медиа с указанной ссылки по токену, сохраняя по определенному пути
# :param path: Путь сохранения медиа
# :param url: Ссылка на медиа
# :param token: Токен медиа
# :return: Числовой статус
# """
# return await DownloadMedia(
# bot=self,
# path=path,
# media_url=url,
# media_token=token
# ).fetch()

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
import asyncio
from typing import Any, Callable, Dict, List, TYPE_CHECKING, Optional
import functools
from typing import Any, Awaitable, Callable, Dict, List, TYPE_CHECKING, Optional
from asyncio.exceptions import TimeoutError as AsyncioTimeoutError
from aiohttp import ClientConnectorError
@@ -74,6 +75,7 @@ class Dispatcher:
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)
@@ -120,6 +122,17 @@ class Dispatcher:
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]]:
for mw in reversed(middlewares):
handler = functools.partial(mw, handler)
return handler
def include_routers(self, *routers: 'Router'):
@@ -141,6 +154,13 @@ class Dispatcher:
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]
@@ -169,38 +189,18 @@ class Dispatcher:
new_ctx = MemoryContext(chat_id, user_id)
self.contexts.append(new_ctx)
return new_ctx
async def process_middlewares(
self,
middlewares: List[BaseMiddleware],
event_object: UpdateUnion,
result_data_kwargs: Dict[str, Any]
):
async def call_handler(self, handler, event_object, data):
"""
Последовательно обрабатывает middleware цепочку.
:param middlewares: Список middleware.
:param event_object: Объект события.
:param result_data_kwargs: Аргументы, передаваемые обработчику.
:return: Изменённые аргументы или None.
Правка аргументов конечной функции хендлера и ее вызов
"""
for middleware in middlewares:
result = await middleware.process_middleware(
event_object=event_object,
result_data_kwargs=result_data_kwargs
)
if result is None or result is False:
return
elif result is True:
continue
result_data_kwargs.update(result)
func_args = handler.func_event.__annotations__.keys()
kwargs_filtered = {k: v for k, v in data.items() if k in func_args}
await handler.func_event(event_object, **kwargs_filtered)
return result_data_kwargs
async def handle(self, event_object: UpdateUnion):
@@ -232,12 +232,6 @@ class Dispatcher:
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
)
for handler in router.event_handlers:
if not handler.update_type == event_object.update_type:
@@ -252,20 +246,19 @@ class Dispatcher:
func_args = handler.func_event.__annotations__.keys()
kwargs = await self.process_middlewares(
middlewares=handler.middlewares,
event_object=event_object,
result_data_kwargs=kwargs
)
if isinstance(router, Router):
full_middlewares = self.middlewares + router.middlewares + handler.middlewares
elif isinstance(router, Dispatcher):
full_middlewares = self.middlewares + handler.middlewares
if not kwargs:
continue
handler_chain = self.build_middleware_chain(
full_middlewares,
functools.partial(self.call_handler, handler)
)
kwargs_filtered = {k: v for k, v in kwargs.items() if k in func_args}
for key in kwargs.copy().keys():
if key not in func_args:
del kwargs[key]
await handler.func_event(event_object, **kwargs)
await handler_chain(event_object, kwargs_filtered)
logger_dp.info(f'Обработано: {router_id} | {process_info}')
@@ -286,12 +279,14 @@ class Dispatcher:
:param bot: Экземпляр бота.
"""
self.polling = True
await self.__ready(bot)
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
while True:
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
while self.polling:
try:
events: Dict = await self.bot.get_updates()

View File

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

View File

@@ -1,27 +1,10 @@
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
):
# пока что заглушка
if result_data_kwargs is None:
return {}
kwargs_temp = {'data': result_data_kwargs.copy()}
for key in kwargs_temp.copy().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) # type: ignore
return result
async def __call__(
self,
handler: Callable[[Any, dict[str, Any]], Awaitable[Any]],
event_object: Any,
data: dict[str, Any]
) -> Any:
return await handler(event_object, data)

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

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

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

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

@@ -8,10 +8,10 @@ from ...types.users import User
class UserRemoved(Update):
"""
Класс для обработки события удаления пользователя из чата.
Класс для обработки события выходе/удаления пользователя из чата.
Attributes:
admin_id (Optional[int]): Идентификатор администратора, удалившего пользователя. Может быть None.
admin_id (Optional[int]): Идентификатор администратора, удалившего пользователя. None при выходе из чата самим пользователем.
chat_id (int): Идентификатор чата. Может быть None.
user (User): Объект пользователя, удаленного из чата.
is_channel (bool): Указывает, был ли пользователь удален из канала или нет

View File

@@ -16,7 +16,7 @@
| `message_edited` | Сообщение было отредактировано |
| `message_removed` | Сообщение было удалено |
| `user_added` | Пользователь добавлен в чат |
| `user_removed` | Пользователь удалён из чата |
| `user_removed` | Пользователь удалён/вышел из чата |
| `on_started` | Бот запущен (**внутреннее** событие библиотеки) |
---