mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-06 07:50:32 +00:00
Merge branch 'dev-2.x'
This commit is contained in:
commit
0e7a9006b3
38 changed files with 1119 additions and 655 deletions
2
Makefile
2
Makefile
|
|
@ -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__)")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ AIOGramBot
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.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
|
||||
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = (
|
|||
'utils',
|
||||
)
|
||||
|
||||
__version__ = '2.19'
|
||||
__api_version__ = '5.7'
|
||||
__version__ = '2.20'
|
||||
__api_version__ = '6.0'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
21
aiogram/types/chat_administrator_rights.py
Normal file
21
aiogram/types/chat_administrator_rights.py
Normal 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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
86
aiogram/types/menu_button.py
Normal file
86
aiogram/types/menu_button.py
Normal 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
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
11
aiogram/types/sent_web_app_message.py
Normal file
11
aiogram/types/sent_web_app_message.py
Normal 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()
|
||||
|
|
@ -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 []
|
||||
|
|
|
|||
13
aiogram/types/video_chat_ended.py
Normal file
13
aiogram/types/video_chat_ended.py
Normal 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()
|
||||
16
aiogram/types/video_chat_participants_invited.py
Normal file
16
aiogram/types/video_chat_participants_invited.py
Normal 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)
|
||||
14
aiogram/types/video_chat_scheduled.py
Normal file
14
aiogram/types/video_chat_scheduled.py
Normal 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()
|
||||
11
aiogram/types/video_chat_started.py
Normal file
11
aiogram/types/video_chat_started.py
Normal 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
|
||||
12
aiogram/types/web_app_data.py
Normal file
12
aiogram/types/web_app_data.py
Normal 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()
|
||||
11
aiogram/types/web_app_info.py
Normal file
11
aiogram/types/web_app_info.py
Normal 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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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
22
test.html
Normal 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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue