Merge branch 'aiogram:dev-2.x' into dev-2.x

This commit is contained in:
Юрий 2022-01-03 14:27:36 +03:00 committed by GitHub
commit 65fba24200
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1732 additions and 244 deletions

View file

@ -27,7 +27,7 @@ upload:
release:
make clean
make test
#make test
make build
make tag
@echo "Released aiogram $(AIOGRAM_VERSION)"

View file

@ -6,7 +6,7 @@
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT)

View file

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

View file

@ -43,5 +43,5 @@ __all__ = (
'utils',
)
__version__ = '2.14.3'
__api_version__ = '5.3'
__version__ = '2.18'
__api_version__ = '5.6'

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import json
import pathlib
import pickle
import typing
from aiogram.utils import json
from .memory import MemoryStorage

View file

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

View file

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

View file

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

View file

@ -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):
"""

View file

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

View file

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

View file

@ -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 havent set any storage yet so no states and no data will be saved. \n"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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,
)

View file

@ -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):
"""

View file

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

View file

@ -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():

View file

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

View file

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

View file

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

View file

@ -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):
"""

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@ from datetime import datetime
from . import base
from . import fields
from .user import User
class VoiceChatScheduled(base.TelegramObject):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {"<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;"}
@ -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}">&#8203;</a>'
return f'<a href="{url}">&#8288;</a>'

View file

@ -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}`"

View file

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

View file

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

View file

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

View 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)

View file

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

View file

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

View file

@ -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',
],
},

View file

@ -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)) + '}'

View file

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

View file

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

View file

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

View 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))

View file

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

View 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"

View 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

View file

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