Merge branch 'dev-2.x'

This commit is contained in:
Alex Root Junior 2022-04-16 18:13:36 +03:00
commit 0e7a9006b3
No known key found for this signature in database
GPG key ID: 074C1D455EBEA4AC
38 changed files with 1119 additions and 655 deletions

View file

@ -1,4 +1,4 @@
VENV_NAME := venv
VENV_NAME := .venv
PYTHON := $(VENV_NAME)/bin/python
AIOGRAM_VERSION := $(shell $(PYTHON) -c "import aiogram;print(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.7-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-6.0-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.7-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.0-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.19'
__api_version__ = '5.7'
__version__ = '2.20'
__api_version__ = '6.0'

View file

@ -278,6 +278,13 @@ class Methods(Helper):
# Inline mode
ANSWER_INLINE_QUERY = Item() # answerInlineQuery
ANSWER_WEB_APP_QUERY = Item() # answerWebAppQuery
SET_CHAT_MENU_BUTTON = Item() # setChatMenuButton
GET_CHAT_MENU_BUTTON = Item() # getChatMenuButton
SET_MY_DEFAULT_ADMINISTRATOR_RIGHTS = Item() # setMyDefaultAdministratorRights
GET_MY_DEFAULT_ADMINISTRATOR_RIGHTS = Item() # getMyDefaultAdministratorRights
# Payments
SEND_INVOICE = Item() # sendInvoice
ANSWER_SHIPPING_QUERY = Item() # answerShippingQuery

View file

@ -277,7 +277,13 @@ class BaseBot:
dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb')
session = await self.get_session()
async with session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response:
async with session.get(
url,
timeout=timeout,
proxy=self.proxy,
proxy_auth=self.proxy_auth,
raise_for_status=True,
) as response:
while True:
chunk = await response.content.read(chunk_size)
if not chunk:

View file

@ -8,7 +8,7 @@ import warnings
from .base import BaseBot, api
from .. import types
from ..types import base
from ..utils.deprecated import deprecated, removed_argument
from ..utils.deprecated import deprecated
from ..utils.exceptions import ValidationError
from ..utils.mixins import DataMixin, ContextInstanceMixin
from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file
@ -1089,7 +1089,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
# 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")
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())
@ -1704,9 +1705,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
datetime.datetime, datetime.timedelta, None]`
:param revoke_messages: Pass True to delete all messages from
the chat for the user that is being removed. If False, the user
will be able to see messages in the group that were sent before
the user was removed. Always True for supergroups and channels.
the chat for the user that is being removed. If False, the user
will be able to see messages in the group that were sent before
the user was removed. Always True for supergroups and channels.
:type revoke_messages: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success
@ -1834,6 +1835,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
can_restrict_members: typing.Optional[base.Boolean] = None,
can_pin_messages: typing.Optional[base.Boolean] = None,
can_promote_members: typing.Optional[base.Boolean] = None,
can_manage_video_chats: typing.Optional[base.Boolean] = None,
) -> base.Boolean:
"""
Use this method to promote or demote a user in a supergroup or a channel.
@ -1885,9 +1887,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
directly or indirectly (promoted by administrators that were appointed by him)
:type can_promote_members: :obj:`typing.Optional[base.Boolean]`
:param can_manage_video_chats: Pass True, if the administrator can manage video chats
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
if can_manage_voice_chats:
warnings.warn(
"Argument `can_manage_voice_chats` was renamed to `can_manage_video_chats` and will be removed in aiogram 2.21")
can_manage_video_chats = can_manage_voice_chats
can_manage_voice_chats = None
payload = generate_payload(**locals())
return await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
@ -1910,11 +1920,10 @@ 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,
self,
chat_id: typing.Union[base.Integer, base.String],
sender_chat_id: base.Integer,
):
"""Ban a channel chat in a supergroup or a channel.
@ -1937,9 +1946,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
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,
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.
@ -2587,6 +2596,87 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.GET_MY_COMMANDS, payload)
return [types.BotCommand(**bot_command_data) for bot_command_data in result]
async def set_chat_menu_button(self, chat_id: typing.Optional[base.Integer] = None,
menu_button: typing.Optional[types.MenuButton] = None) -> bool:
"""
Use this method to change bot's menu button in a private chat, or the default menu button.
Returns True on success.
Source https://core.telegram.org/bots/api#setchatmenubutton
:param chat_id: Unique identifier for the target private chat.
If not specified, default bot's menu button will be changed
:param menu_button:
A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault
:return: Returns True on success.
"""
menu_button = prepare_arg(menu_button)
payload = generate_payload(**locals())
return await self.request(api.Methods.SET_CHAT_MENU_BUTTON, payload)
async def get_chat_menu_button(self, chat_id: typing.Optional[base.Integer] = None) -> typing.Union[
"types.MenuButtonCommands",
"types.MenuButtonDefault",
"types.MenuButtonWebApp",
]:
"""
Use this method to get the current value of the bot's menu button in a private chat,
or the default menu button.
Returns MenuButton on success.
Source https://core.telegram.org/bots/api#getchatmenu
:param chat_id: Unique identifier for the target private chat. If not specified,
default bot's menu button will be returned
:return: Returns MenuButton on success.
"""
payload = generate_payload(**locals())
result = await self.request(api.Methods.GET_CHAT_MENU_BUTTON, payload)
return types.MenuButton.resolve(**result)
async def set_my_default_administrator_rights(self, rights: typing.Optional[types.ChatAdministratorRights] = None,
for_channels: typing.Optional[base.Boolean] = None) -> base.Boolean:
"""
Use this method to change default administrator rights of the bot for adding it as an administrator
to groups or channels.
Returns True on success.
Source: https://core.telegram.org/bots/api#setmydefaultadministratorrights
:param rights: A JSON-serialized object, describing new default administrator rights.
If not specified, the default administrator rights will be cleared.
:param for_channels:
Pass True to change default administrator rights of the bot in channels.
Otherwise, default administrator rights of the bot for groups and supergroups will be changed.
:return: Returns True on success.
"""
rights = prepare_arg(rights)
payload = generate_payload(**locals())
return await self.request(api.Methods.SET_MY_DEFAULT_ADMINISTRATOR_RIGHTS, payload)
async def get_my_default_administrator_rights(self,
for_channels: typing.Optional[base.Boolean] = None
) -> types.ChatAdministratorRights:
"""
Use this method to get the current default administrator rights of the bot.
Returns ChatAdministratorRights on success.
Source: https://core.telegram.org/bots/api#getmydefaultadministratorrights
:param for_channels: Pass True to get default administrator rights of the bot in channels.
Otherwise, default administrator rights of the bot for groups and supergroups will be returned.
:return:
"""
payload = generate_payload(**locals())
result = await self.request(api.Methods.GET_MY_DEFAULT_ADMINISTRATOR_RIGHTS, payload)
return types.ChatAdministratorRights(**result)
async def edit_message_text(self,
text: base.String,
chat_id: typing.Union[base.Integer, base.String, None] = None,
@ -3133,6 +3223,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return await self.request(api.Methods.ANSWER_INLINE_QUERY, payload)
async def answer_web_app_query(self, web_app_query_id: base.String,
result: types.InlineQueryResult) -> types.SentWebAppMessage:
"""
Use this method to set result of interaction with web app and send corresponding message
on behalf of the user to the chat from which the query originated.
On success, SentWebAppMessage is returned.
Source https://core.telegram.org/bots/api#answerwebappquery
:param web_app_query_id: Unique identifier for the answered query
:param result: A JSON-serialized object with a description of the message to send
:return: On success, SentWebAppMessage is returned.
"""
result = prepare_arg(result)
payload = generate_payload(**locals())
response = await self.request(api.Methods.ANSWER_WEB_APP_QUERY, payload)
return types.SentWebAppMessage(**response)
# === Payments ===
# https://core.telegram.org/bots/api#payments

View file

@ -50,7 +50,7 @@ class MongoStorage(BaseStorage):
self._uri = uri
self._username = username
self._password = password
self._kwargs = kwargs
self._kwargs = kwargs # custom client options like SSL configuration, etc.
self._mongo: Optional[AsyncIOMotorClient] = None
self._db: Optional[AsyncIOMotorDatabase] = None
@ -63,7 +63,7 @@ class MongoStorage(BaseStorage):
if self._uri:
try:
self._mongo = AsyncIOMotorClient(self._uri)
self._mongo = AsyncIOMotorClient(self._uri, **self._kwargs)
except pymongo.errors.ConfigurationError as e:
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
import logging

View file

@ -216,11 +216,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
async def skip_updates(self):
"""
You can skip old incoming updates from queue.
This method is not recommended to use if you use payments or you bot has high-load.
This method is not recommended for using in production.
:return: None
Note that the webhook will be deleted!
"""
await self.bot.get_updates(offset=-1, timeout=1)
await self.bot.delete_webhook(drop_pending_updates=True)
async def process_updates(self, updates, fast: bool = True):
"""
@ -768,7 +768,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
.. code-block:: python3
dp.register_chosen_inline_handler(some_chosen_inline_handler, lambda chosen_inline_query: True)
dp.register_chosen_inline_handler(some_chosen_inline_handler, lambda chosen_inline_result: True)
:param callback:
:param state:
@ -793,8 +793,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
.. code-block:: python3
@dp.chosen_inline_handler(lambda chosen_inline_query: True)
async def some_chosen_inline_handler(chosen_inline_query: types.ChosenInlineResult)
@dp.chosen_inline_handler(lambda chosen_inline_result: True)
async def some_chosen_inline_handler(chosen_inline_result: types.ChosenInlineResult)
:param state:
:param custom_filters:

View file

@ -11,6 +11,7 @@ from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministra
from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
from .chat_administrator_rights import ChatAdministratorRights
from .chat_invite_link import ChatInviteLink
from .chat_join_request import ChatJoinRequest
from .chat_location import ChatLocation
@ -48,6 +49,7 @@ from .labeled_price import LabeledPrice
from .location import Location
from .login_url import LoginUrl
from .mask_position import MaskPosition
from .menu_button import MenuButton, MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault
from .message import ContentType, ContentTypes, Message, ParseMode
from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged
from .message_entity import MessageEntity, MessageEntityType
@ -64,6 +66,7 @@ from .pre_checkout_query import PreCheckoutQuery
from .proximity_alert_triggered import ProximityAlertTriggered
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType
from .response_parameters import ResponseParameters
from .sent_web_app_message import SentWebAppMessage
from .shipping_address import ShippingAddress
from .shipping_option import ShippingOption
from .shipping_query import ShippingQuery
@ -75,12 +78,18 @@ from .user import User
from .user_profile_photos import UserProfilePhotos
from .venue import Venue
from .video import Video
from .video_chat_ended import VideoChatEnded
from .video_chat_participants_invited import VideoChatParticipantsInvited
from .video_chat_scheduled import VideoChatScheduled
from .video_chat_started import VideoChatStarted
from .video_note import VideoNote
from .voice import Voice
from .voice_chat_ended import VoiceChatEnded
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
from .voice_chat_scheduled import VoiceChatScheduled
from .voice_chat_started import VoiceChatStarted
from .web_app_data import WebAppData
from .web_app_info import WebAppInfo
from .webhook_info import WebhookInfo
__all__ = (
@ -102,6 +111,7 @@ __all__ = (
'CallbackQuery',
'Chat',
'ChatActions',
'ChatAdministratorRights',
'ChatInviteLink',
'ChatJoinRequest',
'ChatLocation',
@ -174,6 +184,10 @@ __all__ = (
'Location',
'LoginUrl',
'MaskPosition',
'MenuButton',
'MenuButtonCommands',
'MenuButtonWebApp',
'MenuButtonDefault',
'MediaGroup',
'Message',
'MessageAutoDeleteTimerChanged',
@ -201,6 +215,7 @@ __all__ = (
'ReplyKeyboardMarkup',
'ReplyKeyboardRemove',
'ResponseParameters',
'SentWebAppMessage',
'ShippingAddress',
'ShippingOption',
'ShippingQuery',
@ -212,12 +227,18 @@ __all__ = (
'UserProfilePhotos',
'Venue',
'Video',
'VideoChatEnded',
'VideoChatParticipantsInvited',
'VideoChatScheduled',
'VideoChatStarted',
'VideoNote',
'Voice',
'VoiceChatEnded',
'VoiceChatParticipantsInvited',
'VoiceChatScheduled',
'VoiceChatStarted',
'WebAppData',
'WebAppInfo',
'WebhookInfo',
'base',
'fields',

View file

@ -240,7 +240,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
:return:
"""
if key in self.props:
return self.props[key].set_value(self, value, self.conf.get('parent', None))
return self.props[key].set_value(self, value, self.conf.get('parent', self))
self.values[key] = value
# Log warning when Telegram silently adds new Fields

View file

@ -0,0 +1,21 @@
from . import base
from . import fields
class ChatAdministratorRights(base.TelegramObject):
"""
Represents rights of an administrator in a chat.
Source: https://core.telegram.org/bots/api#chatadministratorrights
"""
is_anonymous: base.Boolean = fields.Field()
can_manage_chat: base.Boolean = fields.Field()
can_delete_messages: base.Boolean = fields.Field()
can_manage_video_chats: base.Boolean = fields.Field()
can_restrict_members: base.Boolean = fields.Field()
can_promote_members: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
can_invite_users: base.Boolean = fields.Field()
can_post_messages: base.Boolean = fields.Field()
can_edit_messages: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()

View file

@ -5,7 +5,6 @@ from . import base, fields
from .user import User
from ..utils import helper
T = typing.TypeVar('T')
@ -153,6 +152,7 @@ class ChatMemberAdministrator(ChatMember):
can_edit_messages: base.Boolean = fields.Field()
can_delete_messages: base.Boolean = fields.Field()
can_manage_voice_chats: base.Boolean = fields.Field()
can_manage_video_chats: base.Boolean = fields.Field()
can_restrict_members: base.Boolean = fields.Field()
can_promote_members: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()

View file

@ -4,6 +4,7 @@ from . import base
from . import fields
from .callback_game import CallbackGame
from .login_url import LoginUrl
from .web_app_info import WebAppInfo
class InlineKeyboardMarkup(base.TelegramObject):
@ -95,6 +96,7 @@ class InlineKeyboardButton(base.TelegramObject):
switch_inline_query_current_chat: base.String = fields.Field()
callback_game: CallbackGame = fields.Field(base=CallbackGame)
pay: base.Boolean = fields.Field()
web_app: WebAppInfo = fields.Field(base=WebAppInfo)
def __init__(self, text: base.String,
url: base.String = None,
@ -103,7 +105,9 @@ class InlineKeyboardButton(base.TelegramObject):
switch_inline_query: base.String = None,
switch_inline_query_current_chat: base.String = None,
callback_game: CallbackGame = None,
pay: base.Boolean = None, **kwargs):
pay: base.Boolean = None,
web_app: WebAppInfo = None,
**kwargs):
super(InlineKeyboardButton, self).__init__(text=text,
url=url,
login_url=login_url,
@ -111,4 +115,6 @@ class InlineKeyboardButton(base.TelegramObject):
switch_inline_query=switch_inline_query,
switch_inline_query_current_chat=switch_inline_query_current_chat,
callback_game=callback_game,
pay=pay, **kwargs)
pay=pay,
web_app=web_app,
**kwargs)

View file

@ -145,7 +145,7 @@ class InputTextMessageContent(InputMessageContent):
"""
message_text: base.String = fields.Field()
parse_mode: typing.Optional[base.String] = fields.Field()
caption_entities: typing.Optional[typing.List[MessageEntity]] = fields.Field()
entities: typing.Optional[typing.List[MessageEntity]] = fields.Field()
disable_web_page_preview: base.Boolean = fields.Field()
def safe_get_parse_mode(self):
@ -164,7 +164,7 @@ class InputTextMessageContent(InputMessageContent):
self,
message_text: base.String,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
entities: typing.Optional[typing.List[MessageEntity]] = None,
disable_web_page_preview: typing.Optional[base.Boolean] = None,
):
if parse_mode is None:
@ -175,7 +175,7 @@ class InputTextMessageContent(InputMessageContent):
super().__init__(
message_text=message_text,
parse_mode=parse_mode,
caption_entities=caption_entities,
entities=entities,
disable_web_page_preview=disable_web_page_preview,
)

View file

@ -0,0 +1,86 @@
import typing
from . import base
from . import fields
from .web_app_info import WebAppInfo
from ..utils import helper
from ..utils.helper import Item
class MenuButton(base.TelegramObject):
"""
This object describes the bot's menu button in a private chat. It should be one of
- MenuButtonCommands
- MenuButtonWebApp
- MenuButtonDefault
If a menu button other than MenuButtonDefault is set for a private chat,
then it is applied in the chat.
Otherwise the default menu button is applied.
By default, the menu button opens the list of bot commands.
"""
type: base.String = fields.Field(default='default')
@classmethod
def resolve(cls, **kwargs) -> typing.Union[
"MenuButtonCommands",
"MenuButtonDefault",
"MenuButtonWebApp",
]:
type_ = kwargs.get('type')
mapping = {
MenuButtonType.DEFAULT: MenuButtonDefault,
MenuButtonType.COMMANDS: MenuButtonCommands,
MenuButtonType.WEB_APP: MenuButtonWebApp,
}
class_ = mapping.get(type_)
if not class_:
raise ValueError(f'Unknown MenuButton type: {type_}')
return class_(**kwargs)
class MenuButtonCommands(MenuButton):
"""
Represents a menu button, which opens the bot's list of commands.
Source: https://core.telegram.org/bots/api#menubuttoncommands
"""
type: base.String = fields.Field(default='commands')
def __init__(self, **kwargs):
super().__init__(type='commands', **kwargs)
class MenuButtonWebApp(MenuButton):
"""
Represents a menu button, which launches a Web App.
Source: https://core.telegram.org/bots/api#menubuttonwebapp
"""
type: base.String = fields.Field(default='web_app')
text: base.String = fields.Field()
web_app: WebAppInfo = fields.Field(base=WebAppInfo)
def __init__(self, text: base.String, web_app: WebAppInfo, **kwargs):
super().__init__(type='web_app', text=text, web_app=web_app, **kwargs)
class MenuButtonDefault(MenuButton):
"""
Describes that no specific value for the menu button was set.
Source: https://core.telegram.org/bots/api#menubuttondefault
"""
type: base.String = fields.Field(default='default')
def __init__(self, **kwargs):
super().__init__(type='default', **kwargs)
class MenuButtonType(helper.Helper):
mode = helper.HelperMode.lowercase
DEFAULT = Item()
COMMANDS = Item()
WEB_APP = Item()

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ import typing
from . import base
from . import fields
from .web_app_info import WebAppInfo
class KeyboardButtonPollType(base.TelegramObject):
@ -117,16 +118,19 @@ class KeyboardButton(base.TelegramObject):
request_contact: base.Boolean = fields.Field()
request_location: base.Boolean = fields.Field()
request_poll: KeyboardButtonPollType = fields.Field()
web_app: WebAppInfo = fields.Field(base=WebAppInfo)
def __init__(self, text: base.String,
request_contact: base.Boolean = None,
request_location: base.Boolean = None,
request_poll: KeyboardButtonPollType = None,
web_app: WebAppInfo = None,
**kwargs):
super(KeyboardButton, self).__init__(text=text,
request_contact=request_contact,
request_location=request_location,
request_poll=request_poll,
web_app=web_app,
**kwargs)

View file

@ -0,0 +1,11 @@
from . import base
from . import fields
class SentWebAppMessage(base.TelegramObject):
"""
Contains information about an inline message sent by a Web App on behalf of a user.
Source: https://core.telegram.org/bots/api#sentwebappmessage
"""
inline_message_id: base.String = fields.Field()

View file

@ -11,7 +11,7 @@ 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
from ..utils import helper
class Update(base.TelegramObject):
@ -70,12 +70,6 @@ class AllowedUpdates(helper.Helper):
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. "
"Use `CHOSEN_INLINE_RESULT`",
new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT,
)
@classmethod
def default(cls):
return []

View file

@ -0,0 +1,13 @@
from . import base
from . import fields
from . import mixins
class VideoChatEnded(base.TelegramObject, mixins.Downloadable):
"""
This object represents a service message about a video chat scheduled in the chat.
https://core.telegram.org/bots/api#videochatended
"""
duration: base.Integer = fields.Field()

View file

@ -0,0 +1,16 @@
import typing
from . import base
from . import fields
from . import mixins
from .user import User
class VideoChatParticipantsInvited(base.TelegramObject, mixins.Downloadable):
"""
This object represents a service message about new members invited to a video chat.
https://core.telegram.org/bots/api#videochatparticipantsinvited
"""
users: typing.List[User] = fields.ListField(base=User)

View file

@ -0,0 +1,14 @@
from datetime import datetime
from . import base
from . import fields
class VideoChatScheduled(base.TelegramObject):
"""
This object represents a service message about a video chat scheduled in the chat.
https://core.telegram.org/bots/api#videochatscheduled
"""
start_date: datetime = fields.DateTimeField()

View file

@ -0,0 +1,11 @@
from . import base
from . import mixins
class VideoChatStarted(base.TelegramObject, mixins.Downloadable):
"""
his object represents a service message about a video chat started in the chat. Currently holds no information.
https://core.telegram.org/bots/api#videochatstarted
"""
pass

View file

@ -0,0 +1,12 @@
from . import base
from . import fields
class WebAppData(base.TelegramObject):
"""
Contains data sent from a Web App to the bot.
Source: https://core.telegram.org/bots/api#webappdata
"""
data: str = fields.Field()
button_text: str = fields.Field()

View file

@ -0,0 +1,11 @@
from . import base
from . import fields
class WebAppInfo(base.TelegramObject):
"""
Contains information about a Web App.
Source: https://core.telegram.org/bots/api#webappinfo
"""
url: base.String = fields.Field()

View file

@ -18,3 +18,4 @@ class WebhookInfo(base.TelegramObject):
last_error_message: base.String = fields.Field()
max_connections: base.Integer = fields.Field()
allowed_updates: typing.List[base.String] = fields.ListField()
last_synchronization_error_date: base.Integer = fields.DateTimeField()

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.7-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.0-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -44,7 +44,7 @@ setup(
url='https://github.com/aiogram/aiogram',
license='MIT',
author='Alex Root Junior',
requires_python='>=3.7',
python_requires='>=3.7',
author_email='jroot.junior@gmail.com',
description='Is a pretty simple and fully asynchronous framework for Telegram Bot API',
long_description=get_description(),

22
test.html Normal file
View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<div id="content">not inited</div>
<button onclick="validateData()" type="button" class="btn btn-primary">Show</button>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script src="static/app.js"></script>
</body>
</html>

View file

@ -1,7 +1,12 @@
import asyncio
import aioredis
import pytest
from _pytest.config import UsageError
from aiogram import Bot
from . import TOKEN
try:
import aioredis.util
except ImportError:
@ -72,3 +77,14 @@ def redis_options(request):
raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
raise UsageError("Unsupported aioredis version")
@pytest.fixture(name='bot')
async def bot_fixture():
"""Bot fixture."""
bot = Bot(TOKEN)
yield bot
session = await bot.get_session()
if session and not session.closed:
await session.close()
await asyncio.sleep(0.2)

View file

@ -6,14 +6,6 @@ from . import FakeTelegram, TOKEN, BOT_ID
pytestmark = pytest.mark.asyncio
@pytest.fixture(name='bot')
async def bot_fixture():
""" Bot fixture """
_bot = Bot(TOKEN, parse_mode=types.ParseMode.MARKDOWN_V2)
yield _bot
await _bot.close()
async def test_get_me(bot: Bot):
""" getMe method test """
from .types.dataset import USER

View file

@ -1,11 +1,14 @@
import os
from io import BytesIO
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
from aiohttp import ClientResponseError
from aiogram import Bot
from aiogram.types import File
from aiogram.utils.json import json
from tests import TOKEN
from tests.types.dataset import FILE
@ -14,12 +17,9 @@ 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
_bot.get_file = AsyncMock(return_value=File(**FILE))
yield _bot
session = await _bot.get_session()
await session.close()
@ -37,43 +37,54 @@ def tmppath(tmpdir, request):
os.chdir(request.config.invocation_dir)
@pytest.fixture()
def get_file_response(aresponses):
aresponses.add(response=aresponses.Response(body=json.dumps(FILE)))
class TestBotDownload:
async def test_download_file(self, tmppath, bot, file):
async def test_download_file(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination_with_dir(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination_raise_file_not_found(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination_io_bytes(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_raise_value_error(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination_dir(self, tmppath, bot, file, get_file_response):
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):
async def test_download_file_destination_dir_raise_file_not_found(self, tmppath, bot, file, get_file_response):
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))
async def test_download_file_404(self, tmppath, bot, file):
with pytest.raises(ClientResponseError) as exc_info:
await bot.download_file(file_path=file.file_path)
assert exc_info.value.status == 404

View file

@ -5,14 +5,6 @@ from aiogram import Dispatcher, Bot
pytestmark = pytest.mark.asyncio
@pytest.fixture(name='bot')
async def bot_fixture():
""" Bot fixture """
_bot = Bot(token='123456789:AABBCCDDEEFFaabbccddeeff-1234567890')
yield _bot
await _bot.close()
class TestDispatcherInit:
async def test_successful_init(self, bot):
"""

View file

@ -8,16 +8,8 @@ from . import FakeTelegram, TOKEN
pytestmark = pytest.mark.asyncio
@pytest.fixture(name='bot')
async def bot_fixture():
""" Bot fixture """
_bot = Bot(TOKEN, parse_mode=types.ParseMode.HTML)
yield _bot
await _bot.close()
@pytest.fixture()
async def message(bot):
async def message(bot: Bot):
"""
Message fixture
:param bot: Telegram bot fixture

View file

@ -19,6 +19,14 @@ CHAT = {
"type": "private",
}
CHAT_PHOTO = {
"small_file_id": "small_file_id",
"small_file_unique_id": "small_file_unique_id",
"big_file_id": "big_file_id",
"big_file_unique_id": "big_file_unique_id",
}
PHOTO = {
"file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg",
"file_size": 1101,
@ -485,3 +493,37 @@ REPLY_KEYBOARD_MARKUP = {
"keyboard": [[{"text": "something here"}]],
"resize_keyboard": True,
}
CHAT_PERMISSIONS = {
"can_send_messages": True,
"can_send_media_messages": True,
"can_send_polls": True,
"can_send_other_messages": True,
"can_add_web_page_previews": True,
"can_change_info": True,
"can_invite_users": True,
"can_pin_messages": True,
}
CHAT_LOCATION = {
"location": LOCATION,
"address": "address",
}
FULL_CHAT = {
**CHAT,
"photo": CHAT_PHOTO,
"bio": "bio",
"has_private_forwards": False,
"description": "description",
"invite_link": "invite_link",
"pinned_message": MESSAGE,
"permissions": CHAT_PERMISSIONS,
"slow_mode_delay": 10,
"message_auto_delete_time": 60,
"has_protected_content": True,
"sticker_set_name": "sticker_set_name",
"can_set_sticker_set": True,
"linked_chat_id": -1234567890,
"location": CHAT_LOCATION,
}

View file

@ -1,5 +1,10 @@
from aiogram import types
from .dataset import CHAT
import pytest
from aiogram import Bot, types
from .dataset import CHAT, FULL_CHAT
from .. import FakeTelegram
pytestmark = pytest.mark.asyncio
chat = types.Chat(**CHAT)
@ -59,3 +64,10 @@ def test_chat_actions():
assert types.ChatActions.FIND_LOCATION == 'find_location'
assert types.ChatActions.RECORD_VIDEO_NOTE == 'record_video_note'
assert types.ChatActions.UPLOAD_VIDEO_NOTE == 'upload_video_note'
async def test_update_chat(bot: Bot):
Bot.set_current(bot)
async with FakeTelegram(message_data=FULL_CHAT):
await chat.update_chat()
assert chat.to_python() == types.Chat(**FULL_CHAT).to_python()

View file

@ -1,12 +1,15 @@
import os
from io import BytesIO
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
from aiohttp import ClientResponseError
from aiogram import Bot
from aiogram.types import File
from aiogram.types.mixins import Downloadable
from aiogram.utils.json import json
from tests import TOKEN
from tests.types.dataset import FILE
@ -18,7 +21,8 @@ async def bot_fixture():
""" Bot fixture """
_bot = Bot(TOKEN)
yield _bot
await (await _bot.get_session()).close()
session = await _bot.get_session()
await session.close()
@pytest.fixture
@ -30,73 +34,81 @@ def tmppath(tmpdir, request):
@pytest.fixture
def downloadable(bot):
async def get_file():
return File(**FILE)
downloadable = Downloadable()
downloadable.get_file = get_file
downloadable.get_file = AsyncMock(return_value=File(**FILE))
downloadable.bot = bot
return downloadable
@pytest.fixture()
def get_file_response(aresponses):
aresponses.add(response=aresponses.Response(body=json.dumps(FILE)))
class TestDownloadable:
async def test_download_make_dirs_false_nodir(self, tmppath, downloadable):
async def test_download_make_dirs_false_nodir(self, tmppath, downloadable, get_file_response):
with pytest.raises(FileNotFoundError):
await downloadable.download(make_dirs=False)
async def test_download_make_dirs_false_mkdir(self, tmppath, downloadable):
async def test_download_make_dirs_false_mkdir(self, tmppath, downloadable, get_file_response):
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):
async def test_download_make_dirs_true(self, tmppath, downloadable, get_file_response):
await downloadable.download(make_dirs=True)
assert os.path.isfile(tmppath.joinpath(FILE["file_path"]))
async def test_download_deprecation_warning(self, tmppath, downloadable):
async def test_download_deprecation_warning(self, tmppath, downloadable, get_file_response):
with pytest.deprecated_call():
await downloadable.download("test.file")
async def test_download_destination(self, tmppath, downloadable):
async def test_download_destination(self, tmppath, downloadable, get_file_response):
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):
async def test_download_destination_dir_exist(self, tmppath, downloadable, get_file_response):
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):
async def test_download_destination_with_dir(self, tmppath, downloadable, get_file_response):
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):
async def test_download_destination_io_bytes(self, tmppath, downloadable, get_file_response):
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):
async def test_download_raise_value_error(self, tmppath, downloadable, get_file_response):
with pytest.raises(ValueError):
await downloadable.download(destination_dir="a", destination_file="b")
async def test_download_destination_dir(self, tmppath, downloadable):
async def test_download_destination_dir(self, tmppath, downloadable, get_file_response):
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):
async def test_download_destination_file(self, tmppath, downloadable, get_file_response):
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):
async def test_download_destination_file_with_dir(self, tmppath, downloadable, get_file_response):
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):
async def test_download_io_bytes(self, tmppath, downloadable, get_file_response):
file = BytesIO()
await downloadable.download(destination_file=file)
assert len(file.read()) != 0
async def test_download_404(self, tmppath, downloadable):
with pytest.raises(ClientResponseError) as exc_info:
await downloadable.download(destination_file='file_name')
assert exc_info.value.status == 404