mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'aiogram:dev-2.x' into dev-2.x
This commit is contained in:
commit
65fba24200
58 changed files with 1732 additions and 244 deletions
2
Makefile
2
Makefile
|
|
@ -27,7 +27,7 @@ upload:
|
|||
|
||||
release:
|
||||
make clean
|
||||
make test
|
||||
#make test
|
||||
make build
|
||||
make tag
|
||||
@echo "Released aiogram $(AIOGRAM_VERSION)"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ AIOGramBot
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = (
|
|||
'utils',
|
||||
)
|
||||
|
||||
__version__ = '2.14.3'
|
||||
__api_version__ = '5.3'
|
||||
__version__ = '2.18'
|
||||
__api_version__ = '5.6'
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 5.3
|
||||
List is updated to Bot API 5.6
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -230,11 +230,15 @@ class Methods(Helper):
|
|||
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
|
||||
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
|
||||
SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle
|
||||
BAN_CHAT_SENDER_CHAT = Item() # banChatSenderChat
|
||||
UNBAN_CHAT_SENDER_CHAT = Item() # unbanChatSenderChat
|
||||
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
|
||||
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
|
||||
CREATE_CHAT_INVITE_LINK = Item() # createChatInviteLink
|
||||
EDIT_CHAT_INVITE_LINK = Item() # editChatInviteLink
|
||||
REVOKE_CHAT_INVITE_LINK = Item() # revokeChatInviteLink
|
||||
APPROVE_CHAT_JOIN_REQUEST = Item() # approveChatJoinRequest
|
||||
DECLINE_CHAT_JOIN_REQUEST = Item() # declineChatJoinRequest
|
||||
SET_CHAT_PHOTO = Item() # setChatPhoto
|
||||
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
|
||||
SET_CHAT_TITLE = Item() # setChatTitle
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import ssl
|
||||
import typing
|
||||
import warnings
|
||||
|
|
@ -35,6 +37,7 @@ class BaseBot:
|
|||
proxy_auth: Optional[aiohttp.BasicAuth] = None,
|
||||
validate_token: Optional[base.Boolean] = True,
|
||||
parse_mode: typing.Optional[base.String] = None,
|
||||
disable_web_page_preview: Optional[base.Boolean] = None,
|
||||
timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None,
|
||||
server: TelegramAPIServer = TELEGRAM_PRODUCTION
|
||||
):
|
||||
|
|
@ -55,6 +58,8 @@ class BaseBot:
|
|||
:type validate_token: :obj:`bool`
|
||||
:param parse_mode: You can set default parse mode
|
||||
:type parse_mode: :obj:`str`
|
||||
:param disable_web_page_preview: You can set default disable web page preview parameter
|
||||
:type disable_web_page_preview: :obj:`bool`
|
||||
:param timeout: Request timeout
|
||||
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
|
||||
:param server: Telegram Bot API Server endpoint.
|
||||
|
|
@ -105,10 +110,11 @@ class BaseBot:
|
|||
|
||||
self.parse_mode = parse_mode
|
||||
|
||||
def get_new_session(self) -> aiohttp.ClientSession:
|
||||
self.disable_web_page_preview = disable_web_page_preview
|
||||
|
||||
async def get_new_session(self) -> aiohttp.ClientSession:
|
||||
return aiohttp.ClientSession(
|
||||
connector=self._connector_class(**self._connector_init, loop=self._main_loop),
|
||||
loop=self._main_loop,
|
||||
connector=self._connector_class(**self._connector_init),
|
||||
json_serialize=json.dumps
|
||||
)
|
||||
|
||||
|
|
@ -116,10 +122,25 @@ class BaseBot:
|
|||
def loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
||||
return self._main_loop
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[aiohttp.ClientSession]:
|
||||
async def get_session(self) -> Optional[aiohttp.ClientSession]:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = self.get_new_session()
|
||||
self._session = await self.get_new_session()
|
||||
|
||||
if not self._session._loop.is_running(): # NOQA
|
||||
# Hate `aiohttp` devs because it juggles event-loops and breaks already opened session
|
||||
# So... when we detect a broken session need to fix it by re-creating it
|
||||
# @asvetlov, if you read this, please no more juggle event-loop inside aiohttp, it breaks the brain.
|
||||
await self._session.close()
|
||||
self._session = await self.get_new_session()
|
||||
|
||||
return self._session
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
reason="Client session should be created inside async function, use `await bot.get_session()` instead",
|
||||
stacklevel=3,
|
||||
)
|
||||
def session(self) -> Optional[aiohttp.ClientSession]:
|
||||
return self._session
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -185,7 +206,8 @@ class BaseBot:
|
|||
"""
|
||||
Close all client sessions
|
||||
"""
|
||||
await self.session.close()
|
||||
if self._session:
|
||||
await self._session.close()
|
||||
|
||||
async def request(self, method: base.String,
|
||||
data: Optional[Dict] = None,
|
||||
|
|
@ -205,35 +227,57 @@ class BaseBot:
|
|||
:rtype: Union[List, Dict]
|
||||
:raise: :obj:`aiogram.exceptions.TelegramApiError`
|
||||
"""
|
||||
return await api.make_request(self.session, self.server, self.__token, method, data, files,
|
||||
|
||||
return await api.make_request(await self.get_session(), self.server, self.__token, method, data, files,
|
||||
proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs)
|
||||
|
||||
async def download_file(self, file_path: base.String,
|
||||
destination: Optional[base.InputFile] = None,
|
||||
timeout: Optional[base.Integer] = sentinel,
|
||||
chunk_size: Optional[base.Integer] = 65536,
|
||||
seek: Optional[base.Boolean] = True) -> Union[io.BytesIO, io.FileIO]:
|
||||
async def download_file(
|
||||
self,
|
||||
file_path: base.String,
|
||||
destination: Optional[Union[base.InputFile, pathlib.Path]] = None,
|
||||
timeout: Optional[base.Integer] = sentinel,
|
||||
chunk_size: Optional[base.Integer] = 65536,
|
||||
seek: Optional[base.Boolean] = True,
|
||||
destination_dir: Optional[Union[str, pathlib.Path]] = None,
|
||||
make_dirs: Optional[base.Boolean] = True,
|
||||
) -> Union[io.BytesIO, io.FileIO]:
|
||||
"""
|
||||
Download file by file_path to destination
|
||||
Download file by file_path to destination file or directory
|
||||
|
||||
if You want to automatically create destination (:class:`io.BytesIO`) use default
|
||||
value of destination and handle result of this method.
|
||||
|
||||
At most one of these parameters can be used: :param destination:, :param destination_dir:
|
||||
|
||||
:param file_path: file path on telegram server (You can get it from :obj:`aiogram.types.File`)
|
||||
:type file_path: :obj:`str`
|
||||
:param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||
:param timeout: Integer
|
||||
:param chunk_size: Integer
|
||||
:param seek: Boolean - go to start of file when downloading is finished.
|
||||
:param destination_dir: directory for saving files
|
||||
:param make_dirs: Make dirs if not exist
|
||||
:return: destination
|
||||
"""
|
||||
if destination is None:
|
||||
if destination and destination_dir:
|
||||
raise ValueError(
|
||||
"Use only one of the parameters:destination or destination_dir."
|
||||
)
|
||||
|
||||
if destination is None and destination_dir is None:
|
||||
destination = io.BytesIO()
|
||||
|
||||
elif destination_dir:
|
||||
destination = os.path.join(destination_dir, file_path)
|
||||
|
||||
if make_dirs and not isinstance(destination, io.IOBase) and os.path.dirname(destination):
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
|
||||
url = self.get_file_url(file_path)
|
||||
|
||||
dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb')
|
||||
async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response:
|
||||
session = await self.get_session()
|
||||
async with session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response:
|
||||
while True:
|
||||
chunk = await response.content.read(chunk_size)
|
||||
if not chunk:
|
||||
|
|
@ -294,5 +338,22 @@ class BaseBot:
|
|||
def parse_mode(self):
|
||||
self.parse_mode = None
|
||||
|
||||
@property
|
||||
def disable_web_page_preview(self):
|
||||
return getattr(self, '_disable_web_page_preview', None)
|
||||
|
||||
@disable_web_page_preview.setter
|
||||
def disable_web_page_preview(self, value):
|
||||
if value is None:
|
||||
setattr(self, '_disable_web_page_preview', None)
|
||||
else:
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"Disable web page preview must be bool, not {type(value)}")
|
||||
setattr(self, '_disable_web_page_preview', value)
|
||||
|
||||
@disable_web_page_preview.deleter
|
||||
def disable_web_page_preview(self):
|
||||
self.disable_web_page_preview = None
|
||||
|
||||
def check_auth_widget(self, data):
|
||||
return check_integrity(self.__token, data)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import pathlib
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from .base import BaseBot, api
|
||||
from .. import types
|
||||
from ..types import base
|
||||
from ..utils.deprecated import deprecated
|
||||
from ..utils.deprecated import deprecated, removed_argument
|
||||
from ..utils.exceptions import ValidationError
|
||||
from ..utils.mixins import DataMixin, ContextInstanceMixin
|
||||
from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file
|
||||
|
|
@ -43,25 +44,37 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
if hasattr(self, '_me'):
|
||||
delattr(self, '_me')
|
||||
|
||||
async def download_file_by_id(self, file_id: base.String, destination=None,
|
||||
timeout: base.Integer = 30, chunk_size: base.Integer = 65536,
|
||||
seek: base.Boolean = True):
|
||||
async def download_file_by_id(
|
||||
self,
|
||||
file_id: base.String,
|
||||
destination: typing.Optional[base.InputFile, pathlib.Path] = None,
|
||||
timeout: base.Integer = 30,
|
||||
chunk_size: base.Integer = 65536,
|
||||
seek: base.Boolean = True,
|
||||
destination_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None,
|
||||
make_dirs: typing.Optional[base.Boolean] = True,
|
||||
):
|
||||
"""
|
||||
Download file by file_id to destination
|
||||
Download file by file_id to destination file or directory
|
||||
|
||||
if You want to automatically create destination (:class:`io.BytesIO`) use default
|
||||
value of destination and handle result of this method.
|
||||
|
||||
At most one of these parameters can be used: :param destination:, :param destination_dir:
|
||||
|
||||
:param file_id: str
|
||||
:param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||
:param timeout: int
|
||||
:param chunk_size: int
|
||||
:param seek: bool - go to start of file when downloading is finished
|
||||
:param destination_dir: directory for saving files
|
||||
:param make_dirs: Make dirs if not exist
|
||||
:return: destination
|
||||
"""
|
||||
file = await self.get_file(file_id)
|
||||
return await self.download_file(file_path=file.file_path, destination=destination,
|
||||
timeout=timeout, chunk_size=chunk_size, seek=seek)
|
||||
timeout=timeout, chunk_size=chunk_size, seek=seek,
|
||||
destination_dir=destination_dir, make_dirs=make_dirs)
|
||||
|
||||
# === Getting updates ===
|
||||
# https://core.telegram.org/bots/api#getting-updates
|
||||
|
|
@ -257,6 +270,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
disable_web_page_preview: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -289,6 +303,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -310,26 +328,44 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
payload = generate_payload(**locals())
|
||||
if self.parse_mode and entities is None:
|
||||
payload.setdefault('parse_mode', self.parse_mode)
|
||||
if self.disable_web_page_preview:
|
||||
payload.setdefault('disable_web_page_preview', self.disable_web_page_preview)
|
||||
|
||||
result = await self.request(api.Methods.SEND_MESSAGE, payload)
|
||||
return types.Message(**result)
|
||||
|
||||
async def forward_message(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
from_chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer,
|
||||
disable_notification: typing.Optional[base.Boolean] = None) -> types.Message:
|
||||
async def forward_message(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
from_chat_id: typing.Union[base.Integer, base.String],
|
||||
message_id: base.Integer,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to forward messages of any kind.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#forwardmessage
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param from_chat_id: Unique identifier for the chat where the original message was sent
|
||||
|
||||
:param from_chat_id: Unique identifier for the chat where the
|
||||
original message was sent
|
||||
:type from_chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
|
||||
:param disable_notification: Sends the message silently. Users
|
||||
will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
:param message_id: Message identifier in the chat specified in from_chat_id
|
||||
|
||||
:param protect_content: Protects the contents of the forwarded
|
||||
message from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param message_id: Message identifier in the chat specified in
|
||||
from_chat_id
|
||||
:type message_id: :obj:`base.Integer`
|
||||
|
||||
:return: On success, the sent Message is returned
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
|
|
@ -346,6 +382,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -388,6 +425,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original
|
||||
message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
|
@ -409,7 +450,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
reply_markup = prepare_arg(reply_markup)
|
||||
caption_entities = prepare_arg(caption_entities)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
if self.parse_mode and caption_entities is None:
|
||||
payload.setdefault('parse_mode', self.parse_mode)
|
||||
|
||||
|
|
@ -423,6 +463,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -455,6 +496,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -493,6 +538,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
title: typing.Optional[base.String] = None,
|
||||
thumb: typing.Union[base.InputFile, base.String, None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -540,6 +586,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -577,6 +627,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
disable_content_type_detection: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -622,6 +673,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original
|
||||
message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
|
@ -664,12 +719,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
supports_streaming: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
types.ForceReply, None] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send video files, Telegram clients support mp4 videos
|
||||
(other formats may be sent as Document).
|
||||
|
|
@ -711,6 +768,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -750,6 +811,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -801,6 +863,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -837,6 +903,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None,
|
||||
duration: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -876,6 +943,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -909,12 +980,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
length: typing.Optional[base.Integer] = None,
|
||||
thumb: typing.Union[base.InputFile, base.String, None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
types.ForceReply, None] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
|
||||
Use this method to send video messages.
|
||||
|
|
@ -939,6 +1012,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -967,6 +1044,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
chat_id: typing.Union[base.Integer, base.String],
|
||||
media: typing.Union[types.MediaGroup, typing.List],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
) -> typing.List[types.Message]:
|
||||
|
|
@ -990,6 +1068,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the messages are a reply, ID of the original
|
||||
message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
|
@ -1005,9 +1087,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
if isinstance(media, list):
|
||||
media = types.MediaGroup(media)
|
||||
|
||||
# check MediaGroup quantity
|
||||
if 2 > len(media.media) > 10:
|
||||
raise ValidationError("Media group must include 2-10 items")
|
||||
# Check MediaGroup quantity
|
||||
if not (1 <= len(media.media) <= 10):
|
||||
raise ValidationError("Media group must include 2-10 items as written in docs, but also it works with 1 element")
|
||||
|
||||
files = dict(media.get_files())
|
||||
|
||||
|
|
@ -1024,12 +1106,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
heading: typing.Optional[base.Integer] = None,
|
||||
proximity_alert_radius: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
types.ForceReply, None] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send point on the map.
|
||||
|
||||
|
|
@ -1063,6 +1147,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -1188,6 +1276,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
google_place_id: typing.Optional[base.String] = None,
|
||||
google_place_type: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -1233,6 +1322,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original
|
||||
message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
|
@ -1262,12 +1355,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
last_name: typing.Optional[base.String] = None,
|
||||
vcard: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
types.ForceReply, None] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send phone contacts.
|
||||
|
||||
|
|
@ -1291,6 +1386,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -1331,6 +1430,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
None] = None,
|
||||
is_closed: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -1398,6 +1498,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original
|
||||
message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[Integer]`
|
||||
|
|
@ -1430,6 +1534,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
async def send_dice(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
emoji: typing.Optional[base.String] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
|
|
@ -1458,6 +1563,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -1801,6 +1910,55 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
|
||||
return await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload)
|
||||
|
||||
@removed_argument("until_date", "2.19")
|
||||
async def ban_chat_sender_chat(
|
||||
self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
sender_chat_id: base.Integer,
|
||||
):
|
||||
"""Ban a channel chat in a supergroup or a channel.
|
||||
|
||||
Until the chat is unbanned, the owner of the banned chat won't
|
||||
be able to send messages on behalf of any of their channels.
|
||||
The bot must be an administrator in the supergroup or channel
|
||||
for this to work and must have the appropriate administrator
|
||||
rights. Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#banchatsenderchat
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:param sender_chat_id: Unique identifier of the target sender
|
||||
chat
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.BAN_CHAT_SENDER_CHAT, payload)
|
||||
|
||||
async def unban_chat_sender_chat(
|
||||
self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
sender_chat_id: base.Integer,
|
||||
):
|
||||
"""Unban a previously banned channel chat in a supergroup or
|
||||
channel.
|
||||
|
||||
The bot must be an administrator for this to work and must have
|
||||
the appropriate administrator rights. Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#unbanchatsenderchat
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:param sender_chat_id: Unique identifier of the target sender
|
||||
chat
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.UNBAN_CHAT_SENDER_CHAT, payload)
|
||||
|
||||
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
permissions: types.ChatPermissions) -> base.Boolean:
|
||||
"""
|
||||
|
|
@ -1840,6 +1998,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date: typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None] = None,
|
||||
member_limit: typing.Optional[base.Integer] = None,
|
||||
name: typing.Optional[base.String] = None,
|
||||
creates_join_request: typing.Optional[base.Boolean] = None,
|
||||
) -> types.ChatInviteLink:
|
||||
"""
|
||||
Use this method to create an additional invite link for a chat.
|
||||
|
|
@ -1861,6 +2021,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
simultaneously after joining the chat via this invite link; 1-99999
|
||||
:type member_limit: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
:param name: Invite link name; 0-32 characters
|
||||
:type name: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param creates_join_request: True, if users joining the chat via the link need
|
||||
to be approved by chat administrators. If True, member_limit can't be specified
|
||||
:type creates_join_request: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:return: the new invite link as ChatInviteLink object.
|
||||
:rtype: :obj:`types.ChatInviteLink`
|
||||
"""
|
||||
|
|
@ -1876,6 +2043,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date: typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None] = None,
|
||||
member_limit: typing.Optional[base.Integer] = None,
|
||||
name: typing.Optional[base.String] = None,
|
||||
creates_join_request: typing.Optional[base.Boolean] = None,
|
||||
) -> types.ChatInviteLink:
|
||||
"""
|
||||
Use this method to edit a non-primary invite link created by the bot.
|
||||
|
|
@ -1899,6 +2068,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
simultaneously after joining the chat via this invite link; 1-99999
|
||||
:type member_limit: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
:param name: Invite link name; 0-32 characters
|
||||
:type name: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param creates_join_request: True, if users joining the chat via the link need
|
||||
to be approved by chat administrators. If True, member_limit can't be specified
|
||||
:type creates_join_request: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:return: edited invite link as a ChatInviteLink object.
|
||||
"""
|
||||
expire_date = prepare_arg(expire_date)
|
||||
|
|
@ -1929,6 +2106,59 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def approve_chat_join_request(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to approve a chat join request.
|
||||
The bot must be an administrator in the chat for this to work and must have the
|
||||
can_invite_users administrator right.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#approvechatjoinrequest
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
(in the format @channelusername)
|
||||
:type chat_id: typing.Union[base.Integer, base.String]
|
||||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: base.Integer
|
||||
|
||||
:return:
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.APPROVE_CHAT_JOIN_REQUEST, payload)
|
||||
|
||||
async def decline_chat_join_request(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to decline a chat join request.
|
||||
The bot must be an administrator in the chat for this to work and
|
||||
must have the can_invite_users administrator right.
|
||||
Returns True on success.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#declinechatjoinrequest
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
(in the format @channelusername)
|
||||
:type chat_id: typing.Union[base.Integer, base.String]
|
||||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: base.Integer
|
||||
|
||||
:return:
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.DECLINE_CHAT_JOIN_REQUEST, payload)
|
||||
|
||||
async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
photo: base.InputFile) -> base.Boolean:
|
||||
"""
|
||||
|
|
@ -2129,7 +2359,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
return types.Chat(**result)
|
||||
|
||||
async def get_chat_administrators(self, chat_id: typing.Union[base.Integer, base.String]
|
||||
) -> typing.List[types.ChatMember]:
|
||||
) -> typing.List[
|
||||
typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]:
|
||||
|
||||
"""
|
||||
Use this method to get a list of administrators in a chat.
|
||||
|
||||
|
|
@ -2407,6 +2639,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
payload = generate_payload(**locals())
|
||||
if self.parse_mode and entities is None:
|
||||
payload.setdefault('parse_mode', self.parse_mode)
|
||||
if self.disable_web_page_preview:
|
||||
payload.setdefault('disable_web_page_preview', self.disable_web_page_preview)
|
||||
|
||||
result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload)
|
||||
if isinstance(result, bool):
|
||||
|
|
@ -2594,12 +2828,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
async def send_sticker(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
sticker: typing.Union[base.InputFile, base.String],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
types.ForceReply, None] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send .webp stickers.
|
||||
|
||||
|
|
@ -2614,6 +2850,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -2910,6 +3150,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
send_email_to_provider: typing.Optional[base.Boolean] = None,
|
||||
is_flexible: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None,
|
||||
|
|
@ -3009,6 +3250,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
@ -3129,6 +3374,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
chat_id: base.Integer,
|
||||
game_short_name: base.String,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None,
|
||||
|
|
@ -3148,6 +3394,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import json
|
||||
import pathlib
|
||||
import pickle
|
||||
import typing
|
||||
|
||||
from aiogram.utils import json
|
||||
from .memory import MemoryStorage
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ This module has redis storage for finite-state machine based on `aioredis <https
|
|||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import aioredis
|
||||
|
||||
from ...dispatcher.storage import BaseStorage
|
||||
from ...utils import json
|
||||
from ...utils.deprecated import deprecated
|
||||
|
||||
STATE_KEY = 'state'
|
||||
STATE_DATA_KEY = 'data'
|
||||
|
|
@ -35,17 +37,19 @@ class RedisStorage(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
@deprecated("`RedisStorage` will be removed in aiogram v3.0. "
|
||||
"Use `RedisStorage2` instead.", stacklevel=3)
|
||||
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db = db
|
||||
self._password = password
|
||||
self._ssl = ssl
|
||||
self._loop = loop or asyncio.get_event_loop()
|
||||
self._kwargs = kwargs
|
||||
|
||||
self._redis: typing.Optional[aioredis.RedisConnection] = None
|
||||
self._connection_lock = asyncio.Lock(loop=self._loop)
|
||||
self._redis: typing.Optional["aioredis.RedisConnection"] = None
|
||||
self._connection_lock = asyncio.Lock()
|
||||
|
||||
async def close(self):
|
||||
async with self._connection_lock:
|
||||
|
|
@ -58,7 +62,7 @@ class RedisStorage(BaseStorage):
|
|||
return await self._redis.wait_closed()
|
||||
return True
|
||||
|
||||
async def redis(self) -> aioredis.RedisConnection:
|
||||
async def redis(self) -> "aioredis.RedisConnection":
|
||||
"""
|
||||
Get Redis connection
|
||||
"""
|
||||
|
|
@ -67,7 +71,6 @@ class RedisStorage(BaseStorage):
|
|||
if self._redis is None or self._redis.closed:
|
||||
self._redis = await aioredis.create_connection((self._host, self._port),
|
||||
db=self._db, password=self._password, ssl=self._ssl,
|
||||
loop=self._loop,
|
||||
**self._kwargs)
|
||||
return self._redis
|
||||
|
||||
|
|
@ -204,6 +207,138 @@ class RedisStorage(BaseStorage):
|
|||
await self.set_record(chat=chat, user=user, state=record['state'], data=record_bucket, bucket=bucket)
|
||||
|
||||
|
||||
class AioRedisAdapterBase(ABC):
|
||||
"""Base aioredis adapter class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = "localhost",
|
||||
port: int = 6379,
|
||||
db: typing.Optional[int] = None,
|
||||
password: typing.Optional[str] = None,
|
||||
ssl: typing.Optional[bool] = None,
|
||||
pool_size: int = 10,
|
||||
loop: typing.Optional[asyncio.AbstractEventLoop] = None,
|
||||
prefix: str = "fsm",
|
||||
state_ttl: typing.Optional[int] = None,
|
||||
data_ttl: typing.Optional[int] = None,
|
||||
bucket_ttl: typing.Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db = db
|
||||
self._password = password
|
||||
self._ssl = ssl
|
||||
self._pool_size = pool_size
|
||||
self._kwargs = kwargs
|
||||
self._prefix = (prefix,)
|
||||
|
||||
self._state_ttl = state_ttl
|
||||
self._data_ttl = data_ttl
|
||||
self._bucket_ttl = bucket_ttl
|
||||
|
||||
self._redis: typing.Optional["aioredis.Redis"] = None
|
||||
self._connection_lock = asyncio.Lock()
|
||||
|
||||
@abstractmethod
|
||||
async def get_redis(self) -> aioredis.Redis:
|
||||
"""Get Redis connection."""
|
||||
pass
|
||||
|
||||
async def close(self):
|
||||
"""Grace shutdown."""
|
||||
pass
|
||||
|
||||
async def wait_closed(self):
|
||||
"""Wait for grace shutdown finishes."""
|
||||
pass
|
||||
|
||||
async def set(self, name, value, ex=None, **kwargs):
|
||||
"""Set the value at key ``name`` to ``value``."""
|
||||
if ex == 0:
|
||||
ex = None
|
||||
return await self._redis.set(name, value, ex=ex, **kwargs)
|
||||
|
||||
async def get(self, name, **kwargs):
|
||||
"""Return the value at key ``name`` or None."""
|
||||
return await self._redis.get(name, **kwargs)
|
||||
|
||||
async def delete(self, *names):
|
||||
"""Delete one or more keys specified by ``names``"""
|
||||
return await self._redis.delete(*names)
|
||||
|
||||
async def keys(self, pattern, **kwargs):
|
||||
"""Returns a list of keys matching ``pattern``."""
|
||||
return await self._redis.keys(pattern, **kwargs)
|
||||
|
||||
async def flushdb(self):
|
||||
"""Delete all keys in the current database."""
|
||||
return await self._redis.flushdb()
|
||||
|
||||
|
||||
class AioRedisAdapterV1(AioRedisAdapterBase):
|
||||
"""Redis adapter for aioredis v1."""
|
||||
|
||||
async def get_redis(self) -> aioredis.Redis:
|
||||
"""Get Redis connection."""
|
||||
async with self._connection_lock: # to prevent race
|
||||
if self._redis is None or self._redis.closed:
|
||||
self._redis = await aioredis.create_redis_pool(
|
||||
(self._host, self._port),
|
||||
db=self._db,
|
||||
password=self._password,
|
||||
ssl=self._ssl,
|
||||
minsize=1,
|
||||
maxsize=self._pool_size,
|
||||
**self._kwargs,
|
||||
)
|
||||
return self._redis
|
||||
|
||||
async def close(self):
|
||||
async with self._connection_lock:
|
||||
if self._redis and not self._redis.closed:
|
||||
self._redis.close()
|
||||
|
||||
async def wait_closed(self):
|
||||
async with self._connection_lock:
|
||||
if self._redis:
|
||||
return await self._redis.wait_closed()
|
||||
return True
|
||||
|
||||
async def get(self, name, **kwargs):
|
||||
return await self._redis.get(name, encoding="utf8", **kwargs)
|
||||
|
||||
async def set(self, name, value, ex=None, **kwargs):
|
||||
if ex == 0:
|
||||
ex = None
|
||||
return await self._redis.set(name, value, expire=ex, **kwargs)
|
||||
|
||||
async def keys(self, pattern, **kwargs):
|
||||
"""Returns a list of keys matching ``pattern``."""
|
||||
return await self._redis.keys(pattern, encoding="utf8", **kwargs)
|
||||
|
||||
|
||||
class AioRedisAdapterV2(AioRedisAdapterBase):
|
||||
"""Redis adapter for aioredis v2."""
|
||||
|
||||
async def get_redis(self) -> aioredis.Redis:
|
||||
"""Get Redis connection."""
|
||||
async with self._connection_lock: # to prevent race
|
||||
if self._redis is None:
|
||||
self._redis = aioredis.Redis(
|
||||
host=self._host,
|
||||
port=self._port,
|
||||
db=self._db,
|
||||
password=self._password,
|
||||
ssl=self._ssl,
|
||||
max_connections=self._pool_size,
|
||||
decode_responses=True,
|
||||
**self._kwargs,
|
||||
)
|
||||
return self._redis
|
||||
|
||||
|
||||
class RedisStorage2(BaseStorage):
|
||||
"""
|
||||
Busted Redis-base storage for FSM.
|
||||
|
|
@ -224,19 +359,28 @@ class RedisStorage2(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
|
||||
ssl=None, pool_size=10, loop=None, prefix='fsm',
|
||||
state_ttl: int = 0,
|
||||
data_ttl: int = 0,
|
||||
bucket_ttl: int = 0,
|
||||
**kwargs):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = "localhost",
|
||||
port: int = 6379,
|
||||
db: typing.Optional[int] = None,
|
||||
password: typing.Optional[str] = None,
|
||||
ssl: typing.Optional[bool] = None,
|
||||
pool_size: int = 10,
|
||||
loop: typing.Optional[asyncio.AbstractEventLoop] = None,
|
||||
prefix: str = "fsm",
|
||||
state_ttl: typing.Optional[int] = None,
|
||||
data_ttl: typing.Optional[int] = None,
|
||||
bucket_ttl: typing.Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db = db
|
||||
self._password = password
|
||||
self._ssl = ssl
|
||||
self._pool_size = pool_size
|
||||
self._loop = loop or asyncio.get_event_loop()
|
||||
self._kwargs = kwargs
|
||||
self._prefix = (prefix,)
|
||||
|
||||
|
|
@ -244,49 +388,62 @@ class RedisStorage2(BaseStorage):
|
|||
self._data_ttl = data_ttl
|
||||
self._bucket_ttl = bucket_ttl
|
||||
|
||||
self._redis: typing.Optional[aioredis.RedisConnection] = None
|
||||
self._connection_lock = asyncio.Lock(loop=self._loop)
|
||||
self._redis: typing.Optional[AioRedisAdapterBase] = None
|
||||
self._connection_lock = asyncio.Lock()
|
||||
|
||||
@deprecated("This method will be removed in aiogram v3.0. "
|
||||
"You should use your own instance of Redis.", stacklevel=3)
|
||||
async def redis(self) -> aioredis.Redis:
|
||||
"""
|
||||
Get Redis connection
|
||||
"""
|
||||
# Use thread-safe asyncio Lock because this method without that is not safe
|
||||
async with self._connection_lock:
|
||||
if self._redis is None or self._redis.closed:
|
||||
self._redis = await aioredis.create_redis_pool((self._host, self._port),
|
||||
db=self._db, password=self._password, ssl=self._ssl,
|
||||
minsize=1, maxsize=self._pool_size,
|
||||
loop=self._loop, **self._kwargs)
|
||||
adapter = await self._get_adapter()
|
||||
return await adapter.get_redis()
|
||||
|
||||
async def _get_adapter(self) -> AioRedisAdapterBase:
|
||||
"""Get adapter based on aioredis version."""
|
||||
if self._redis is None:
|
||||
redis_version = int(aioredis.__version__.split(".")[0])
|
||||
connection_data = dict(
|
||||
host=self._host,
|
||||
port=self._port,
|
||||
db=self._db,
|
||||
password=self._password,
|
||||
ssl=self._ssl,
|
||||
pool_size=self._pool_size,
|
||||
**self._kwargs,
|
||||
)
|
||||
if redis_version == 1:
|
||||
self._redis = AioRedisAdapterV1(**connection_data)
|
||||
elif redis_version == 2:
|
||||
self._redis = AioRedisAdapterV2(**connection_data)
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported aioredis version: {redis_version}")
|
||||
await self._redis.get_redis()
|
||||
return self._redis
|
||||
|
||||
def generate_key(self, *parts):
|
||||
return ':'.join(self._prefix + tuple(map(str, parts)))
|
||||
|
||||
async def close(self):
|
||||
async with self._connection_lock:
|
||||
if self._redis and not self._redis.closed:
|
||||
self._redis.close()
|
||||
if self._redis:
|
||||
return await self._redis.close()
|
||||
|
||||
async def wait_closed(self):
|
||||
async with self._connection_lock:
|
||||
if self._redis:
|
||||
return await self._redis.wait_closed()
|
||||
return True
|
||||
if self._redis:
|
||||
await self._redis.wait_closed()
|
||||
self._redis = None
|
||||
|
||||
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_KEY)
|
||||
redis = await self.redis()
|
||||
return await redis.get(key, encoding='utf8') or self.resolve_state(default)
|
||||
redis = await self._get_adapter()
|
||||
return await redis.get(key) or self.resolve_state(default)
|
||||
|
||||
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[dict] = None) -> typing.Dict:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_DATA_KEY)
|
||||
redis = await self.redis()
|
||||
raw_result = await redis.get(key, encoding='utf8')
|
||||
redis = await self._get_adapter()
|
||||
raw_result = await redis.get(key)
|
||||
if raw_result:
|
||||
return json.loads(raw_result)
|
||||
return default or {}
|
||||
|
|
@ -295,19 +452,19 @@ class RedisStorage2(BaseStorage):
|
|||
state: typing.Optional[typing.AnyStr] = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_KEY)
|
||||
redis = await self.redis()
|
||||
redis = await self._get_adapter()
|
||||
if state is None:
|
||||
await redis.delete(key)
|
||||
else:
|
||||
await redis.set(key, self.resolve_state(state), expire=self._state_ttl)
|
||||
await redis.set(key, self.resolve_state(state), ex=self._state_ttl)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_DATA_KEY)
|
||||
redis = await self.redis()
|
||||
redis = await self._get_adapter()
|
||||
if data:
|
||||
await redis.set(key, json.dumps(data), expire=self._data_ttl)
|
||||
await redis.set(key, json.dumps(data), ex=self._data_ttl)
|
||||
else:
|
||||
await redis.delete(key)
|
||||
|
||||
|
|
@ -326,8 +483,8 @@ class RedisStorage2(BaseStorage):
|
|||
default: typing.Optional[dict] = None) -> typing.Dict:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
|
||||
redis = await self.redis()
|
||||
raw_result = await redis.get(key, encoding='utf8')
|
||||
redis = await self._get_adapter()
|
||||
raw_result = await redis.get(key)
|
||||
if raw_result:
|
||||
return json.loads(raw_result)
|
||||
return default or {}
|
||||
|
|
@ -336,9 +493,9 @@ class RedisStorage2(BaseStorage):
|
|||
bucket: typing.Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
|
||||
redis = await self.redis()
|
||||
redis = await self._get_adapter()
|
||||
if bucket:
|
||||
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
|
||||
await redis.set(key, json.dumps(bucket), ex=self._bucket_ttl)
|
||||
else:
|
||||
await redis.delete(key)
|
||||
|
||||
|
|
@ -358,13 +515,13 @@ class RedisStorage2(BaseStorage):
|
|||
:param full: clean DB or clean only states
|
||||
:return:
|
||||
"""
|
||||
conn = await self.redis()
|
||||
redis = await self._get_adapter()
|
||||
|
||||
if full:
|
||||
await conn.flushdb()
|
||||
await redis.flushdb()
|
||||
else:
|
||||
keys = await conn.keys(self.generate_key('*'))
|
||||
await conn.delete(*keys)
|
||||
keys = await redis.keys(self.generate_key('*'))
|
||||
await redis.delete(*keys)
|
||||
|
||||
async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
|
||||
"""
|
||||
|
|
@ -372,10 +529,10 @@ class RedisStorage2(BaseStorage):
|
|||
|
||||
:return: list of tuples where first element is chat id and second is user id
|
||||
"""
|
||||
conn = await self.redis()
|
||||
redis = await self._get_adapter()
|
||||
result = []
|
||||
|
||||
keys = await conn.keys(self.generate_key('*', '*', STATE_KEY), encoding='utf8')
|
||||
keys = await redis.keys(self.generate_key('*', '*', STATE_KEY))
|
||||
for item in keys:
|
||||
*_, chat, user, _ = item.split(':')
|
||||
result.append((chat, user))
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class EnvironmentMiddleware(BaseMiddleware):
|
|||
data.update(
|
||||
bot=dp.bot,
|
||||
dispatcher=dp,
|
||||
loop=dp.loop or asyncio.get_event_loop()
|
||||
loop=asyncio.get_event_loop()
|
||||
)
|
||||
if self.context:
|
||||
data.update(self.context)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import copy
|
||||
import weakref
|
||||
|
||||
from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware
|
||||
from aiogram.dispatcher.storage import FSMContext
|
||||
|
|
@ -8,10 +7,6 @@ from aiogram.dispatcher.storage import FSMContext
|
|||
class FSMMiddleware(LifetimeControllerMiddleware):
|
||||
skip_patterns = ['error', 'update']
|
||||
|
||||
def __init__(self):
|
||||
super(FSMMiddleware, self).__init__()
|
||||
self._proxies = weakref.WeakKeyDictionary()
|
||||
|
||||
async def pre_process(self, obj, data, *args):
|
||||
proxy = await FSMSStorageProxy.create(self.manager.dispatcher.current_state())
|
||||
data['state_data'] = proxy
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import time
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.dispatcher.middlewares import BaseMiddleware
|
||||
|
|
@ -89,13 +88,15 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
|
||||
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
|
||||
if callback_query.message:
|
||||
message = callback_query.message
|
||||
text = (f"Received callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
f"for message [ID:{message.message_id}] "
|
||||
f"in chat [{message.chat.type}:{message.chat.id}] "
|
||||
f"with data: {callback_query.data}")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
if message.from_user:
|
||||
text = f"{text} originally posted by user [ID:{message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
|
|
@ -106,14 +107,16 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
|
||||
async def on_post_process_callback_query(self, callback_query, results, data: dict):
|
||||
if callback_query.message:
|
||||
message = callback_query.message
|
||||
text = (f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
f"for message [ID:{message.message_id}] "
|
||||
f"in chat [{message.chat.type}:{message.chat.id}] "
|
||||
f"with data: {callback_query.data}")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
if message.from_user:
|
||||
text = f"{text} originally posted by user [ID:{message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
|
|
@ -180,6 +183,16 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat_member "
|
||||
f"for user [ID:{chat_member_update.from_user.id}]")
|
||||
|
||||
async def on_pre_chat_join_request(self, chat_join_request, data):
|
||||
self.logger.info(f"Received chat join request "
|
||||
f"for user [ID:{chat_join_request.from_user.id}] "
|
||||
f"in chat [ID:{chat_join_request.chat.id}]")
|
||||
|
||||
async def on_post_chat_join_request(self, chat_join_request, results, data):
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat join request "
|
||||
f"for user [ID:{chat_join_request.from_user.id}] "
|
||||
f"in chat [ID:{chat_join_request.chat.id}]")
|
||||
|
||||
|
||||
class LoggingFilter(logging.Filter):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory = FiltersFactory(self)
|
||||
|
||||
self.bot: Bot = bot
|
||||
if loop is not None:
|
||||
_ensure_loop(loop)
|
||||
self._main_loop = loop
|
||||
self.storage = storage
|
||||
self.run_tasks_by_default = run_tasks_by_default
|
||||
|
|
@ -80,6 +78,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.poll_answer_handlers = Handler(self, middleware_key='poll_answer')
|
||||
self.my_chat_member_handlers = Handler(self, middleware_key='my_chat_member')
|
||||
self.chat_member_handlers = Handler(self, middleware_key='chat_member')
|
||||
self.chat_join_request_handlers = Handler(self, middleware_key='chat_join_request')
|
||||
self.errors_handlers = Handler(self, once=False, middleware_key='error')
|
||||
|
||||
self.middleware = MiddlewareManager(self)
|
||||
|
|
@ -103,10 +102,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
@property
|
||||
def _close_waiter(self) -> "asyncio.Future":
|
||||
if self._dispatcher_close_waiter is None:
|
||||
if self._main_loop is not None:
|
||||
self._dispatcher_close_waiter = self._main_loop.create_future()
|
||||
else:
|
||||
self._dispatcher_close_waiter = asyncio.get_event_loop().create_future()
|
||||
self._dispatcher_close_waiter = asyncio.get_event_loop().create_future()
|
||||
return self._dispatcher_close_waiter
|
||||
|
||||
def _setup_filters(self):
|
||||
|
|
@ -159,13 +155,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.errors_handlers,
|
||||
])
|
||||
filters_factory.bind(AdminFilter, event_handlers=[
|
||||
self.message_handlers,
|
||||
self.message_handlers,
|
||||
self.edited_message_handlers,
|
||||
self.channel_post_handlers,
|
||||
self.channel_post_handlers,
|
||||
self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers,
|
||||
self.callback_query_handlers,
|
||||
self.inline_query_handlers,
|
||||
self.chat_member_handlers,
|
||||
self.chat_join_request_handlers,
|
||||
])
|
||||
filters_factory.bind(IDFilter, event_handlers=[
|
||||
self.message_handlers,
|
||||
|
|
@ -176,6 +173,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.inline_query_handlers,
|
||||
self.chat_member_handlers,
|
||||
self.my_chat_member_handlers,
|
||||
self.chat_join_request_handlers,
|
||||
])
|
||||
filters_factory.bind(IsReplyFilter, event_handlers=[
|
||||
self.message_handlers,
|
||||
|
|
@ -202,7 +200,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers,
|
||||
self.my_chat_member_handlers,
|
||||
self.chat_member_handlers
|
||||
self.chat_member_handlers,
|
||||
self.chat_join_request_handlers,
|
||||
])
|
||||
filters_factory.bind(MediaGroupFilter, event_handlers=[
|
||||
self.message_handlers,
|
||||
|
|
@ -305,6 +304,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
types.ChatMemberUpdated.set_current(update.chat_member)
|
||||
types.User.set_current(update.chat_member.from_user)
|
||||
return await self.chat_member_handlers.notify(update.chat_member)
|
||||
if update.chat_join_request:
|
||||
types.ChatJoinRequest.set_current(update.chat_join_request)
|
||||
types.Chat.set_current(update.chat_join_request.chat)
|
||||
types.User.set_current(update.chat_join_request.from_user)
|
||||
return await self.chat_join_request_handlers.notify(update.chat_join_request)
|
||||
except Exception as e:
|
||||
err = await self.errors_handlers.notify(update, e)
|
||||
if err:
|
||||
|
|
@ -326,10 +330,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
return await self.bot.delete_webhook()
|
||||
|
||||
def _loop_create_task(self, coro):
|
||||
if self._main_loop is None:
|
||||
return asyncio.create_task(coro)
|
||||
_ensure_loop(self._main_loop)
|
||||
return self._main_loop.create_task(coro)
|
||||
return asyncio.create_task(coro)
|
||||
|
||||
async def start_polling(self,
|
||||
timeout=20,
|
||||
|
|
@ -394,7 +395,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
log.debug(f"Received {len(updates)} updates.")
|
||||
offset = updates[-1].update_id + 1
|
||||
|
||||
self._loop_create_task(self._process_polling_updates(updates, fast))
|
||||
asyncio.create_task(self._process_polling_updates(updates, fast))
|
||||
|
||||
if relax:
|
||||
await asyncio.sleep(relax)
|
||||
|
|
@ -980,14 +981,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
|
||||
|
||||
def decorator(callback):
|
||||
self.register_poll_handler(callback, *custom_filters, run_task=run_task,
|
||||
**kwargs)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_poll_answer_handler(self, callback, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Register handler for poll_answer
|
||||
|
|
@ -1007,7 +1008,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
*custom_filters,
|
||||
**kwargs)
|
||||
self.poll_answer_handlers.register(self._wrap_async_task(callback, run_task), filters_set)
|
||||
|
||||
|
||||
def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Decorator for poll_answer handler
|
||||
|
|
@ -1026,7 +1027,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
def decorator(callback):
|
||||
self.register_poll_answer_handler(callback, *custom_filters, run_task=run_task,
|
||||
**kwargs)
|
||||
**kwargs)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
|
@ -1143,6 +1144,62 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
return decorator
|
||||
|
||||
def register_chat_join_request_handler(self,
|
||||
callback: typing.Callable,
|
||||
*custom_filters,
|
||||
run_task: typing.Optional[bool] = None,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Register handler for chat_join_request
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
dp.register_chat_join_request(some_chat_join_request)
|
||||
|
||||
:param callback:
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
filters_set = self.filters_factory.resolve(
|
||||
self.chat_join_request_handlers,
|
||||
*custom_filters,
|
||||
**kwargs,
|
||||
)
|
||||
self.chat_join_request_handlers.register(
|
||||
handler=self._wrap_async_task(callback, run_task),
|
||||
filters=filters_set,
|
||||
)
|
||||
|
||||
def chat_join_request_handler(self, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Decorator for chat_join_request handler
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@dp.chat_join_request()
|
||||
async def some_handler(chat_member: types.ChatJoinRequest)
|
||||
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
|
||||
def decorator(callback):
|
||||
self.register_chat_join_request_handler(
|
||||
callback,
|
||||
*custom_filters,
|
||||
run_task=run_task,
|
||||
**kwargs,
|
||||
)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs):
|
||||
"""
|
||||
Register handler for errors
|
||||
|
|
@ -1336,15 +1393,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
try:
|
||||
response = task.result()
|
||||
except Exception as e:
|
||||
self._loop_create_task(
|
||||
asyncio.create_task(
|
||||
self.errors_handlers.notify(types.Update.get_current(), e))
|
||||
else:
|
||||
if isinstance(response, BaseResponse):
|
||||
self._loop_create_task(response.execute_response(self.bot))
|
||||
asyncio.create_task(response.execute_response(self.bot))
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
task = self._loop_create_task(func(*args, **kwargs))
|
||||
task = asyncio.create_task(func(*args, **kwargs))
|
||||
task.add_done_callback(process_response)
|
||||
|
||||
return wrapper
|
||||
|
|
@ -1382,6 +1439,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
:param chat_id: chat id
|
||||
:return: decorator
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args, **kwargs):
|
||||
|
|
@ -1411,6 +1469,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
asyncio.get_running_loop().run_in_executor(
|
||||
None, partial_func
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import typing
|
|||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
||||
from typing import Any, Dict, Iterable, Optional, Union
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
|
|
|
|||
|
|
@ -408,6 +408,8 @@ class FSMContextProxy:
|
|||
def update(self, data=None, **kwargs):
|
||||
self._check_closed()
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
self._data.update(data, **kwargs)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
|
|
@ -461,7 +463,6 @@ class DisabledStorage(BaseStorage):
|
|||
"""
|
||||
Empty storage. Use it if you don't want to use Finite-State Machine
|
||||
"""
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
|
|
@ -499,6 +500,25 @@ class DisabledStorage(BaseStorage):
|
|||
data: typing.Dict = None):
|
||||
self._warn()
|
||||
|
||||
async def get_bucket(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[dict] = None) -> typing.Dict:
|
||||
self._warn()
|
||||
return {}
|
||||
|
||||
async def set_bucket(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
bucket: typing.Dict = None):
|
||||
self._warn()
|
||||
|
||||
async def update_bucket(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
bucket: typing.Dict = None, **kwargs):
|
||||
self._warn()
|
||||
|
||||
@staticmethod
|
||||
def _warn():
|
||||
warn(f"You haven’t set any storage yet so no states and no data will be saved. \n"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ allow_ip(TELEGRAM_SUBNET_1, TELEGRAM_SUBNET_2)
|
|||
|
||||
class WebhookRequestHandler(web.View):
|
||||
"""
|
||||
Simple Wehhook request handler for aiohttp web server.
|
||||
Simple Webhook request handler for aiohttp web server.
|
||||
|
||||
You need to register that in app:
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ class WebhookRequestHandler(web.View):
|
|||
web_response = web.Response(text='ok')
|
||||
|
||||
if self.request.app.get('RETRY_AFTER', None):
|
||||
web_response.headers['Retry-After'] = self.request.app['RETRY_AFTER']
|
||||
web_response.headers['Retry-After'] = str(self.request.app['RETRY_AFTER'])
|
||||
|
||||
return web_response
|
||||
|
||||
|
|
@ -168,14 +168,14 @@ class WebhookRequestHandler(web.View):
|
|||
:return:
|
||||
"""
|
||||
dispatcher = self.get_dispatcher()
|
||||
loop = dispatcher.loop or asyncio.get_event_loop()
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Analog of `asyncio.wait_for` but without cancelling task
|
||||
waiter = loop.create_future()
|
||||
timeout_handle = loop.call_later(RESPONSE_TIMEOUT, asyncio.tasks._release_waiter, waiter)
|
||||
cb = functools.partial(asyncio.tasks._release_waiter, waiter)
|
||||
|
||||
fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update), loop=loop)
|
||||
fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update))
|
||||
fut.add_done_callback(cb)
|
||||
|
||||
try:
|
||||
|
|
@ -207,7 +207,7 @@ class WebhookRequestHandler(web.View):
|
|||
TimeoutWarning)
|
||||
|
||||
dispatcher = self.get_dispatcher()
|
||||
loop = dispatcher.loop or asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
try:
|
||||
results = task.result()
|
||||
|
|
@ -217,7 +217,7 @@ class WebhookRequestHandler(web.View):
|
|||
else:
|
||||
response = self.get_response(results)
|
||||
if response is not None:
|
||||
asyncio.ensure_future(response.execute_response(dispatcher.bot), loop=loop)
|
||||
asyncio.ensure_future(response.execute_response(dispatcher.bot))
|
||||
|
||||
def get_response(self, results):
|
||||
"""
|
||||
|
|
@ -241,6 +241,8 @@ class WebhookRequestHandler(web.View):
|
|||
# For reverse proxy (nginx)
|
||||
forwarded_for = self.request.headers.get('X-Forwarded-For', None)
|
||||
if forwarded_for:
|
||||
# get the left-most ip when there is multiple ips (request got through multiple proxy/load balancers)
|
||||
forwarded_for = forwarded_for.split(",")[0]
|
||||
return forwarded_for, _check_ip(forwarded_for)
|
||||
|
||||
# For default method
|
||||
|
|
@ -433,6 +435,18 @@ class DisableWebPagePreviewMixin:
|
|||
setattr(self, 'disable_web_page_preview', True)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _global_disable_web_page_preview():
|
||||
"""
|
||||
Detect global disable web page preview value
|
||||
|
||||
:return:
|
||||
"""
|
||||
from aiogram import Bot
|
||||
bot = Bot.get_current()
|
||||
if bot is not None:
|
||||
return bot.disable_web_page_preview
|
||||
|
||||
|
||||
class ParseModeMixin:
|
||||
def as_html(self):
|
||||
|
|
@ -504,6 +518,8 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio
|
|||
text = ''
|
||||
if parse_mode is None:
|
||||
parse_mode = self._global_parse_mode()
|
||||
if disable_web_page_preview is None:
|
||||
disable_web_page_preview = self._global_disable_web_page_preview()
|
||||
|
||||
self.chat_id = chat_id
|
||||
self.text = text
|
||||
|
|
@ -1589,6 +1605,8 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin):
|
|||
"""
|
||||
if parse_mode is None:
|
||||
parse_mode = self._global_parse_mode()
|
||||
if disable_web_page_preview is None:
|
||||
disable_web_page_preview = self._global_disable_web_page_preview()
|
||||
|
||||
self.chat_id = chat_id
|
||||
self.message_id = message_id
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from .callback_game import CallbackGame
|
|||
from .callback_query import CallbackQuery
|
||||
from .chat import Chat, ChatActions, ChatType
|
||||
from .chat_invite_link import ChatInviteLink
|
||||
from .chat_join_request import ChatJoinRequest
|
||||
from .chat_location import ChatLocation
|
||||
from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberBanned, \
|
||||
ChatMemberLeft, ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, \
|
||||
|
|
@ -102,6 +103,7 @@ __all__ = (
|
|||
'Chat',
|
||||
'ChatActions',
|
||||
'ChatInviteLink',
|
||||
'ChatJoinRequest',
|
||||
'ChatLocation',
|
||||
'ChatMember',
|
||||
'ChatMemberStatus',
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
Abstract class for telegram objects
|
||||
"""
|
||||
|
||||
def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None:
|
||||
def __init__(self, conf: typing.Dict[str, typing.Any] = None, **kwargs: typing.Any) -> None:
|
||||
"""
|
||||
Deserialize object
|
||||
|
||||
|
|
@ -211,6 +211,15 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
"""
|
||||
return self.as_json()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return object readable representation.
|
||||
|
||||
Example: <ObjectName {"id": 123456}>
|
||||
:return: object class name and object data as a string
|
||||
"""
|
||||
return f"<{type(self).__name__} {self}>"
|
||||
|
||||
def __getitem__(self, item: typing.Union[str, int]) -> typing.Any:
|
||||
"""
|
||||
Item getter (by key)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ class BotCommand(base.TelegramObject):
|
|||
description: base.String = fields.Field()
|
||||
|
||||
def __init__(self, command: base.String, description: base.String):
|
||||
super(BotCommand, self).__init__(command=command, description=description)
|
||||
super(BotCommand, self).__init__(command=command, description=description)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import typing
|
|||
from . import base, fields
|
||||
from .chat_invite_link import ChatInviteLink
|
||||
from .chat_location import ChatLocation
|
||||
from .chat_member import ChatMember
|
||||
from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberOwner
|
||||
from .chat_permissions import ChatPermissions
|
||||
from .chat_photo import ChatPhoto
|
||||
from .input_file import InputFile
|
||||
from ..utils import helper, markdown
|
||||
from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar
|
||||
from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar, removed_argument
|
||||
|
||||
|
||||
class Chat(base.TelegramObject):
|
||||
|
|
@ -30,12 +30,14 @@ class Chat(base.TelegramObject):
|
|||
all_members_are_administrators: base.Boolean = fields.Field()
|
||||
photo: ChatPhoto = fields.Field(base=ChatPhoto)
|
||||
bio: base.String = fields.Field()
|
||||
has_private_forwards: base.Boolean = fields.Field()
|
||||
description: base.String = fields.Field()
|
||||
invite_link: base.String = fields.Field()
|
||||
pinned_message: 'Message' = fields.Field(base='Message')
|
||||
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
|
||||
slow_mode_delay: base.Integer = fields.Field()
|
||||
message_auto_delete_time: base.Integer = fields.Field()
|
||||
has_protected_content: base.Boolean = fields.Field()
|
||||
sticker_set_name: base.String = fields.Field()
|
||||
can_set_sticker_set: base.Boolean = fields.Field()
|
||||
linked_chat_id: base.Integer = fields.Field()
|
||||
|
|
@ -470,7 +472,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.leave_chat(self.id)
|
||||
|
||||
async def get_administrators(self) -> typing.List[ChatMember]:
|
||||
async def get_administrators(self) -> typing.List[typing.Union[ChatMemberOwner, ChatMemberAdministrator]]:
|
||||
"""
|
||||
Use this method to get a list of administrators in a chat.
|
||||
|
||||
|
|
@ -480,7 +482,7 @@ class Chat(base.TelegramObject):
|
|||
chat administrators except other bots.
|
||||
If the chat is a group or a supergroup and no administrators were appointed,
|
||||
only the creator will be returned.
|
||||
:rtype: :obj:`typing.List[types.ChatMember]`
|
||||
:rtype: :obj:`typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]`
|
||||
"""
|
||||
return await self.bot.get_chat_administrators(self.id)
|
||||
|
||||
|
|
@ -497,7 +499,7 @@ class Chat(base.TelegramObject):
|
|||
|
||||
async def get_members_count(self) -> base.Integer:
|
||||
"""Renamed to get_member_count."""
|
||||
return await self.get_member_count(self.id)
|
||||
return await self.get_member_count()
|
||||
|
||||
async def get_member(self, user_id: base.Integer) -> ChatMember:
|
||||
"""
|
||||
|
|
@ -621,6 +623,27 @@ class Chat(base.TelegramObject):
|
|||
message_id=message_id,
|
||||
)
|
||||
|
||||
@removed_argument("until_date", "2.19")
|
||||
async def ban_sender_chat(
|
||||
self,
|
||||
sender_chat_id: base.Integer,
|
||||
):
|
||||
"""Shortcut for banChatSenderChat method."""
|
||||
return await self.bot.ban_chat_sender_chat(
|
||||
chat_id=self.id,
|
||||
sender_chat_id=sender_chat_id,
|
||||
)
|
||||
|
||||
async def unban_sender_chat(
|
||||
self,
|
||||
sender_chat_id: base.Integer,
|
||||
):
|
||||
"""Shortcut for unbanChatSenderChat method."""
|
||||
return await self.bot.unban_chat_sender_chat(
|
||||
chat_id=self.id,
|
||||
sender_chat_id=sender_chat_id,
|
||||
)
|
||||
|
||||
def __int__(self):
|
||||
return self.id
|
||||
|
||||
|
|
@ -742,6 +765,7 @@ class ChatActions(helper.Helper):
|
|||
FIND_LOCATION: str = helper.Item() # find_location
|
||||
RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note
|
||||
UPLOAD_VIDEO_NOTE: str = helper.Item() # upload_video_note
|
||||
CHOOSE_STICKER: str = helper.Item() # choose_sticker
|
||||
|
||||
@classmethod
|
||||
async def _do(cls, action: str, sleep=None):
|
||||
|
|
@ -882,3 +906,13 @@ class ChatActions(helper.Helper):
|
|||
:return:
|
||||
"""
|
||||
await cls._do(cls.UPLOAD_VIDEO_NOTE, sleep)
|
||||
|
||||
@classmethod
|
||||
async def choose_sticker(cls, sleep=None):
|
||||
"""
|
||||
Do choose sticker
|
||||
|
||||
:param sleep: sleep timeout
|
||||
:return:
|
||||
"""
|
||||
await cls._do(cls.CHOOSE_STICKER, sleep)
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ class ChatInviteLink(base.TelegramObject):
|
|||
creator: User = fields.Field(base=User)
|
||||
is_primary: base.Boolean = fields.Field()
|
||||
is_revoked: base.Boolean = fields.Field()
|
||||
name: base.String = fields.Field()
|
||||
expire_date: datetime = fields.DateTimeField()
|
||||
member_limit: base.Integer = fields.Field()
|
||||
creates_join_request: datetime = fields.DateTimeField()
|
||||
pending_join_request_count: base.Integer = fields.Field()
|
||||
|
|
|
|||
33
aiogram/types/chat_join_request.py
Normal file
33
aiogram/types/chat_join_request.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from datetime import datetime
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .chat import Chat
|
||||
from .chat_invite_link import ChatInviteLink
|
||||
from .user import User
|
||||
|
||||
|
||||
class ChatJoinRequest(base.TelegramObject):
|
||||
"""
|
||||
Represents a join request sent to a chat.
|
||||
|
||||
https://core.telegram.org/bots/api#chatinvitelink
|
||||
"""
|
||||
|
||||
chat: Chat = fields.Field(base=Chat)
|
||||
from_user: User = fields.Field(alias="from", base=User)
|
||||
date: datetime = fields.DateTimeField()
|
||||
bio: base.String = fields.Field()
|
||||
invite_link: ChatInviteLink = fields.Field(base=ChatInviteLink)
|
||||
|
||||
async def approve(self) -> base.Boolean:
|
||||
return await self.bot.approve_chat_join_request(
|
||||
chat_id=self.chat.id,
|
||||
user_id=self.from_user.id,
|
||||
)
|
||||
|
||||
async def decline(self) -> base.Boolean:
|
||||
return await self.bot.decline_chat_join_request(
|
||||
chat_id=self.chat.id,
|
||||
user_id=self.from_user.id,
|
||||
)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import datetime
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from . import base, fields
|
||||
from .user import User
|
||||
|
|
@ -29,6 +28,8 @@ class ChatMemberStatus(helper.Helper):
|
|||
def is_chat_creator(cls, role: str) -> bool:
|
||||
return role == cls.CREATOR
|
||||
|
||||
is_chat_owner = is_chat_creator
|
||||
|
||||
@classmethod
|
||||
def is_chat_admin(cls, role: str) -> bool:
|
||||
return role in (cls.ADMINISTRATOR, cls.CREATOR)
|
||||
|
|
@ -38,7 +39,7 @@ class ChatMemberStatus(helper.Helper):
|
|||
return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED)
|
||||
|
||||
@classmethod
|
||||
def get_class_by_status(cls, status: str) -> Optional["ChatMember"]:
|
||||
def get_class_by_status(cls, status: str) -> typing.Optional[typing.Type["ChatMember"]]:
|
||||
return {
|
||||
cls.OWNER: ChatMemberOwner,
|
||||
cls.ADMINISTRATOR: ChatMemberAdministrator,
|
||||
|
|
@ -69,7 +70,9 @@ class ChatMember(base.TelegramObject):
|
|||
return self.user.id
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, **kwargs) -> "ChatMember":
|
||||
def resolve(cls, **kwargs) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator",
|
||||
"ChatMemberMember", "ChatMemberRestricted",
|
||||
"ChatMemberLeft", "ChatMemberBanned"]:
|
||||
status = kwargs.get("status")
|
||||
mapping = {
|
||||
ChatMemberStatus.OWNER: ChatMemberOwner,
|
||||
|
|
@ -89,12 +92,16 @@ class ChatMember(base.TelegramObject):
|
|||
def to_object(cls,
|
||||
data: typing.Dict[str, typing.Any],
|
||||
conf: typing.Dict[str, typing.Any] = None
|
||||
) -> "ChatMember":
|
||||
return cls.resolve(**data)
|
||||
) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator",
|
||||
"ChatMemberMember", "ChatMemberRestricted",
|
||||
"ChatMemberLeft", "ChatMemberBanned"]:
|
||||
return cls.resolve(conf=conf, **data)
|
||||
|
||||
def is_chat_creator(self) -> bool:
|
||||
return ChatMemberStatus.is_chat_creator(self.status)
|
||||
|
||||
is_chat_owner = is_chat_creator
|
||||
|
||||
def is_chat_admin(self) -> bool:
|
||||
return ChatMemberStatus.is_chat_admin(self.status)
|
||||
|
||||
|
|
@ -113,6 +120,22 @@ class ChatMemberOwner(ChatMember):
|
|||
custom_title: base.String = fields.Field()
|
||||
is_anonymous: base.Boolean = fields.Field()
|
||||
|
||||
# Next fields cannot be received from API but
|
||||
# it useful for compatibility and cleaner code:
|
||||
# >>> if member.is_admin() and member.can_promote_members:
|
||||
# >>> await message.reply('You can promote me')
|
||||
can_be_edited: base.Boolean = fields.ConstField(False)
|
||||
can_manage_chat: base.Boolean = fields.ConstField(True)
|
||||
can_post_messages: base.Boolean = fields.ConstField(True)
|
||||
can_edit_messages: base.Boolean = fields.ConstField(True)
|
||||
can_delete_messages: base.Boolean = fields.ConstField(True)
|
||||
can_manage_voice_chats: base.Boolean = fields.ConstField(True)
|
||||
can_restrict_members: base.Boolean = fields.ConstField(True)
|
||||
can_promote_members: base.Boolean = fields.ConstField(True)
|
||||
can_change_info: base.Boolean = fields.ConstField(True)
|
||||
can_invite_users: base.Boolean = fields.ConstField(True)
|
||||
can_pin_messages: base.Boolean = fields.ConstField(True)
|
||||
|
||||
|
||||
class ChatMemberAdministrator(ChatMember):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import abc
|
|||
import datetime
|
||||
import weakref
|
||||
|
||||
__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists')
|
||||
__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists', 'ConstField')
|
||||
|
||||
|
||||
class BaseField(metaclass=abc.ABCMeta):
|
||||
|
|
@ -118,7 +118,7 @@ class Field(BaseField):
|
|||
|
||||
class ListField(Field):
|
||||
"""
|
||||
Field contains list ob objects
|
||||
The field contains a list of objects
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
@ -162,7 +162,7 @@ class ListOfLists(Field):
|
|||
|
||||
class DateTimeField(Field):
|
||||
"""
|
||||
In this field st_ored datetime
|
||||
In this field stored datetime
|
||||
|
||||
in: unixtime
|
||||
out: datetime
|
||||
|
|
@ -192,5 +192,13 @@ class TextField(Field):
|
|||
|
||||
def deserialize(self, value, parent=None):
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise TypeError(f"Field '{self.alias}' should be str not {type(value).__name__}")
|
||||
raise TypeError(f"Field {self.alias!r} should be str not {type(value).__name__!r}")
|
||||
return value
|
||||
|
||||
|
||||
class ConstField(Field):
|
||||
def __init__(self, default=None, **kwargs):
|
||||
super(ConstField, self).__init__(default=default, **kwargs)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
raise TypeError(f"Field {self.alias!r} is not mutable")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from typing import Union, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ class InputFile(base.TelegramObject):
|
|||
https://core.telegram.org/bots/api#inputfile
|
||||
"""
|
||||
|
||||
def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path], filename=None, conf=None):
|
||||
def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path, '_WebPipe'], filename=None, conf=None):
|
||||
"""
|
||||
|
||||
:param path_or_bytesio:
|
||||
|
|
@ -118,7 +118,7 @@ class InputFile(base.TelegramObject):
|
|||
if filename is None:
|
||||
filename = pipe.name
|
||||
|
||||
return cls(pipe, filename, chunk_size)
|
||||
return cls(pipe, filename)
|
||||
|
||||
def save(self, filename, chunk_size=CHUNK_SIZE):
|
||||
"""
|
||||
|
|
@ -159,8 +159,8 @@ class _WebPipe:
|
|||
self.url = url
|
||||
self.chunk_size = chunk_size
|
||||
|
||||
self._session: aiohttp.ClientSession = None
|
||||
self._response: aiohttp.ClientResponse = None
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self._response: Optional[aiohttp.ClientResponse] = None
|
||||
self._reader = None
|
||||
self._name = None
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ class _WebPipe:
|
|||
|
||||
async def close(self):
|
||||
if self._response and not self._response.closed:
|
||||
await self._response.close()
|
||||
self._response.close()
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
if self._lock.locked():
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ class InputTextMessageContent(InputMessageContent):
|
|||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def safe_get_disable_web_page_preview(self):
|
||||
try:
|
||||
return self.bot.disable_web_page_preview
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_text: base.String,
|
||||
|
|
@ -163,6 +169,8 @@ class InputTextMessageContent(InputMessageContent):
|
|||
):
|
||||
if parse_mode is None:
|
||||
parse_mode = self.safe_get_parse_mode()
|
||||
if disable_web_page_preview is None:
|
||||
disable_web_page_preview = self.safe_get_disable_web_page_preview()
|
||||
|
||||
super().__init__(
|
||||
message_text=message_text,
|
||||
|
|
|
|||
|
|
@ -58,9 +58,11 @@ class Message(base.TelegramObject):
|
|||
forward_from_message_id: base.Integer = fields.Field()
|
||||
forward_signature: base.String = fields.Field()
|
||||
forward_date: datetime.datetime = fields.DateTimeField()
|
||||
is_automatic_forward: base.Boolean = fields.Field()
|
||||
reply_to_message: Message = fields.Field(base="Message")
|
||||
via_bot: User = fields.Field(base=User)
|
||||
edit_date: datetime.datetime = fields.DateTimeField()
|
||||
has_protected_content: base.Boolean = fields.Field()
|
||||
media_group_id: base.String = fields.Field()
|
||||
author_signature: base.String = fields.Field()
|
||||
forward_sender_name: base.String = fields.Field()
|
||||
|
|
@ -313,6 +315,7 @@ class Message(base.TelegramObject):
|
|||
entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_web_page_preview: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -343,6 +346,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -365,6 +372,7 @@ class Message(base.TelegramObject):
|
|||
entities=entities,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -377,6 +385,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -409,6 +418,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -431,6 +444,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -447,6 +461,7 @@ class Message(base.TelegramObject):
|
|||
title: typing.Optional[base.String] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -495,6 +510,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -521,6 +540,7 @@ class Message(base.TelegramObject):
|
|||
title=title,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -537,6 +557,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -587,6 +608,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -613,6 +638,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -627,6 +653,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_content_type_detection: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -670,6 +697,10 @@ class Message(base.TelegramObject):
|
|||
notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -696,6 +727,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
disable_content_type_detection=disable_content_type_detection,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -713,6 +745,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
supports_streaming: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -762,6 +795,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -789,6 +826,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
supports_streaming=supports_streaming,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -802,6 +840,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
duration: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -841,6 +880,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -864,6 +907,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
duration=duration,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -876,6 +920,7 @@ class Message(base.TelegramObject):
|
|||
length: typing.Optional[base.Integer] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -908,6 +953,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -930,6 +979,7 @@ class Message(base.TelegramObject):
|
|||
length=length,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -939,6 +989,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
media: typing.Union[MediaGroup, typing.List],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply: base.Boolean = False,
|
||||
) -> typing.List[Message]:
|
||||
|
|
@ -957,6 +1008,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -971,6 +1026,7 @@ class Message(base.TelegramObject):
|
|||
self.chat.id,
|
||||
media=media,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
)
|
||||
|
|
@ -981,6 +1037,7 @@ class Message(base.TelegramObject):
|
|||
longitude: base.Float,
|
||||
live_period: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
horizontal_accuracy: typing.Optional[base.Float] = None,
|
||||
heading: typing.Optional[base.Integer] = None,
|
||||
|
|
@ -1024,6 +1081,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1048,6 +1109,7 @@ class Message(base.TelegramObject):
|
|||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1064,6 +1126,7 @@ class Message(base.TelegramObject):
|
|||
google_place_id: typing.Optional[base.String] = None,
|
||||
google_place_type: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1108,6 +1171,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1136,6 +1203,7 @@ class Message(base.TelegramObject):
|
|||
google_place_id=google_place_id,
|
||||
google_place_type=google_place_type,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1147,6 +1215,7 @@ class Message(base.TelegramObject):
|
|||
first_name: base.String,
|
||||
last_name: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1174,6 +1243,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1195,6 +1268,7 @@ class Message(base.TelegramObject):
|
|||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1204,6 +1278,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
sticker: typing.Union[base.InputFile, base.String],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1225,6 +1300,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1244,6 +1323,7 @@ class Message(base.TelegramObject):
|
|||
chat_id=self.chat.id,
|
||||
sticker=sticker,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1264,6 +1344,7 @@ class Message(base.TelegramObject):
|
|||
close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None,
|
||||
is_closed: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1330,6 +1411,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1362,6 +1447,7 @@ class Message(base.TelegramObject):
|
|||
close_date=close_date,
|
||||
is_closed=is_closed,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1371,6 +1457,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
emoji: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1397,6 +1484,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1418,6 +1509,7 @@ class Message(base.TelegramObject):
|
|||
chat_id=self.chat.id,
|
||||
emoji=emoji,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1454,6 +1546,7 @@ class Message(base.TelegramObject):
|
|||
entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_web_page_preview: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1484,6 +1577,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1506,6 +1603,7 @@ class Message(base.TelegramObject):
|
|||
entities=entities,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1518,6 +1616,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1550,6 +1649,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1572,6 +1675,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1588,6 +1692,7 @@ class Message(base.TelegramObject):
|
|||
title: typing.Optional[base.String] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1636,6 +1741,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1662,6 +1771,7 @@ class Message(base.TelegramObject):
|
|||
title=title,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1678,6 +1788,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1728,6 +1839,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1754,6 +1869,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1768,6 +1884,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_content_type_detection: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1811,6 +1928,10 @@ class Message(base.TelegramObject):
|
|||
notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1837,6 +1958,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
disable_content_type_detection=disable_content_type_detection,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1854,6 +1976,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
supports_streaming: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1903,6 +2026,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -1930,6 +2057,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
supports_streaming=supports_streaming,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1943,6 +2071,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
duration: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -1982,6 +2111,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2005,6 +2138,7 @@ class Message(base.TelegramObject):
|
|||
caption_entities=caption_entities,
|
||||
duration=duration,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2017,6 +2151,7 @@ class Message(base.TelegramObject):
|
|||
length: typing.Optional[base.Integer] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2049,6 +2184,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2071,6 +2210,7 @@ class Message(base.TelegramObject):
|
|||
length=length,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2080,6 +2220,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
media: typing.Union[MediaGroup, typing.List],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply: base.Boolean = True,
|
||||
) -> typing.List[Message]:
|
||||
|
|
@ -2098,6 +2239,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2112,6 +2257,7 @@ class Message(base.TelegramObject):
|
|||
self.chat.id,
|
||||
media=media,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
)
|
||||
|
|
@ -2122,6 +2268,7 @@ class Message(base.TelegramObject):
|
|||
longitude: base.Float,
|
||||
live_period: typing.Optional[base.Integer] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
horizontal_accuracy: typing.Optional[base.Float] = None,
|
||||
heading: typing.Optional[base.Integer] = None,
|
||||
proximity_alert_radius: typing.Optional[base.Integer] = None,
|
||||
|
|
@ -2164,6 +2311,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -2184,6 +2335,7 @@ class Message(base.TelegramObject):
|
|||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
|
|
@ -2199,6 +2351,7 @@ class Message(base.TelegramObject):
|
|||
google_place_id: typing.Optional[base.String] = None,
|
||||
google_place_type: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2243,6 +2396,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2271,6 +2428,7 @@ class Message(base.TelegramObject):
|
|||
google_place_id=google_place_id,
|
||||
google_place_type=google_place_type,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2282,6 +2440,7 @@ class Message(base.TelegramObject):
|
|||
first_name: base.String,
|
||||
last_name: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2309,6 +2468,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2330,6 +2493,7 @@ class Message(base.TelegramObject):
|
|||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2350,6 +2514,7 @@ class Message(base.TelegramObject):
|
|||
close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None,
|
||||
is_closed: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2416,6 +2581,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2448,6 +2617,7 @@ class Message(base.TelegramObject):
|
|||
close_date=close_date,
|
||||
is_closed=is_closed,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2457,6 +2627,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
sticker: typing.Union[base.InputFile, base.String],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2478,6 +2649,10 @@ class Message(base.TelegramObject):
|
|||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2497,6 +2672,7 @@ class Message(base.TelegramObject):
|
|||
chat_id=self.chat.id,
|
||||
sticker=sticker,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2506,6 +2682,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
emoji: typing.Optional[base.String] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[
|
||||
InlineKeyboardMarkup,
|
||||
|
|
@ -2532,6 +2709,10 @@ class Message(base.TelegramObject):
|
|||
a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of sent messages
|
||||
from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param allow_sending_without_reply: Pass True, if the message should be sent
|
||||
even if the specified replied-to message is not found
|
||||
:type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]`
|
||||
|
|
@ -2553,6 +2734,7 @@ class Message(base.TelegramObject):
|
|||
chat_id=self.chat.id,
|
||||
emoji=emoji,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2562,6 +2744,7 @@ class Message(base.TelegramObject):
|
|||
self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Forward this message
|
||||
|
|
@ -2570,13 +2753,23 @@ class Message(base.TelegramObject):
|
|||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:param protect_content: Protects the contents of the forwarded
|
||||
message from forwarding and saving
|
||||
:type protect_content: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:return: On success, the sent Message is returned
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
return await self.bot.forward_message(
|
||||
chat_id, self.chat.id, self.message_id, disable_notification
|
||||
chat_id=chat_id,
|
||||
from_chat_id=self.chat.id,
|
||||
message_id=self.message_id,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
)
|
||||
|
||||
async def edit_text(
|
||||
|
|
@ -2793,7 +2986,8 @@ class Message(base.TelegramObject):
|
|||
return await self.bot.delete_message(self.chat.id, self.message_id)
|
||||
|
||||
async def pin(
|
||||
self, disable_notification: typing.Optional[base.Boolean] = None,
|
||||
self,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to add a message to the list of pinned messages in a chat.
|
||||
|
|
@ -2811,7 +3005,10 @@ class Message(base.TelegramObject):
|
|||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.chat.pin_message(self.message_id, disable_notification)
|
||||
return await self.chat.pin_message(
|
||||
message_id=self.message_id,
|
||||
disable_notification=disable_notification,
|
||||
)
|
||||
|
||||
async def unpin(self) -> base.Boolean:
|
||||
"""
|
||||
|
|
@ -2834,6 +3031,7 @@ class Message(base.TelegramObject):
|
|||
self: Message,
|
||||
chat_id: typing.Union[str, int],
|
||||
disable_notification: typing.Optional[bool] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
disable_web_page_preview: typing.Optional[bool] = None,
|
||||
reply_to_message_id: typing.Optional[int] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
|
|
@ -2846,6 +3044,7 @@ class Message(base.TelegramObject):
|
|||
|
||||
:param chat_id:
|
||||
:param disable_notification:
|
||||
:param protect_content:
|
||||
:param disable_web_page_preview: for text messages only
|
||||
:param reply_to_message_id:
|
||||
:param allow_sending_without_reply:
|
||||
|
|
@ -2858,6 +3057,7 @@ class Message(base.TelegramObject):
|
|||
"reply_markup": reply_markup or self.reply_markup,
|
||||
"parse_mode": ParseMode.HTML,
|
||||
"disable_notification": disable_notification,
|
||||
"protect_content": protect_content,
|
||||
"reply_to_message_id": reply_to_message_id,
|
||||
}
|
||||
text = self.html_text if (self.text or self.caption) else None
|
||||
|
|
@ -2899,7 +3099,9 @@ class Message(base.TelegramObject):
|
|||
video_note=self.video_note.file_id, **kwargs
|
||||
)
|
||||
elif self.voice:
|
||||
return await self.bot.send_voice(voice=self.voice.file_id, **kwargs)
|
||||
return await self.bot.send_voice(
|
||||
voice=self.voice.file_id, caption=text, **kwargs
|
||||
)
|
||||
elif self.contact:
|
||||
kwargs.pop("parse_mode")
|
||||
return await self.bot.send_contact(
|
||||
|
|
@ -2952,6 +3154,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode: typing.Optional[base.String] = None,
|
||||
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
protect_content: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
|
|
@ -2967,6 +3170,7 @@ class Message(base.TelegramObject):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup
|
||||
|
|
@ -3039,10 +3243,10 @@ class ContentType(helper.Helper):
|
|||
GROUP_CHAT_CREATED = helper.Item() # group_chat_created
|
||||
PASSPORT_DATA = helper.Item() # passport_data
|
||||
PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered
|
||||
VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled
|
||||
VOICE_CHAT_STARTED = helper.Item() # voice_chat_started
|
||||
VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended
|
||||
VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited
|
||||
VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled
|
||||
VOICE_CHAT_STARTED = helper.Item() # voice_chat_started
|
||||
VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended
|
||||
VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited
|
||||
|
||||
UNKNOWN = helper.Item() # unknown
|
||||
ANY = helper.Item() # any
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ class MessageEntity(base.TelegramObject):
|
|||
:return: part of text
|
||||
"""
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
return text[self.offset : self.offset + self.length]
|
||||
return text[self.offset: self.offset + self.length]
|
||||
|
||||
entity_text = (
|
||||
text.encode("utf-16-le") if not isinstance(text, bytes) else text
|
||||
)
|
||||
entity_text = entity_text[self.offset * 2 : (self.offset + self.length) * 2]
|
||||
entity_text = entity_text[self.offset * 2: (self.offset + self.length) * 2]
|
||||
return entity_text.decode("utf-16-le")
|
||||
|
||||
@deprecated(
|
||||
|
|
@ -77,6 +77,9 @@ class MessageEntity(base.TelegramObject):
|
|||
if self.type == MessageEntityType.ITALIC:
|
||||
method = markdown.hitalic if as_html else markdown.italic
|
||||
return method(entity_text)
|
||||
if self.type == MessageEntityType.SPOILER:
|
||||
method = markdown.spoiler if as_html else markdown.hspoiler
|
||||
return method(entity_text)
|
||||
if self.type == MessageEntityType.PRE:
|
||||
method = markdown.hpre if as_html else markdown.pre
|
||||
return method(entity_text)
|
||||
|
|
@ -108,10 +111,11 @@ class MessageEntityType(helper.Helper):
|
|||
:key: PHONE_NUMBER
|
||||
:key: BOLD
|
||||
:key: ITALIC
|
||||
:key: CODE
|
||||
:key: PRE
|
||||
:key: UNDERLINE
|
||||
:key: STRIKETHROUGH
|
||||
:key: SPOILER
|
||||
:key: CODE
|
||||
:key: PRE
|
||||
:key: TEXT_LINK
|
||||
:key: TEXT_MENTION
|
||||
"""
|
||||
|
|
@ -127,9 +131,10 @@ class MessageEntityType(helper.Helper):
|
|||
PHONE_NUMBER = helper.Item() # phone_number
|
||||
BOLD = helper.Item() # bold - bold text
|
||||
ITALIC = helper.Item() # italic - italic text
|
||||
CODE = helper.Item() # code - monowidth string
|
||||
PRE = helper.Item() # pre - monowidth block
|
||||
UNDERLINE = helper.Item() # underline
|
||||
STRIKETHROUGH = helper.Item() # strikethrough
|
||||
SPOILER = helper.Item() # spoiler
|
||||
CODE = helper.Item() # code - monowidth string
|
||||
PRE = helper.Item() # pre - monowidth block
|
||||
TEXT_LINK = helper.Item() # text_link - for clickable text URLs
|
||||
TEXT_MENTION = helper.Item() # text_mention - for users without usernames
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import os
|
||||
import pathlib
|
||||
from io import IOBase
|
||||
from typing import Union, Optional
|
||||
|
||||
from aiogram.utils.deprecated import warn_deprecated
|
||||
|
||||
|
||||
class Downloadable:
|
||||
|
|
@ -7,32 +11,83 @@ class Downloadable:
|
|||
Mixin for files
|
||||
"""
|
||||
|
||||
async def download(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True):
|
||||
async def download(
|
||||
self,
|
||||
destination=None,
|
||||
timeout=30,
|
||||
chunk_size=65536,
|
||||
seek=True,
|
||||
make_dirs=True,
|
||||
*,
|
||||
destination_dir: Optional[Union[str, pathlib.Path]] = None,
|
||||
destination_file: Optional[Union[str, pathlib.Path, IOBase]] = None
|
||||
):
|
||||
"""
|
||||
Download file
|
||||
|
||||
:param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||
At most one of these parameters can be used: :param destination_dir:, :param destination_file:
|
||||
|
||||
:param destination: deprecated, use :param destination_dir: or :param destination_file: instead
|
||||
:param timeout: Integer
|
||||
:param chunk_size: Integer
|
||||
:param seek: Boolean - go to start of file when downloading is finished.
|
||||
:param make_dirs: Make dirs if not exist
|
||||
:param destination_dir: directory for saving files
|
||||
:param destination_file: path to the file or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||
:return: destination
|
||||
"""
|
||||
if destination:
|
||||
warn_deprecated(
|
||||
"destination parameter is deprecated, please use destination_dir or destination_file."
|
||||
)
|
||||
if destination_dir and destination_file:
|
||||
raise ValueError(
|
||||
"Use only one of the parameters: destination_dir or destination_file."
|
||||
)
|
||||
|
||||
file, destination = await self._prepare_destination(
|
||||
destination,
|
||||
destination_dir,
|
||||
destination_file,
|
||||
)
|
||||
|
||||
return await self.bot.download_file(
|
||||
file_path=file.file_path,
|
||||
destination=destination,
|
||||
timeout=timeout,
|
||||
chunk_size=chunk_size,
|
||||
seek=seek,
|
||||
make_dirs=make_dirs
|
||||
)
|
||||
|
||||
async def _prepare_destination(self, dest, destination_dir, destination_file):
|
||||
file = await self.get_file()
|
||||
|
||||
is_path = True
|
||||
if destination is None:
|
||||
if not(any((dest, destination_dir, destination_file))):
|
||||
destination = file.file_path
|
||||
elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination):
|
||||
destination = os.path.join(destination, file.file_path)
|
||||
|
||||
elif dest: # backward compatibility
|
||||
if isinstance(dest, IOBase):
|
||||
return file, dest
|
||||
if isinstance(dest, (str, pathlib.Path)) and os.path.isdir(dest):
|
||||
destination = os.path.join(dest, file.file_path)
|
||||
else:
|
||||
destination = dest
|
||||
|
||||
elif destination_dir:
|
||||
if isinstance(destination_dir, (str, pathlib.Path)):
|
||||
destination = os.path.join(destination_dir, file.file_path)
|
||||
else:
|
||||
raise TypeError("destination_dir must be str or pathlib.Path")
|
||||
else:
|
||||
is_path = False
|
||||
if isinstance(destination_file, IOBase):
|
||||
return file, destination_file
|
||||
elif isinstance(destination_file, (str, pathlib.Path)):
|
||||
destination = destination_file
|
||||
else:
|
||||
raise TypeError("destination_file must be str, pathlib.Path or io.IOBase type")
|
||||
|
||||
if is_path and make_dirs:
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
|
||||
return await self.bot.download_file(file_path=file.file_path, destination=destination, timeout=timeout,
|
||||
chunk_size=chunk_size, seek=seek)
|
||||
return file, destination
|
||||
|
||||
async def get_file(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -35,14 +35,17 @@ class ReplyKeyboardMarkup(base.TelegramObject):
|
|||
one_time_keyboard: base.Boolean = None,
|
||||
input_field_placeholder: base.String = None,
|
||||
selective: base.Boolean = None,
|
||||
row_width: base.Integer = 3):
|
||||
row_width: base.Integer = 3,
|
||||
conf=None):
|
||||
if conf is None:
|
||||
conf = {}
|
||||
super().__init__(
|
||||
keyboard=keyboard,
|
||||
resize_keyboard=resize_keyboard,
|
||||
one_time_keyboard=one_time_keyboard,
|
||||
input_field_placeholder=input_field_placeholder,
|
||||
selective=selective,
|
||||
conf={'row_width': row_width},
|
||||
conf={'row_width': row_width, **conf},
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -129,7 +132,9 @@ class KeyboardButton(base.TelegramObject):
|
|||
|
||||
class ReplyKeyboardRemove(base.TelegramObject):
|
||||
"""
|
||||
Upon receiving a message with this object, Telegram clients will remove the current custom keyboard and display the default letter-keyboard. By default, custom keyboards are displayed until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup).
|
||||
Upon receiving a message with this object, Telegram clients will remove the current custom keyboard
|
||||
and display the default letter-keyboard. By default, custom keyboards are displayed until a new keyboard is sent by a bot.
|
||||
An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup).
|
||||
|
||||
https://core.telegram.org/bots/api#replykeyboardremove
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
|
|||
|
||||
Source: https://core.telegram.org/bots/api#deletestickerfromset
|
||||
|
||||
:param sticker: File identifier of the sticker
|
||||
:type sticker: :obj:`base.String`
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from .message import Message
|
|||
from .poll import Poll, PollAnswer
|
||||
from .pre_checkout_query import PreCheckoutQuery
|
||||
from .shipping_query import ShippingQuery
|
||||
from .chat_join_request import ChatJoinRequest
|
||||
from ..utils import helper, deprecated
|
||||
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ class Update(base.TelegramObject):
|
|||
poll_answer: PollAnswer = fields.Field(base=PollAnswer)
|
||||
my_chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated)
|
||||
chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated)
|
||||
chat_join_request: ChatJoinRequest = fields.Field(base=ChatJoinRequest)
|
||||
|
||||
def __hash__(self):
|
||||
return self.update_id
|
||||
|
|
@ -66,6 +68,7 @@ class AllowedUpdates(helper.Helper):
|
|||
POLL_ANSWER = helper.ListItem() # poll_answer
|
||||
MY_CHAT_MEMBER = helper.ListItem() # my_chat_member
|
||||
CHAT_MEMBER = helper.ListItem() # chat_member
|
||||
CHAT_JOIN_REQUEST = helper.ListItem() # chat_join_request
|
||||
|
||||
CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar(
|
||||
"`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. "
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ class User(base.TelegramObject):
|
|||
return getattr(self, '_locale')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def url(self) -> str:
|
||||
return f"tg://user?id={self.id}"
|
||||
|
||||
def get_mention(self, name=None, as_html=None):
|
||||
def get_mention(self, name: Optional[str] = None, as_html: Optional[bool] = None) -> str:
|
||||
if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html':
|
||||
as_html = True
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from datetime import datetime
|
|||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .user import User
|
||||
|
||||
|
||||
class VoiceChatScheduled(base.TelegramObject):
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ class CallbackData:
|
|||
raise ValueError("Prefix can't be empty")
|
||||
if sep in prefix:
|
||||
raise ValueError(f"Separator {sep!r} can't be used in prefix")
|
||||
if not parts:
|
||||
raise TypeError('Parts were not passed!')
|
||||
|
||||
self.prefix = prefix
|
||||
self.sep = sep
|
||||
|
|
@ -64,8 +62,6 @@ class CallbackData:
|
|||
if value is not None and not isinstance(value, str):
|
||||
value = str(value)
|
||||
|
||||
if not value:
|
||||
raise ValueError(f"Value for part {part!r} can't be empty!'")
|
||||
if self.sep in value:
|
||||
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,57 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
|
|||
return decorator
|
||||
|
||||
|
||||
def removed_argument(name: str, until_version: str, stacklevel: int = 3):
|
||||
"""
|
||||
A meta-decorator to mark an argument as removed.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@removed_argument("until_date", "3.0") # stacklevel=3 by default
|
||||
def some_function(user_id, chat_id=None):
|
||||
print(f"user_id={user_id}, chat_id={chat_id}")
|
||||
|
||||
:param name:
|
||||
:param until_version: the version in which the argument is scheduled to be removed
|
||||
:param stacklevel: leave it to default if it's the first decorator used.
|
||||
Increment with any new decorator used.
|
||||
:return: decorator
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
is_coroutine = asyncio.iscoroutinefunction(func)
|
||||
|
||||
def _handling(kwargs):
|
||||
"""
|
||||
Returns updated version of kwargs.
|
||||
"""
|
||||
routine_type = 'coroutine' if is_coroutine else 'function'
|
||||
if name in kwargs:
|
||||
warn_deprecated(
|
||||
f"In {routine_type} {func.__name__!r} argument {name!r} "
|
||||
f"is planned to be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel,
|
||||
)
|
||||
kwargs = kwargs.copy()
|
||||
del kwargs[name]
|
||||
return kwargs
|
||||
|
||||
if is_coroutine:
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args, **kwargs):
|
||||
kwargs = _handling(kwargs)
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
kwargs = _handling(kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
_OwnerCls = TypeVar("_OwnerCls")
|
||||
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ class Executor:
|
|||
:param timeout:
|
||||
"""
|
||||
self._prepare_polling()
|
||||
loop: asyncio.AbstractEventLoop = self.loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
loop.run_until_complete(self._startup_polling())
|
||||
|
|
@ -365,7 +365,8 @@ class Executor:
|
|||
self.dispatcher.stop_polling()
|
||||
await self.dispatcher.storage.close()
|
||||
await self.dispatcher.storage.wait_closed()
|
||||
await self.dispatcher.bot.session.close()
|
||||
session = await self.dispatcher.bot.get_session()
|
||||
await session.close()
|
||||
|
||||
async def _startup_polling(self):
|
||||
await self._welcome()
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class HelperMode(Helper):
|
|||
@classmethod
|
||||
def _snake_case(cls, text):
|
||||
"""
|
||||
Transform text to snake cale (Based on SCREAMING_SNAKE_CASE)
|
||||
Transform text to snake case (Based on SCREAMING_SNAKE_CASE)
|
||||
|
||||
:param text:
|
||||
:return:
|
||||
|
|
|
|||
|
|
@ -20,28 +20,46 @@ for json_lib in (RAPIDJSON, UJSON):
|
|||
break
|
||||
|
||||
if mode == RAPIDJSON:
|
||||
|
||||
def dump(*args, **kwargs):
|
||||
return json.dump(*args, **kwargs)
|
||||
|
||||
def load(*args, **kwargs):
|
||||
return json.load(*args, **kwargs)
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data, number_mode=json.NM_NATIVE)
|
||||
|
||||
|
||||
elif mode == UJSON:
|
||||
|
||||
def dump(*args, **kwargs):
|
||||
return json.dump(*args, **kwargs)
|
||||
|
||||
def load(*args, **kwargs):
|
||||
return json.load(*args, **kwargs)
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
else:
|
||||
import json
|
||||
|
||||
def dump(*args, **kwargs):
|
||||
return json.dump(*args, **kwargs)
|
||||
|
||||
def load(*args, **kwargs):
|
||||
return json.load(*args, **kwargs)
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ MD_SYMBOLS = (
|
|||
(LIST_MD_SYMBOLS[1], LIST_MD_SYMBOLS[1]),
|
||||
(LIST_MD_SYMBOLS[2], LIST_MD_SYMBOLS[2]),
|
||||
(LIST_MD_SYMBOLS[2] * 3 + "\n", "\n" + LIST_MD_SYMBOLS[2] * 3),
|
||||
("||", "||"),
|
||||
("<b>", "</b>"),
|
||||
("<i>", "</i>"),
|
||||
("<code>", "</code>"),
|
||||
("<pre>", "</pre>"),
|
||||
('<span class="tg-spoiler">', "</span>"),
|
||||
("<tg-spoiler>", "</tg-spoiler>"),
|
||||
)
|
||||
|
||||
HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """}
|
||||
|
|
@ -113,6 +116,32 @@ def hitalic(*content, sep=" ") -> str:
|
|||
)
|
||||
|
||||
|
||||
def spoiler(*content, sep=" ") -> str:
|
||||
"""
|
||||
Make spoiler text (Markdown)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return markdown_decoration.spoiler(
|
||||
value=markdown_decoration.quote(_join(*content, sep=sep))
|
||||
)
|
||||
|
||||
|
||||
def hspoiler(*content, sep=" ") -> str:
|
||||
"""
|
||||
Make spoiler text (HTML)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return html_decoration.spoiler(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
)
|
||||
|
||||
|
||||
def code(*content, sep=" ") -> str:
|
||||
"""
|
||||
Make mono-width text (Markdown)
|
||||
|
|
@ -247,4 +276,4 @@ def hide_link(url: str) -> str:
|
|||
:param url:
|
||||
:return:
|
||||
"""
|
||||
return f'<a href="{url}">​</a>'
|
||||
return f'<a href="{url}">⁠</a>'
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ class TextDecoration(ABC):
|
|||
:return:
|
||||
"""
|
||||
if entity.type in {"bot_command", "url", "mention", "phone_number"}:
|
||||
# This entities should not be changed
|
||||
# These entities should not be changed
|
||||
return text
|
||||
if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}:
|
||||
if entity.type in {"bold", "italic", "spoiler", "code", "underline", "strikethrough"}:
|
||||
return cast(str, getattr(self, entity.type)(value=text))
|
||||
if entity.type == "pre":
|
||||
return (
|
||||
|
|
@ -115,6 +115,10 @@ class TextDecoration(ABC):
|
|||
def italic(self, value: str) -> str: # pragma: no cover
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def spoiler(self, value: str) -> str: # pragma: no cover
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def code(self, value: str) -> str: # pragma: no cover
|
||||
pass
|
||||
|
|
@ -150,6 +154,9 @@ class HtmlDecoration(TextDecoration):
|
|||
def italic(self, value: str) -> str:
|
||||
return f"<i>{value}</i>"
|
||||
|
||||
def spoiler(self, value: str) -> str:
|
||||
return f'<span class="tg-spoiler">{value}</span>'
|
||||
|
||||
def code(self, value: str) -> str:
|
||||
return f"<code>{value}</code>"
|
||||
|
||||
|
|
@ -181,6 +188,9 @@ class MarkdownDecoration(TextDecoration):
|
|||
def italic(self, value: str) -> str:
|
||||
return f"_\r{value}_\r"
|
||||
|
||||
def spoiler(self, value: str) -> str:
|
||||
return f"||{value}||"
|
||||
|
||||
def code(self, value: str) -> str:
|
||||
return f"`{value}`"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Memory storage
|
|||
Redis storage
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage
|
||||
.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage2
|
||||
:show-inheritance:
|
||||
|
||||
Mongo storage
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ API_TOKEN = 'BOT TOKEN HERE'
|
|||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN)
|
||||
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN_V2)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
|
|||
125
examples/custom_filter_example.py
Normal file
125
examples/custom_filter_example.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
from typing import List, Union
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
from aiogram.dispatcher.filters import BoundFilter
|
||||
|
||||
API_TOKEN = "BOT_TOKEN_HERE"
|
||||
|
||||
|
||||
ADMIN_IDS = [
|
||||
000000000,
|
||||
111111111,
|
||||
222222222,
|
||||
333333333,
|
||||
444444444,
|
||||
]
|
||||
|
||||
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
class GlobalAdminFilter(BoundFilter):
|
||||
"""
|
||||
Check if the user is a bot admin
|
||||
"""
|
||||
|
||||
key = "global_admin"
|
||||
|
||||
def __init__(self, global_admin: bool):
|
||||
self.global_admin = global_admin
|
||||
|
||||
async def check(self, obj: Union[types.Message, types.CallbackQuery]):
|
||||
user = obj.from_user
|
||||
if user.id in ADMIN_IDS:
|
||||
return self.global_admin is True
|
||||
return self.global_admin is False
|
||||
|
||||
|
||||
class MimeTypeFilter(BoundFilter):
|
||||
"""
|
||||
Check document mime_type
|
||||
"""
|
||||
|
||||
key = "mime_type"
|
||||
|
||||
def __init__(self, mime_type: Union[str, List[str]]):
|
||||
if isinstance(mime_type, str):
|
||||
self.mime_types = [mime_type]
|
||||
|
||||
elif isinstance(mime_type, list):
|
||||
self.mime_types = mime_type
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"filter mime_types must be a str or list of str, not {type(mime_type).__name__}"
|
||||
)
|
||||
|
||||
async def check(self, obj: types.Message):
|
||||
if not obj.document:
|
||||
return False
|
||||
|
||||
if obj.document.mime_type in self.mime_types:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class LettersInMessageFilter(BoundFilter):
|
||||
"""
|
||||
Checking for the number of characters in a message/callback_data
|
||||
"""
|
||||
|
||||
key = "letters"
|
||||
|
||||
def __init__(self, letters: int):
|
||||
if isinstance(letters, int):
|
||||
self.letters = letters
|
||||
else:
|
||||
raise ValueError(
|
||||
f"filter letters must be a int, not {type(letters).__name__}"
|
||||
)
|
||||
|
||||
async def check(self, obj: Union[types.Message, types.CallbackQuery]):
|
||||
data = obj.text or obj.data
|
||||
if data:
|
||||
letters_in_message = len(data)
|
||||
if letters_in_message > self.letters:
|
||||
return False
|
||||
return {"letters": letters_in_message}
|
||||
return False
|
||||
|
||||
|
||||
# Binding filters
|
||||
dp.filters_factory.bind(
|
||||
GlobalAdminFilter,
|
||||
exclude_event_handlers=[dp.channel_post_handlers, dp.edited_channel_post_handlers],
|
||||
)
|
||||
dp.filters_factory.bind(MimeTypeFilter, event_handlers=[dp.message_handlers])
|
||||
dp.filters_factory.bind(LettersInMessageFilter)
|
||||
|
||||
|
||||
@dp.message_handler(letters=5)
|
||||
async def handle_letters_in_message(message: types.Message, letters: int):
|
||||
await message.answer(f"Message too short!\nYou sent only {letters} letters")
|
||||
|
||||
|
||||
@dp.message_handler(content_types=types.ContentTypes.DOCUMENT, mime_type="text/plain")
|
||||
async def handle_txt_documents(message: types.Message):
|
||||
await message.answer("This is a text file!")
|
||||
|
||||
|
||||
@dp.message_handler(
|
||||
content_types=types.ContentTypes.DOCUMENT, mime_type=["image/jpeg", "image/png"]
|
||||
)
|
||||
async def handle_photo_documents(message: types.Message):
|
||||
await message.answer("This is a photo file!")
|
||||
|
||||
|
||||
@dp.message_handler(global_admin=True)
|
||||
async def handle_admins(message: types.Message):
|
||||
await message.answer("Congratulations, you are global admin!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
allowed_updates = types.AllowedUpdates.MESSAGE | types.AllowedUpdates.CALLBACK_QUERY
|
||||
executor.start_polling(dp, allowed_updates=allowed_updates, skip_updates=True)
|
||||
|
|
@ -50,7 +50,7 @@ async def cmd_start(message: types.Message):
|
|||
# This line is formatted to '🌎 *IP:* `YOUR IP`'
|
||||
|
||||
# Make request through bot's proxy
|
||||
ip = await fetch(GET_IP_URL, bot.session)
|
||||
ip = await fetch(GET_IP_URL, await bot.get_session())
|
||||
content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy')))
|
||||
# This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
aiohttp>=3.7.2,<4.0.0
|
||||
Babel>=2.8.0
|
||||
certifi>=2020.6.20
|
||||
aiohttp>=3.8.0,<3.9.0
|
||||
Babel>=2.9.1,<2.10.0
|
||||
certifi>=2021.10.8
|
||||
|
|
|
|||
9
setup.py
9
setup.py
|
|
@ -58,19 +58,20 @@ setup(
|
|||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
||||
],
|
||||
install_requires=[
|
||||
'aiohttp>=3.7.2,<4.0.0',
|
||||
'Babel>=2.8.0',
|
||||
'certifi>=2020.6.20',
|
||||
'aiohttp>=3.8.0,<3.9.0',
|
||||
'Babel>=2.9.1,<2.10.0',
|
||||
'certifi>=2021.10.8',
|
||||
],
|
||||
extras_require={
|
||||
'proxy': [
|
||||
'aiohttp-socks>=0.5.3,<0.6.0',
|
||||
],
|
||||
'fast': [
|
||||
'uvloop>=0.14.0,<0.15.0',
|
||||
'uvloop>=0.16.0,<0.17.0',
|
||||
'ujson>=1.35',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class FakeTelegram(aresponses.ResponsesMockServer):
|
|||
|
||||
@staticmethod
|
||||
def parse_data(message_data):
|
||||
import json
|
||||
from aiogram.utils import json
|
||||
from aiogram.utils.payload import _normalize
|
||||
|
||||
_body = '{"ok":true,"result":' + json.dumps(_normalize(message_data)) + '}'
|
||||
|
|
|
|||
|
|
@ -1,28 +1,53 @@
|
|||
import aioredis
|
||||
import pytest
|
||||
from _pytest.config import UsageError
|
||||
import aioredis.util
|
||||
|
||||
try:
|
||||
import aioredis.util
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--redis", default=None,
|
||||
help="run tests which require redis connection")
|
||||
parser.addoption(
|
||||
"--redis",
|
||||
default=None,
|
||||
help="run tests which require redis connection",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "redis: marked tests require redis connection to run")
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"redis: marked tests require redis connection to run",
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
redis_uri = config.getoption("--redis")
|
||||
if redis_uri is None:
|
||||
skip_redis = pytest.mark.skip(reason="need --redis option with redis URI to run")
|
||||
skip_redis = pytest.mark.skip(
|
||||
reason="need --redis option with redis URI to run"
|
||||
)
|
||||
for item in items:
|
||||
if "redis" in item.keywords:
|
||||
item.add_marker(skip_redis)
|
||||
return
|
||||
|
||||
redis_version = int(aioredis.__version__.split(".")[0])
|
||||
options = None
|
||||
if redis_version == 1:
|
||||
(host, port), options = aioredis.util.parse_url(redis_uri)
|
||||
options.update({'host': host, 'port': port})
|
||||
elif redis_version == 2:
|
||||
try:
|
||||
options = aioredis.connection.parse_url(redis_uri)
|
||||
except ValueError as e:
|
||||
raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
|
||||
|
||||
try:
|
||||
address, options = aioredis.util.parse_url(redis_uri)
|
||||
assert isinstance(address, tuple), "Only redis and rediss schemas are supported, eg redis://foo."
|
||||
assert isinstance(options, dict), \
|
||||
"Only redis and rediss schemas are supported, eg redis://foo."
|
||||
except AssertionError as e:
|
||||
raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
|
||||
|
||||
|
|
@ -30,6 +55,20 @@ def pytest_collection_modifyitems(config, items):
|
|||
@pytest.fixture(scope='session')
|
||||
def redis_options(request):
|
||||
redis_uri = request.config.getoption("--redis")
|
||||
(host, port), options = aioredis.util.parse_url(redis_uri)
|
||||
options.update({'host': host, 'port': port})
|
||||
return options
|
||||
if redis_uri is None:
|
||||
pytest.skip("need --redis option with redis URI to run")
|
||||
return
|
||||
|
||||
redis_version = int(aioredis.__version__.split(".")[0])
|
||||
if redis_version == 1:
|
||||
(host, port), options = aioredis.util.parse_url(redis_uri)
|
||||
options.update({'host': host, 'port': port})
|
||||
return options
|
||||
|
||||
if redis_version == 2:
|
||||
try:
|
||||
return aioredis.connection.parse_url(redis_uri)
|
||||
except ValueError as e:
|
||||
raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
|
||||
|
||||
raise UsageError("Unsupported aioredis version")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import aioredis
|
||||
import pytest
|
||||
|
||||
from pytest_lazyfixture import lazy_fixture
|
||||
from aiogram.contrib.fsm_storage.memory import MemoryStorage
|
||||
from aiogram.contrib.fsm_storage.redis import RedisStorage2, RedisStorage
|
||||
from aiogram.contrib.fsm_storage.redis import RedisStorage, RedisStorage2
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.mark.redis
|
||||
async def redis_store(redis_options):
|
||||
if int(aioredis.__version__.split(".")[0]) == 2:
|
||||
pytest.skip('aioredis v2 is not supported.')
|
||||
return
|
||||
s = RedisStorage(**redis_options)
|
||||
try:
|
||||
yield s
|
||||
|
|
@ -37,9 +41,9 @@ async def memory_store():
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"store", [
|
||||
pytest.lazy_fixture('redis_store'),
|
||||
pytest.lazy_fixture('redis_store2'),
|
||||
pytest.lazy_fixture('memory_store'),
|
||||
lazy_fixture('redis_store'),
|
||||
lazy_fixture('redis_store2'),
|
||||
lazy_fixture('memory_store'),
|
||||
]
|
||||
)
|
||||
class TestStorage:
|
||||
|
|
@ -63,8 +67,8 @@ class TestStorage:
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"store", [
|
||||
pytest.lazy_fixture('redis_store'),
|
||||
pytest.lazy_fixture('redis_store2'),
|
||||
lazy_fixture('redis_store'),
|
||||
lazy_fixture('redis_store2'),
|
||||
]
|
||||
)
|
||||
class TestRedisStorage2:
|
||||
|
|
@ -74,6 +78,7 @@ class TestRedisStorage2:
|
|||
assert await store.get_data(chat='1234') == {'foo': 'bar'}
|
||||
pool_id = id(store._redis)
|
||||
await store.close()
|
||||
await store.wait_closed()
|
||||
assert await store.get_data(chat='1234') == {
|
||||
'foo': 'bar'} # new pool was opened at this point
|
||||
assert id(store._redis) != pool_id
|
||||
|
|
|
|||
|
|
@ -425,14 +425,19 @@ async def test_get_chat(bot: Bot):
|
|||
|
||||
async def test_get_chat_administrators(bot: Bot):
|
||||
""" getChatAdministrators method test """
|
||||
from .types.dataset import CHAT, CHAT_MEMBER
|
||||
from .types.dataset import CHAT, CHAT_MEMBER, CHAT_MEMBER_OWNER
|
||||
chat = types.Chat(**CHAT)
|
||||
member = types.ChatMember.resolve(**CHAT_MEMBER)
|
||||
owner = types.ChatMember.resolve(**CHAT_MEMBER_OWNER)
|
||||
|
||||
async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]):
|
||||
async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER_OWNER]):
|
||||
result = await bot.get_chat_administrators(chat_id=chat.id)
|
||||
assert result[0] == member
|
||||
assert result[1] == owner
|
||||
assert len(result) == 2
|
||||
for m in result:
|
||||
assert m.is_chat_admin()
|
||||
assert hasattr(m, "can_be_edited")
|
||||
|
||||
|
||||
async def test_get_chat_member_count(bot: Bot):
|
||||
|
|
|
|||
79
tests/test_bot/test_bot_download_file.py
Normal file
79
tests/test_bot/test_bot_download_file.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import File
|
||||
from tests import TOKEN
|
||||
from tests.types.dataset import FILE
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.fixture(name='bot')
|
||||
async def bot_fixture():
|
||||
async def get_file():
|
||||
return File(**FILE)
|
||||
|
||||
""" Bot fixture """
|
||||
_bot = Bot(TOKEN)
|
||||
_bot.get_file = get_file
|
||||
yield _bot
|
||||
session = await _bot.get_session()
|
||||
await session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file():
|
||||
return File(**FILE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmppath(tmpdir, request):
|
||||
os.chdir(tmpdir)
|
||||
yield Path(tmpdir)
|
||||
os.chdir(request.config.invocation_dir)
|
||||
|
||||
|
||||
class TestBotDownload:
|
||||
async def test_download_file(self, tmppath, bot, file):
|
||||
f = await bot.download_file(file_path=file.file_path)
|
||||
assert len(f.read()) != 0
|
||||
|
||||
async def test_download_file_destination(self, tmppath, bot, file):
|
||||
await bot.download_file(file_path=file.file_path, destination="test.file")
|
||||
assert os.path.isfile(tmppath.joinpath('test.file'))
|
||||
|
||||
async def test_download_file_destination_with_dir(self, tmppath, bot, file):
|
||||
await bot.download_file(file_path=file.file_path,
|
||||
destination=os.path.join('dir_name', 'file_name'))
|
||||
assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name'))
|
||||
|
||||
async def test_download_file_destination_raise_file_not_found(self, tmppath, bot, file):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await bot.download_file(file_path=file.file_path,
|
||||
destination=os.path.join('dir_name', 'file_name'),
|
||||
make_dirs=False)
|
||||
|
||||
async def test_download_file_destination_io_bytes(self, tmppath, bot, file):
|
||||
f = BytesIO()
|
||||
await bot.download_file(file_path=file.file_path,
|
||||
destination=f)
|
||||
assert len(f.read()) != 0
|
||||
|
||||
async def test_download_file_raise_value_error(self, tmppath, bot, file):
|
||||
with pytest.raises(ValueError):
|
||||
await bot.download_file(file_path=file.file_path, destination="a", destination_dir="b")
|
||||
|
||||
async def test_download_file_destination_dir(self, tmppath, bot, file):
|
||||
await bot.download_file(file_path=file.file_path, destination_dir='test_dir')
|
||||
assert os.path.isfile(tmppath.joinpath('test_dir', file.file_path))
|
||||
|
||||
async def test_download_file_destination_dir_raise_file_not_found(self, tmppath, bot, file):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await bot.download_file(file_path=file.file_path,
|
||||
destination_dir='test_dir',
|
||||
make_dirs=False)
|
||||
assert os.path.isfile(tmppath.joinpath('test_dir', file.file_path))
|
||||
|
|
@ -23,7 +23,6 @@ class TestAiohttpSession:
|
|||
|
||||
assert bot._session is None
|
||||
|
||||
assert isinstance(bot.session, aiohttp.ClientSession)
|
||||
assert bot.session == bot._session
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -51,11 +50,11 @@ class TestAiohttpSession:
|
|||
@pytest.mark.asyncio
|
||||
async def test_close_session(self):
|
||||
bot = BaseBot(token="42:correct",)
|
||||
aiohttp_client_0 = bot.session
|
||||
aiohttp_client_0 = await bot.get_session()
|
||||
|
||||
with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close:
|
||||
await aiohttp_client_0.close()
|
||||
mocked_close.assert_called_once()
|
||||
|
||||
await aiohttp_client_0.close()
|
||||
assert aiohttp_client_0 != bot.session # will create new session
|
||||
assert aiohttp_client_0 != await bot.get_session() # will create new session
|
||||
|
|
|
|||
14
tests/test_dispatcher/test_fsm_context.py
Normal file
14
tests/test_dispatcher/test_fsm_context.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
from aiogram.contrib.fsm_storage.memory import MemoryStorage
|
||||
from aiogram.dispatcher import FSMContext
|
||||
|
||||
|
||||
class TestFSMContext:
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_data(self):
|
||||
context = FSMContext(MemoryStorage(), chat=1, user=1)
|
||||
async with context.proxy() as data:
|
||||
data.update(key1="value1", key2="value2")
|
||||
async with context.proxy() as data:
|
||||
assert data['key1'] == "value1"
|
||||
assert data['key2'] == "value2"
|
||||
39
tests/test_utils/test_callback_data.py
Normal file
39
tests/test_utils/test_callback_data.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram.utils.callback_data import CallbackData
|
||||
|
||||
|
||||
class TestCallbackData:
|
||||
@pytest.mark.asyncio
|
||||
async def test_cb(self):
|
||||
cb = CallbackData('simple', 'action')
|
||||
assert cb.new('x') == 'simple:x'
|
||||
assert cb.new(action='y') == 'simple:y'
|
||||
assert cb.new('') == 'simple:'
|
||||
|
||||
assert (await cb.filter().check(CallbackQuery(data='simple:'))) == {'callback_data': {'@': 'simple', 'action': ''}}
|
||||
assert (await cb.filter().check(CallbackQuery(data='simple:x'))) == {'callback_data': {'@': 'simple', 'action': 'x'}}
|
||||
assert (await cb.filter(action='y').check(CallbackQuery(data='simple:x'))) is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cb_double(self):
|
||||
cb = CallbackData('double', 'pid', 'action')
|
||||
assert cb.new('123', 'x') == 'double:123:x'
|
||||
assert cb.new(pid=456, action='y') == 'double:456:y'
|
||||
assert cb.new('', 'z') == 'double::z'
|
||||
assert cb.new('789', '') == 'double:789:'
|
||||
|
||||
assert (await cb.filter().check(CallbackQuery(data='double::'))) == {'callback_data': {'@': 'double', 'pid': '', 'action': ''}}
|
||||
assert (await cb.filter().check(CallbackQuery(data='double:x:'))) == {'callback_data': {'@': 'double', 'pid': 'x', 'action': ''}}
|
||||
assert (await cb.filter().check(CallbackQuery(data='double::y'))) == {'callback_data': {'@': 'double', 'pid': '', 'action': 'y'}}
|
||||
assert (await cb.filter(action='x').check(CallbackQuery(data='double:123:x'))) == {'callback_data': {'@': 'double', 'pid': '123', 'action': 'x'}}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cb_zero(self):
|
||||
cb = CallbackData('zero')
|
||||
assert cb.new() == 'zero'
|
||||
|
||||
assert (await cb.filter().check(CallbackQuery(data='zero'))) == {'callback_data': {'@': 'zero'}}
|
||||
assert (await cb.filter().check(CallbackQuery(data='zero:'))) is False
|
||||
assert (await cb.filter().check(CallbackQuery(data='bla'))) is False
|
||||
|
|
@ -44,12 +44,21 @@ CHAT_MEMBER = {
|
|||
"user": USER,
|
||||
"status": "administrator",
|
||||
"can_be_edited": False,
|
||||
"can_manage_chat": True,
|
||||
"can_change_info": True,
|
||||
"can_delete_messages": True,
|
||||
"can_invite_users": True,
|
||||
"can_restrict_members": True,
|
||||
"can_pin_messages": True,
|
||||
"can_promote_members": False,
|
||||
"can_manage_voice_chats": True,
|
||||
"is_anonymous": False,
|
||||
}
|
||||
|
||||
CHAT_MEMBER_OWNER = {
|
||||
"user": USER,
|
||||
"status": "creator",
|
||||
"is_anonymous": False,
|
||||
}
|
||||
|
||||
CONTACT = {
|
||||
|
|
|
|||
102
tests/types/test_mixins.py
Normal file
102
tests/types/test_mixins.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import File
|
||||
from aiogram.types.mixins import Downloadable
|
||||
from tests import TOKEN
|
||||
from tests.types.dataset import FILE
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.fixture(name='bot')
|
||||
async def bot_fixture():
|
||||
""" Bot fixture """
|
||||
_bot = Bot(TOKEN)
|
||||
yield _bot
|
||||
await (await _bot.get_session()).close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmppath(tmpdir, request):
|
||||
os.chdir(tmpdir)
|
||||
yield Path(tmpdir)
|
||||
os.chdir(request.config.invocation_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def downloadable(bot):
|
||||
async def get_file():
|
||||
return File(**FILE)
|
||||
|
||||
downloadable = Downloadable()
|
||||
downloadable.get_file = get_file
|
||||
downloadable.bot = bot
|
||||
|
||||
return downloadable
|
||||
|
||||
|
||||
class TestDownloadable:
|
||||
async def test_download_make_dirs_false_nodir(self, tmppath, downloadable):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await downloadable.download(make_dirs=False)
|
||||
|
||||
async def test_download_make_dirs_false_mkdir(self, tmppath, downloadable):
|
||||
os.mkdir('voice')
|
||||
await downloadable.download(make_dirs=False)
|
||||
assert os.path.isfile(tmppath.joinpath(FILE["file_path"]))
|
||||
|
||||
async def test_download_make_dirs_true(self, tmppath, downloadable):
|
||||
await downloadable.download(make_dirs=True)
|
||||
assert os.path.isfile(tmppath.joinpath(FILE["file_path"]))
|
||||
|
||||
async def test_download_deprecation_warning(self, tmppath, downloadable):
|
||||
with pytest.deprecated_call():
|
||||
await downloadable.download("test.file")
|
||||
|
||||
async def test_download_destination(self, tmppath, downloadable):
|
||||
with pytest.deprecated_call():
|
||||
await downloadable.download("test.file")
|
||||
assert os.path.isfile(tmppath.joinpath('test.file'))
|
||||
|
||||
async def test_download_destination_dir_exist(self, tmppath, downloadable):
|
||||
os.mkdir("test_folder")
|
||||
with pytest.deprecated_call():
|
||||
await downloadable.download("test_folder")
|
||||
assert os.path.isfile(tmppath.joinpath('test_folder', FILE["file_path"]))
|
||||
|
||||
async def test_download_destination_with_dir(self, tmppath, downloadable):
|
||||
with pytest.deprecated_call():
|
||||
await downloadable.download(os.path.join('dir_name', 'file_name'))
|
||||
assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name'))
|
||||
|
||||
async def test_download_destination_io_bytes(self, tmppath, downloadable):
|
||||
file = BytesIO()
|
||||
with pytest.deprecated_call():
|
||||
await downloadable.download(file)
|
||||
assert len(file.read()) != 0
|
||||
|
||||
async def test_download_raise_value_error(self, tmppath, downloadable):
|
||||
with pytest.raises(ValueError):
|
||||
await downloadable.download(destination_dir="a", destination_file="b")
|
||||
|
||||
async def test_download_destination_dir(self, tmppath, downloadable):
|
||||
await downloadable.download(destination_dir='test_dir')
|
||||
assert os.path.isfile(tmppath.joinpath('test_dir', FILE["file_path"]))
|
||||
|
||||
async def test_download_destination_file(self, tmppath, downloadable):
|
||||
await downloadable.download(destination_file='file_name')
|
||||
assert os.path.isfile(tmppath.joinpath('file_name'))
|
||||
|
||||
async def test_download_destination_file_with_dir(self, tmppath, downloadable):
|
||||
await downloadable.download(destination_file=os.path.join('dir_name', 'file_name'))
|
||||
assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name'))
|
||||
|
||||
async def test_download_io_bytes(self, tmppath, downloadable):
|
||||
file = BytesIO()
|
||||
await downloadable.download(destination_file=file)
|
||||
assert len(file.read()) != 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue