mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x'
# Conflicts: # aiogram/__init__.py
This commit is contained in:
commit
c954509a2a
43 changed files with 745 additions and 243 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1,2 +1 @@
|
|||
github: [JRootJunior]
|
||||
open_collective: aiogram
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -6,14 +6,14 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
|
||||
[](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)
|
||||
|
||||
**aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
|
||||
|
||||
You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
|
||||
You can [read the docs here](http://docs.aiogram.dev/en/latest/).
|
||||
|
||||
|
||||
## Official aiogram resources:
|
||||
|
|
@ -21,7 +21,7 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
|
|||
- Community: [@aiogram](https://t.me/aiogram)
|
||||
- Russian community: [@aiogram_ru](https://t.me/aiogram_ru)
|
||||
- Pip: [aiogram](https://pypi.python.org/pypi/aiogram)
|
||||
- Docs: [ReadTheDocs](http://aiogram.readthedocs.io)
|
||||
- Docs: [ReadTheDocs](http://docs.aiogram.dev)
|
||||
- Source: [Github repo](https://github.com/aiogram/aiogram)
|
||||
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
|
||||
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
|
||||
|
|
@ -45,13 +45,13 @@ Become a financial contributor and help us sustain our community. [[Contribute](
|
|||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/aiogram/organization/0/website"><img src="https://opencollective.com/aiogram/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/1/website"><img src="https://opencollective.com/aiogram/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/2/website"><img src="https://opencollective.com/aiogram/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/3/website"><img src="https://opencollective.com/aiogram/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/4/website"><img src="https://opencollective.com/aiogram/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/5/website"><img src="https://opencollective.com/aiogram/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/6/website"><img src="https://opencollective.com/aiogram/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/7/website"><img src="https://opencollective.com/aiogram/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/8/website"><img src="https://opencollective.com/aiogram/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/9/website"><img src="https://opencollective.com/aiogram/organization/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/0/website"><img src="https://opencollective.com/aiogram/organizations/0/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/1/website"><img src="https://opencollective.com/aiogram/organizations/1/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/2/website"><img src="https://opencollective.com/aiogram/organizations/2/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/3/website"><img src="https://opencollective.com/aiogram/organizations/3/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/4/website"><img src="https://opencollective.com/aiogram/organizations/4/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/5/website"><img src="https://opencollective.com/aiogram/organizations/5/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/6/website"><img src="https://opencollective.com/aiogram/organizations/6/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/7/website"><img src="https://opencollective.com/aiogram/organizations/7/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/8/website"><img src="https://opencollective.com/aiogram/organizations/8/avatar"></a>
|
||||
<a href="https://opencollective.com/aiogram/organizations/9/website"><img src="https://opencollective.com/aiogram/organizations/9/avatar"></a>
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ AIOGramBot
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
|
||||
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
|
||||
:target: http://docs.aiogram.dev/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
|
||||
|
|
@ -40,7 +40,7 @@ AIOGramBot
|
|||
|
||||
**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.7 with `asyncio <https://docs.python.org/3/library/asyncio.html>`_ and `aiohttp <https://github.com/aio-libs/aiohttp>`_. It helps you to make your bots faster and simpler.
|
||||
|
||||
You can `read the docs here <http://aiogram.readthedocs.io/en/latest/>`_.
|
||||
You can `read the docs here <http://docs.aiogram.dev/en/latest/>`_.
|
||||
|
||||
Official aiogram resources
|
||||
--------------------------
|
||||
|
|
@ -49,7 +49,7 @@ Official aiogram resources
|
|||
- Community: `@aiogram <https://t.me/aiogram>`_
|
||||
- Russian community: `@aiogram_ru <https://t.me/aiogram_ru>`_
|
||||
- Pip: `aiogram <https://pypi.python.org/pypi/aiogram>`_
|
||||
- Docs: `ReadTheDocs <http://aiogram.readthedocs.io>`_
|
||||
- Docs: `ReadTheDocs <http://docs.aiogram.dev>`_
|
||||
- Source: `Github repo <https://github.com/aiogram/aiogram>`_
|
||||
- Issues/Bug tracker: `Github issues tracker <https://github.com/aiogram/aiogram/issues>`_
|
||||
- Test bot: `@aiogram_bot <https://t.me/aiogram_bot>`_
|
||||
|
|
|
|||
|
|
@ -38,5 +38,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.4'
|
||||
__api_version__ = '4.4'
|
||||
__version__ = '2.5'
|
||||
__api_version__ = '4.5'
|
||||
|
|
|
|||
|
|
@ -24,11 +24,17 @@ def check_token(token: str) -> bool:
|
|||
:param token:
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(token, str):
|
||||
message = (f"Token is invalid! "
|
||||
f"It must be 'str' type instead of {type(token)} type.")
|
||||
raise exceptions.ValidationError(message)
|
||||
|
||||
if any(x.isspace() for x in token):
|
||||
raise exceptions.ValidationError('Token is invalid!')
|
||||
message = "Token is invalid! It can't contains spaces."
|
||||
raise exceptions.ValidationError(message)
|
||||
|
||||
left, sep, right = token.partition(':')
|
||||
if (not sep) or (not left.isdigit()) or (len(left) < 3):
|
||||
if (not sep) or (not left.isdigit()) or (not right):
|
||||
raise exceptions.ValidationError('Token is invalid!')
|
||||
|
||||
return True
|
||||
|
|
@ -147,7 +153,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 4.4
|
||||
List is updated to Bot API 4.5
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -182,6 +188,7 @@ class Methods(Helper):
|
|||
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
|
||||
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
|
||||
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
|
||||
SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle
|
||||
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
|
||||
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
|
||||
SET_CHAT_PHOTO = Item() # setChatPhoto
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import contextlib
|
|||
import io
|
||||
import ssl
|
||||
import typing
|
||||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
|
|
@ -74,9 +75,9 @@ class BaseBot:
|
|||
|
||||
if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')):
|
||||
from aiohttp_socks import SocksConnector
|
||||
from aiohttp_socks.helpers import parse_socks_url
|
||||
from aiohttp_socks.utils import parse_proxy_url
|
||||
|
||||
socks_ver, host, port, username, password = parse_socks_url(proxy)
|
||||
socks_ver, host, port, username, password = parse_proxy_url(proxy)
|
||||
if proxy_auth:
|
||||
if not username:
|
||||
username = proxy_auth.login
|
||||
|
|
@ -269,6 +270,10 @@ class BaseBot:
|
|||
if value not in ParseMode.all():
|
||||
raise ValueError(f"Parse mode must be one of {ParseMode.all()}")
|
||||
setattr(self, '_parse_mode', value)
|
||||
if value == 'markdown':
|
||||
warnings.warn("Parse mode `Markdown` is legacy since Telegram Bot API 4.5, "
|
||||
"retained for backward compatibility. Use `MarkdownV2` instead.\n"
|
||||
"https://core.telegram.org/bots/api#markdown-style", stacklevel=3)
|
||||
|
||||
@parse_mode.deleter
|
||||
def parse_mode(self):
|
||||
|
|
|
|||
|
|
@ -1118,6 +1118,24 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
|
||||
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
|
||||
return result
|
||||
|
||||
async def set_chat_administrator_custom_title(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, custom_title: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set a custom title for an administrator in a supergroup promoted by the bot.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target supergroup
|
||||
:param user_id: Unique identifier of the target user
|
||||
:param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed
|
||||
:return: True on success.
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload)
|
||||
return result
|
||||
|
||||
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
permissions: types.ChatPermissions) -> base.Boolean:
|
||||
|
|
@ -1825,8 +1843,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
Use this method to delete a sticker from a set created by the bot.
|
||||
|
||||
The following methods and objects allow your bot to work in inline mode.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#deletestickerfromset
|
||||
|
||||
:param sticker: File identifier of the sticker
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class I18nMiddleware(BaseMiddleware):
|
|||
locale = self.ctx_locale.get()
|
||||
|
||||
if locale not in self.locales:
|
||||
if n is 1:
|
||||
if n == 1:
|
||||
return singular
|
||||
return plural
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,9 @@ class CommandStart(Command):
|
|||
This filter based on :obj:`Command` filter but can handle only ``/start`` command.
|
||||
"""
|
||||
|
||||
def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None):
|
||||
def __init__(self,
|
||||
deep_link: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None,
|
||||
encoded: bool = False):
|
||||
"""
|
||||
Also this filter can handle `deep-linking <https://core.telegram.org/bots#deep-linking>`_ arguments.
|
||||
|
||||
|
|
@ -151,9 +153,11 @@ class CommandStart(Command):
|
|||
@dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)')))
|
||||
|
||||
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
|
||||
:param encoded: set True if you're waiting for encoded payload (default - False).
|
||||
"""
|
||||
super().__init__(['start'])
|
||||
self.deep_link = deep_link
|
||||
self.encoded = encoded
|
||||
|
||||
async def check(self, message: types.Message):
|
||||
"""
|
||||
|
|
@ -162,18 +166,21 @@ class CommandStart(Command):
|
|||
:param message:
|
||||
:return:
|
||||
"""
|
||||
from ...utils.deep_linking import decode_payload
|
||||
check = await super().check(message)
|
||||
|
||||
if check and self.deep_link is not None:
|
||||
if not isinstance(self.deep_link, re.Pattern):
|
||||
return message.get_args() == self.deep_link
|
||||
payload = decode_payload(message.get_args()) if self.encoded else message.get_args()
|
||||
|
||||
match = self.deep_link.match(message.get_args())
|
||||
if not isinstance(self.deep_link, typing.Pattern):
|
||||
return False if payload != self.deep_link else {'deep_link': payload}
|
||||
|
||||
match = self.deep_link.match(payload)
|
||||
if match:
|
||||
return {'deep_link': match}
|
||||
return False
|
||||
|
||||
return check
|
||||
return {'deep_link': None}
|
||||
|
||||
|
||||
class CommandHelp(Command):
|
||||
|
|
@ -244,7 +251,7 @@ class Text(Filter):
|
|||
raise ValueError(f"No one mode is specified!")
|
||||
|
||||
equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
|
||||
else e,
|
||||
else e,
|
||||
(equals, contains, endswith, startswith))
|
||||
self.equals = equals
|
||||
self.contains = contains
|
||||
|
|
@ -370,7 +377,7 @@ class Regexp(Filter):
|
|||
"""
|
||||
|
||||
def __init__(self, regexp):
|
||||
if not isinstance(regexp, re.Pattern):
|
||||
if not isinstance(regexp, typing.Pattern):
|
||||
regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE)
|
||||
self.regexp = regexp
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Animation(base.TelegramObject, mixins.Downloadable):
|
|||
"""
|
||||
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
thumb: PhotoSize = fields.Field(base=PhotoSize)
|
||||
file_name: base.String = fields.Field()
|
||||
mime_type: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class Audio(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#audio
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
duration: base.Integer = fields.Field()
|
||||
performer: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import asyncio
|
|||
import datetime
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from ..utils import helper, markdown
|
||||
from . import base, fields
|
||||
from .chat_member import ChatMember
|
||||
from .chat_permissions import ChatPermissions
|
||||
from .chat_photo import ChatPhoto
|
||||
from ..utils import helper
|
||||
from ..utils import markdown
|
||||
from .input_file import InputFile
|
||||
|
||||
|
||||
class Chat(base.TelegramObject):
|
||||
|
|
@ -30,6 +30,7 @@ class Chat(base.TelegramObject):
|
|||
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()
|
||||
sticker_set_name: base.String = fields.Field()
|
||||
can_set_sticker_set: base.Boolean = fields.Field()
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ class Chat(base.TelegramObject):
|
|||
return self.id
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
def full_name(self) -> base.String:
|
||||
if self.type == ChatType.PRIVATE:
|
||||
full_name = self.first_name
|
||||
if self.last_name:
|
||||
|
|
@ -46,7 +47,7 @@ class Chat(base.TelegramObject):
|
|||
return self.title
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
def mention(self) -> typing.Union[base.String, None]:
|
||||
"""
|
||||
Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned
|
||||
"""
|
||||
|
|
@ -57,20 +58,20 @@ class Chat(base.TelegramObject):
|
|||
return None
|
||||
|
||||
@property
|
||||
def user_url(self):
|
||||
def user_url(self) -> base.String:
|
||||
if self.type != ChatType.PRIVATE:
|
||||
raise TypeError('`user_url` property is only available in private chats!')
|
||||
|
||||
return f"tg://user?id={self.id}"
|
||||
|
||||
def get_mention(self, name=None, as_html=False):
|
||||
def get_mention(self, name=None, as_html=True) -> base.String:
|
||||
if name is None:
|
||||
name = self.mention
|
||||
if as_html:
|
||||
return markdown.hlink(name, self.user_url)
|
||||
return markdown.link(name, self.user_url)
|
||||
|
||||
async def get_url(self):
|
||||
async def get_url(self) -> base.String:
|
||||
"""
|
||||
Use this method to get chat link.
|
||||
Private chat returns user link.
|
||||
|
|
@ -101,7 +102,7 @@ class Chat(base.TelegramObject):
|
|||
for key, value in other:
|
||||
self[key] = value
|
||||
|
||||
async def set_photo(self, photo):
|
||||
async def set_photo(self, photo: InputFile) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set a new profile photo for the chat. Photos can't be changed for private chats.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -118,7 +119,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.set_chat_photo(self.id, photo)
|
||||
|
||||
async def delete_photo(self):
|
||||
async def delete_photo(self) -> base.Boolean:
|
||||
"""
|
||||
Use this method to delete a chat photo. Photos can't be changed for private chats.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -133,7 +134,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.delete_chat_photo(self.id)
|
||||
|
||||
async def set_title(self, title):
|
||||
async def set_title(self, title: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method to change the title of a chat. Titles can't be changed for private chats.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -150,7 +151,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.set_chat_title(self.id, title)
|
||||
|
||||
async def set_description(self, description):
|
||||
async def set_description(self, description: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method to change the description of a supergroup or a channel.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -165,7 +166,7 @@ class Chat(base.TelegramObject):
|
|||
return await self.bot.delete_chat_description(self.id, description)
|
||||
|
||||
async def kick(self, user_id: base.Integer,
|
||||
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None):
|
||||
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean:
|
||||
"""
|
||||
Use this method to kick a user from a group, a supergroup or a channel.
|
||||
In the case of supergroups and channels, the user will not be able to return to the group
|
||||
|
|
@ -188,7 +189,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date)
|
||||
|
||||
async def unban(self, user_id: base.Integer):
|
||||
async def unban(self, user_id: base.Integer) -> base.Boolean:
|
||||
"""
|
||||
Use this method to unban a previously kicked user in a supergroup or channel. `
|
||||
The user will not return to the group or channel automatically, but will be able to join via link, etc.
|
||||
|
|
@ -296,7 +297,34 @@ class Chat(base.TelegramObject):
|
|||
can_pin_messages=can_pin_messages,
|
||||
can_promote_members=can_promote_members)
|
||||
|
||||
async def pin_message(self, message_id: int, disable_notification: bool = False):
|
||||
async def set_permissions(self, permissions: ChatPermissions) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set default chat permissions for all members.
|
||||
The bot must be an administrator in the group or a supergroup for this to work and must have the
|
||||
can_restrict_members admin rights.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
:param permissions: New default chat permissions
|
||||
:return: True on success.
|
||||
"""
|
||||
return await self.bot.set_chat_permissions(self.id, permissions=permissions)
|
||||
|
||||
async def set_administrator_custom_title(self, user_id: base.Integer, custom_title: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set a custom title for an administrator in a supergroup promoted by the bot.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle
|
||||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed
|
||||
:return: True on success.
|
||||
"""
|
||||
return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id, custom_title=custom_title)
|
||||
|
||||
async def pin_message(self, message_id: base.Integer, disable_notification: base.Boolean = False) -> base.Boolean:
|
||||
"""
|
||||
Use this method to pin a message in a supergroup.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -313,7 +341,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.pin_chat_message(self.id, message_id, disable_notification)
|
||||
|
||||
async def unpin_message(self):
|
||||
async def unpin_message(self) -> base.Boolean:
|
||||
"""
|
||||
Use this method to unpin a message in a supergroup chat.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
@ -325,7 +353,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.unpin_chat_message(self.id)
|
||||
|
||||
async def leave(self):
|
||||
async def leave(self) -> base.Boolean:
|
||||
"""
|
||||
Use this method for your bot to leave a group, supergroup or channel.
|
||||
|
||||
|
|
@ -336,7 +364,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.leave_chat(self.id)
|
||||
|
||||
async def get_administrators(self):
|
||||
async def get_administrators(self) -> typing.List[ChatMember]:
|
||||
"""
|
||||
Use this method to get a list of administrators in a chat.
|
||||
|
||||
|
|
@ -350,7 +378,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.get_chat_administrators(self.id)
|
||||
|
||||
async def get_members_count(self):
|
||||
async def get_members_count(self) -> base.Integer:
|
||||
"""
|
||||
Use this method to get the number of members in a chat.
|
||||
|
||||
|
|
@ -361,7 +389,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.get_chat_members_count(self.id)
|
||||
|
||||
async def get_member(self, user_id):
|
||||
async def get_member(self, user_id: base.Integer) -> ChatMember:
|
||||
"""
|
||||
Use this method to get information about a member of a chat.
|
||||
|
||||
|
|
@ -374,7 +402,39 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.get_chat_member(self.id, user_id)
|
||||
|
||||
async def do(self, action):
|
||||
async def set_sticker_set(self, sticker_set_name: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set a new group sticker set for a supergroup.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
||||
Use the field can_set_sticker_set optionally returned in getChat requests to check
|
||||
if the bot can use this method.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setchatstickerset
|
||||
|
||||
:param sticker_set_name: Name of the sticker set to be set as the group sticker set
|
||||
:type sticker_set_name: :obj:`base.String`
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.set_chat_sticker_set(self.id, sticker_set_name=sticker_set_name)
|
||||
|
||||
async def delete_sticker_set(self) -> base.Boolean:
|
||||
"""
|
||||
Use this method to delete a group sticker set from a supergroup.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
||||
Use the field can_set_sticker_set optionally returned in getChat requests
|
||||
to check if the bot can use this method.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#deletechatstickerset
|
||||
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.delete_chat_sticker_set(self.id)
|
||||
|
||||
async def do(self, action: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method when you need to tell the user that something is happening on the bot's side.
|
||||
The status is set for 5 seconds or less
|
||||
|
|
@ -392,7 +452,7 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.send_chat_action(self.id, action)
|
||||
|
||||
async def export_invite_link(self):
|
||||
async def export_invite_link(self) -> base.String:
|
||||
"""
|
||||
Use this method to export an invite link to a supergroup or a channel.
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class ChatMember(base.TelegramObject):
|
|||
"""
|
||||
user: User = fields.Field(base=User)
|
||||
status: base.String = fields.Field()
|
||||
custom_title: base.String = fields.Field()
|
||||
until_date: datetime.datetime = fields.DateTimeField()
|
||||
can_be_edited: base.Boolean = fields.Field()
|
||||
can_change_info: base.Boolean = fields.Field()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ class ChatPhoto(base.TelegramObject):
|
|||
https://core.telegram.org/bots/api#chatphoto
|
||||
"""
|
||||
small_file_id: base.String = fields.Field()
|
||||
small_file_unique_id: base.String = fields.Field()
|
||||
big_file_id: base.String = fields.Field()
|
||||
big_file_unique_id: base.String = fields.Field()
|
||||
|
||||
async def download_small(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class Document(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#document
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
thumb: PhotoSize = fields.Field(base=PhotoSize)
|
||||
file_name: base.String = fields.Field()
|
||||
mime_type: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@ class File(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#file
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
file_size: base.Integer = fields.Field()
|
||||
file_path: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import functools
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
|
|
@ -32,6 +31,7 @@ from .video_note import VideoNote
|
|||
from .voice import Voice
|
||||
from ..utils import helper
|
||||
from ..utils import markdown as md
|
||||
from ..utils.text_decorations import html_decoration, markdown_decoration
|
||||
|
||||
|
||||
class Message(base.TelegramObject):
|
||||
|
|
@ -200,38 +200,10 @@ class Message(base.TelegramObject):
|
|||
if text is None:
|
||||
raise TypeError("This message doesn't have any text.")
|
||||
|
||||
quote_fn = md.quote_html if as_html else md.escape_md
|
||||
|
||||
entities = self.entities or self.caption_entities
|
||||
if not entities:
|
||||
return quote_fn(text)
|
||||
text_decorator = html_decoration if as_html else markdown_decoration
|
||||
|
||||
if not sys.maxunicode == 0xffff:
|
||||
text = text.encode('utf-16-le')
|
||||
|
||||
result = ''
|
||||
offset = 0
|
||||
|
||||
for entity in sorted(entities, key=lambda item: item.offset):
|
||||
entity_text = entity.parse(text, as_html=as_html)
|
||||
|
||||
if sys.maxunicode == 0xffff:
|
||||
part = text[offset:entity.offset]
|
||||
result += quote_fn(part) + entity_text
|
||||
else:
|
||||
part = text[offset * 2:entity.offset * 2]
|
||||
result += quote_fn(part.decode('utf-16-le')) + entity_text
|
||||
|
||||
offset = entity.offset + entity.length
|
||||
|
||||
if sys.maxunicode == 0xffff:
|
||||
part = text[offset:]
|
||||
result += quote_fn(part)
|
||||
else:
|
||||
part = text[offset * 2:]
|
||||
result += quote_fn(part.decode('utf-16-le'))
|
||||
|
||||
return result
|
||||
return text_decorator.unparse(text, entities)
|
||||
|
||||
@property
|
||||
def md_text(self) -> str:
|
||||
|
|
@ -1798,4 +1770,5 @@ class ParseMode(helper.Helper):
|
|||
mode = helper.HelperMode.lowercase
|
||||
|
||||
MARKDOWN = helper.Item()
|
||||
MARKDOWN_V2 = helper.Item()
|
||||
HTML = helper.Item()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from . import base
|
|||
from . import fields
|
||||
from .user import User
|
||||
from ..utils import helper, markdown
|
||||
from ..utils.deprecated import deprecated
|
||||
|
||||
|
||||
class MessageEntity(base.TelegramObject):
|
||||
|
|
@ -36,6 +37,7 @@ class MessageEntity(base.TelegramObject):
|
|||
entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2]
|
||||
return entity_text.decode('utf-16-le')
|
||||
|
||||
@deprecated("This method doesn't work with nested entities and will be removed in aiogram 3.0")
|
||||
def parse(self, text, as_html=True):
|
||||
"""
|
||||
Get entity value with markup
|
||||
|
|
@ -87,6 +89,8 @@ class MessageEntityType(helper.Helper):
|
|||
:key: ITALIC
|
||||
:key: CODE
|
||||
:key: PRE
|
||||
:key: UNDERLINE
|
||||
:key: STRIKETHROUGH
|
||||
:key: TEXT_LINK
|
||||
:key: TEXT_MENTION
|
||||
"""
|
||||
|
|
@ -101,7 +105,9 @@ 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
|
||||
CODE = helper.Item() # code - monowidth string
|
||||
PRE = helper.Item() # pre - monowidth block
|
||||
UNDERLINE = helper.Item() # underline
|
||||
STRIKETHROUGH = helper.Item() # strikethrough
|
||||
TEXT_LINK = helper.Item() # text_link - for clickable text URLs
|
||||
TEXT_MENTION = helper.Item() # text_mention - for users without usernames
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class PassportFile(base.TelegramObject):
|
|||
|
||||
https://core.telegram.org/bots/api#passportfile
|
||||
"""
|
||||
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
file_size: base.Integer = fields.Field()
|
||||
file_date: base.Integer = fields.Field()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class PhotoSize(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#photosize
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
file_size: base.Integer = fields.Field()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#sticker
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
is_animated: base.Boolean = fields.Field()
|
||||
|
|
@ -20,3 +21,29 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
|
|||
set_name: base.String = fields.Field()
|
||||
mask_position: MaskPosition = fields.Field(base=MaskPosition)
|
||||
file_size: base.Integer = fields.Field()
|
||||
|
||||
async def set_position_in_set(self, position: base.Integer) -> base.Boolean:
|
||||
"""
|
||||
Use this method to move a sticker in a set created by the bot to a specific position.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setstickerpositioninset
|
||||
|
||||
:param position: New sticker position in the set, zero-based
|
||||
:type position: :obj:`base.Integer`
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.set_sticker_position_in_set(self.file_id, position=position)
|
||||
|
||||
async def delete_from_set(self) -> base.Boolean:
|
||||
"""
|
||||
Use this method to delete a sticker from a set created by the bot.
|
||||
|
||||
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`
|
||||
"""
|
||||
return await self.bot.delete_sticker_from_set(self.file_id)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import babel
|
|||
from . import base
|
||||
from . import fields
|
||||
from ..utils import markdown
|
||||
from ..utils.deprecated import deprecated
|
||||
|
||||
|
||||
class User(base.TelegramObject):
|
||||
|
|
@ -73,9 +74,16 @@ class User(base.TelegramObject):
|
|||
return markdown.hlink(name, self.url)
|
||||
return markdown.link(name, self.url)
|
||||
|
||||
@deprecated(
|
||||
'`get_user_profile_photos` is outdated, please use `get_profile_photos`',
|
||||
stacklevel=3
|
||||
)
|
||||
async def get_user_profile_photos(self, offset=None, limit=None):
|
||||
return await self.bot.get_user_profile_photos(self.id, offset, limit)
|
||||
|
||||
async def get_profile_photos(self, offset=None, limit=None):
|
||||
return await self.bot.get_user_profile_photos(self.id, offset, limit)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class Video(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#video
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
duration: base.Integer = fields.Field()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class VideoNote(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#videonote
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
length: base.Integer = fields.Field()
|
||||
duration: base.Integer = fields.Field()
|
||||
thumb: PhotoSize = fields.Field(base=PhotoSize)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Voice(base.TelegramObject, mixins.Downloadable):
|
|||
https://core.telegram.org/bots/api#voice
|
||||
"""
|
||||
file_id: base.String = fields.Field()
|
||||
file_unique_id: base.String = fields.Field()
|
||||
duration: base.Integer = fields.Field()
|
||||
mime_type: base.String = fields.Field()
|
||||
file_size: base.Integer = fields.Field()
|
||||
|
|
|
|||
101
aiogram/utils/deep_linking.py
Normal file
101
aiogram/utils/deep_linking.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
Deep linking
|
||||
|
||||
Telegram bots have a deep linking mechanism, that allows for passing additional
|
||||
parameters to the bot on startup. It could be a command that launches the bot — or
|
||||
an auth token to connect the user's Telegram account to their account on some
|
||||
external service.
|
||||
|
||||
You can read detailed description in the source:
|
||||
https://core.telegram.org/bots#deep-linking
|
||||
|
||||
We have add some utils to get deep links more handy.
|
||||
|
||||
Basic link example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link
|
||||
link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo'
|
||||
|
||||
Encoded link example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link, decode_payload
|
||||
link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v'
|
||||
# and decode it back:
|
||||
payload = decode_payload('Zm9v') # result: 'foo'
|
||||
|
||||
"""
|
||||
|
||||
|
||||
async def get_start_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'start' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('start', payload, encode)
|
||||
|
||||
|
||||
async def get_startgroup_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'startgroup' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('startgroup', payload, encode)
|
||||
|
||||
|
||||
async def _create_link(link_type, payload: str, encode=False):
|
||||
bot = await _get_bot_user()
|
||||
payload = filter_payload(payload)
|
||||
if encode:
|
||||
payload = encode_payload(payload)
|
||||
return f'https://t.me/{bot.username}?{link_type}={payload}'
|
||||
|
||||
|
||||
def encode_payload(payload: str) -> str:
|
||||
""" Encode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64encode
|
||||
result: bytes = urlsafe_b64encode(payload.encode())
|
||||
return result.decode()
|
||||
|
||||
|
||||
def decode_payload(payload: str) -> str:
|
||||
""" Decode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64decode
|
||||
result: bytes = urlsafe_b64decode(payload + '=' * (4 - len(payload) % 4))
|
||||
return result.decode()
|
||||
|
||||
|
||||
def filter_payload(payload: str) -> str:
|
||||
""" Convert payload to text and search for not allowed symbols. """
|
||||
import re
|
||||
|
||||
# convert to string
|
||||
if not isinstance(payload, str):
|
||||
payload = str(payload)
|
||||
|
||||
# search for not allowed characters
|
||||
if re.search(r'[^_A-z0-9-]', payload):
|
||||
message = ('Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. '
|
||||
'We recommend to encode parameters with binary and other '
|
||||
'types of content.')
|
||||
raise ValueError(message)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
async def _get_bot_user():
|
||||
""" Get current user of bot. """
|
||||
from ..bot import Bot
|
||||
bot = Bot.get_current()
|
||||
return await bot.me
|
||||
|
|
@ -99,35 +99,27 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
|
|||
"""
|
||||
|
||||
def decorator(func):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
is_coroutine = asyncio.iscoroutinefunction(func)
|
||||
|
||||
def _handling(kwargs):
|
||||
routine_type = 'coroutine' if is_coroutine else 'function'
|
||||
if old_name in kwargs:
|
||||
warn_deprecated(f"In {routine_type} '{func.__name__}' argument '{old_name}' "
|
||||
f"is renamed to '{new_name}' "
|
||||
f"and will be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel)
|
||||
kwargs.update({new_name: kwargs.pop(old_name)})
|
||||
return kwargs
|
||||
|
||||
if is_coroutine:
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args, **kwargs):
|
||||
if old_name in kwargs:
|
||||
warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' "
|
||||
f"is renamed to '{new_name}' "
|
||||
f"and will be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel)
|
||||
kwargs.update(
|
||||
{
|
||||
new_name: kwargs[old_name],
|
||||
}
|
||||
)
|
||||
kwargs.pop(old_name)
|
||||
kwargs = _handling(kwargs)
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if old_name in kwargs:
|
||||
warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` "
|
||||
f"is renamed to `{new_name}` "
|
||||
f"and will be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel)
|
||||
kwargs.update(
|
||||
{
|
||||
new_name: kwargs[old_name],
|
||||
}
|
||||
)
|
||||
kwargs.pop(old_name)
|
||||
kwargs = _handling(kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
- UnsupportedUrlProtocol
|
||||
- CantParseEntities
|
||||
- ResultIdDuplicate
|
||||
- MethodIsNotAvailable
|
||||
- ConflictError
|
||||
- TerminatedByOtherGetUpdates
|
||||
- CantGetUpdates
|
||||
|
|
@ -461,6 +462,10 @@ class BotDomainInvalid(BadRequest):
|
|||
text = 'Invalid bot domain'
|
||||
|
||||
|
||||
class MethodIsNotAvailable(BadRequest):
|
||||
match = "Method is available only for supergroups"
|
||||
|
||||
|
||||
class NotFound(TelegramAPIError, _MatchErrorMixin):
|
||||
__group = True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,28 @@
|
|||
LIST_MD_SYMBOLS = '*_`['
|
||||
from .text_decorations import html_decoration, markdown_decoration
|
||||
|
||||
LIST_MD_SYMBOLS = "*_`["
|
||||
|
||||
MD_SYMBOLS = (
|
||||
(LIST_MD_SYMBOLS[0], LIST_MD_SYMBOLS[0]),
|
||||
(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>'),
|
||||
(LIST_MD_SYMBOLS[2] * 3 + "\n", "\n" + LIST_MD_SYMBOLS[2] * 3),
|
||||
("<b>", "</b>"),
|
||||
("<i>", "</i>"),
|
||||
("<code>", "</code>"),
|
||||
("<pre>", "</pre>"),
|
||||
)
|
||||
|
||||
HTML_QUOTES_MAP = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
}
|
||||
HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """}
|
||||
|
||||
_HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS
|
||||
|
||||
|
||||
def _join(*content, sep=' '):
|
||||
def _join(*content, sep=" "):
|
||||
return sep.join(map(str, content))
|
||||
|
||||
|
||||
def _escape(s, symbols=LIST_MD_SYMBOLS):
|
||||
for symbol in symbols:
|
||||
s = s.replace(symbol, '\\' + symbol)
|
||||
return s
|
||||
|
||||
|
||||
def _md(string, symbols=('', '')):
|
||||
start, end = symbols
|
||||
return start + string + end
|
||||
|
||||
|
||||
def quote_html(content):
|
||||
"""
|
||||
Quote HTML symbols
|
||||
|
||||
All <, >, & and " symbols that are not a part of a tag or
|
||||
an HTML entity must be replaced with the corresponding HTML entities
|
||||
(< with < > with > & with & and " with ").
|
||||
|
||||
:param content: str
|
||||
:return: str
|
||||
"""
|
||||
new_content = ''
|
||||
for symbol in content:
|
||||
new_content += HTML_QUOTES_MAP[symbol] if symbol in _HQS else symbol
|
||||
return new_content
|
||||
|
||||
|
||||
def text(*content, sep=' '):
|
||||
def text(*content, sep=" "):
|
||||
"""
|
||||
Join all elements with a separator
|
||||
|
||||
|
|
@ -64,7 +33,7 @@ def text(*content, sep=' '):
|
|||
return _join(*content, sep=sep)
|
||||
|
||||
|
||||
def bold(*content, sep=' '):
|
||||
def bold(*content, sep=" "):
|
||||
"""
|
||||
Make bold text (Markdown)
|
||||
|
||||
|
|
@ -72,10 +41,10 @@ def bold(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[0])
|
||||
return markdown_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def hbold(*content, sep=' '):
|
||||
def hbold(*content, sep=" "):
|
||||
"""
|
||||
Make bold text (HTML)
|
||||
|
||||
|
|
@ -83,10 +52,10 @@ def hbold(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[4])
|
||||
return html_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def italic(*content, sep=' '):
|
||||
def italic(*content, sep=" "):
|
||||
"""
|
||||
Make italic text (Markdown)
|
||||
|
||||
|
|
@ -94,10 +63,10 @@ def italic(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[1])
|
||||
return markdown_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def hitalic(*content, sep=' '):
|
||||
def hitalic(*content, sep=" "):
|
||||
"""
|
||||
Make italic text (HTML)
|
||||
|
||||
|
|
@ -105,10 +74,10 @@ def hitalic(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[5])
|
||||
return html_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def code(*content, sep=' '):
|
||||
def code(*content, sep=" "):
|
||||
"""
|
||||
Make mono-width text (Markdown)
|
||||
|
||||
|
|
@ -116,10 +85,10 @@ def code(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[2])
|
||||
return markdown_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def hcode(*content, sep=' '):
|
||||
def hcode(*content, sep=" "):
|
||||
"""
|
||||
Make mono-width text (HTML)
|
||||
|
||||
|
|
@ -127,10 +96,10 @@ def hcode(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[6])
|
||||
return html_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def pre(*content, sep='\n'):
|
||||
def pre(*content, sep="\n"):
|
||||
"""
|
||||
Make mono-width text block (Markdown)
|
||||
|
||||
|
|
@ -138,10 +107,10 @@ def pre(*content, sep='\n'):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[3])
|
||||
return markdown_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def hpre(*content, sep='\n'):
|
||||
def hpre(*content, sep="\n"):
|
||||
"""
|
||||
Make mono-width text block (HTML)
|
||||
|
||||
|
|
@ -149,10 +118,60 @@ def hpre(*content, sep='\n'):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[7])
|
||||
return html_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def link(title, url):
|
||||
def underline(*content, sep=" "):
|
||||
"""
|
||||
Make underlined text (Markdown)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return markdown_decoration.underline.format(
|
||||
value=markdown_decoration.quote(_join(*content, sep=sep))
|
||||
)
|
||||
|
||||
|
||||
def hunderline(*content, sep=" "):
|
||||
"""
|
||||
Make underlined text (HTML)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return html_decoration.underline.format(value=html_decoration.quote(_join(*content, sep=sep)))
|
||||
|
||||
|
||||
def strikethrough(*content, sep=" "):
|
||||
"""
|
||||
Make strikethrough text (Markdown)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return markdown_decoration.strikethrough.format(
|
||||
value=markdown_decoration.quote(_join(*content, sep=sep))
|
||||
)
|
||||
|
||||
|
||||
def hstrikethrough(*content, sep=" "):
|
||||
"""
|
||||
Make strikethrough text (HTML)
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return html_decoration.strikethrough.format(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
)
|
||||
|
||||
|
||||
def link(title: str, url: str) -> str:
|
||||
"""
|
||||
Format URL (Markdown)
|
||||
|
||||
|
|
@ -160,10 +179,10 @@ def link(title, url):
|
|||
:param url:
|
||||
:return:
|
||||
"""
|
||||
return "[{0}]({1})".format(title, url)
|
||||
return markdown_decoration.link.format(value=html_decoration.quote(title), link=url)
|
||||
|
||||
|
||||
def hlink(title, url):
|
||||
def hlink(title: str, url: str) -> str:
|
||||
"""
|
||||
Format URL (HTML)
|
||||
|
||||
|
|
@ -171,23 +190,10 @@ def hlink(title, url):
|
|||
:param url:
|
||||
:return:
|
||||
"""
|
||||
return '<a href="{0}">{1}</a>'.format(url, quote_html(title))
|
||||
return html_decoration.link.format(value=html_decoration.quote(title), link=url)
|
||||
|
||||
|
||||
def escape_md(*content, sep=' '):
|
||||
"""
|
||||
Escape markdown text
|
||||
|
||||
E.g. for usernames
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _escape(_join(*content, sep=sep))
|
||||
|
||||
|
||||
def hide_link(url):
|
||||
def hide_link(url: str) -> str:
|
||||
"""
|
||||
Hide URL (HTML only)
|
||||
Can be used for adding an image to a text message
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ class DataMixin:
|
|||
def __delitem__(self, key):
|
||||
del self.data[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.data
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.data.get(key, default)
|
||||
|
||||
|
|
|
|||
143
aiogram/utils/text_decorations.py
Normal file
143
aiogram/utils/text_decorations.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
from __future__ import annotations
|
||||
import html
|
||||
import re
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.types import MessageEntity
|
||||
|
||||
__all__ = (
|
||||
"TextDecoration",
|
||||
"html_decoration",
|
||||
"markdown_decoration",
|
||||
"add_surrogate",
|
||||
"remove_surrogate",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextDecoration:
|
||||
link: str
|
||||
bold: str
|
||||
italic: str
|
||||
code: str
|
||||
pre: str
|
||||
underline: str
|
||||
strikethrough: str
|
||||
quote: Callable[[AnyStr], AnyStr]
|
||||
|
||||
def apply_entity(self, entity: MessageEntity, text: str) -> str:
|
||||
"""
|
||||
Apply single entity to text
|
||||
|
||||
:param entity:
|
||||
:param text:
|
||||
:return:
|
||||
"""
|
||||
if entity.type in (
|
||||
"bold",
|
||||
"italic",
|
||||
"code",
|
||||
"pre",
|
||||
"underline",
|
||||
"strikethrough",
|
||||
):
|
||||
return getattr(self, entity.type).format(value=text)
|
||||
elif entity.type == "text_mention":
|
||||
return self.link.format(value=text, link=f"tg://user?id={entity.user.id}")
|
||||
elif entity.type == "text_link":
|
||||
return self.link.format(value=text, link=entity.url)
|
||||
elif entity.type == "url":
|
||||
return text
|
||||
return self.quote(text)
|
||||
|
||||
def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str:
|
||||
"""
|
||||
Unparse message entities
|
||||
|
||||
:param text: raw text
|
||||
:param entities: Array of MessageEntities
|
||||
:return:
|
||||
"""
|
||||
text = add_surrogate(text)
|
||||
result = "".join(
|
||||
self._unparse_entities(
|
||||
text, sorted(entities, key=lambda item: item.offset) if entities else []
|
||||
)
|
||||
)
|
||||
return remove_surrogate(result)
|
||||
|
||||
def _unparse_entities(
|
||||
self,
|
||||
text: str,
|
||||
entities: Iterable[MessageEntity],
|
||||
offset: Optional[int] = None,
|
||||
length: Optional[int] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
offset = offset or 0
|
||||
length = length or len(text)
|
||||
|
||||
for index, entity in enumerate(entities):
|
||||
if entity.offset < offset:
|
||||
continue
|
||||
if entity.offset > offset:
|
||||
yield self.quote(text[offset : entity.offset])
|
||||
start = entity.offset
|
||||
offset = entity.offset + entity.length
|
||||
|
||||
sub_entities = list(
|
||||
filter(lambda e: e.offset < offset, entities[index + 1 :])
|
||||
)
|
||||
yield self.apply_entity(
|
||||
entity,
|
||||
"".join(
|
||||
self._unparse_entities(
|
||||
text, sub_entities, offset=start, length=offset
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if offset < length:
|
||||
yield self.quote(text[offset:length])
|
||||
|
||||
|
||||
html_decoration = TextDecoration(
|
||||
link='<a href="{link}">{value}</a>',
|
||||
bold="<b>{value}</b>",
|
||||
italic="<i>{value}</i>",
|
||||
code="<code>{value}</code>",
|
||||
pre="<pre>{value}</pre>",
|
||||
underline="<u>{value}</u>",
|
||||
strikethrough="<s>{value}</s>",
|
||||
quote=html.escape,
|
||||
)
|
||||
|
||||
MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-|{}.!])")
|
||||
|
||||
markdown_decoration = TextDecoration(
|
||||
link="[{value}]({link})",
|
||||
bold="*{value}*",
|
||||
italic="_{value}_\r",
|
||||
code="`{value}`",
|
||||
pre="```{value}```",
|
||||
underline="__{value}__",
|
||||
strikethrough="~{value}~",
|
||||
quote=lambda text: re.sub(
|
||||
pattern=MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=text
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def add_surrogate(text: str) -> str:
|
||||
return "".join(
|
||||
"".join(chr(d) for d in struct.unpack("<HH", s.encode("utf-16-le")))
|
||||
if (0x10000 <= ord(s) <= 0x10FFFF)
|
||||
else s
|
||||
for s in text
|
||||
)
|
||||
|
||||
|
||||
def remove_surrogate(text: str) -> str:
|
||||
return text.encode("utf-16", "surrogatepass").decode("utf-16")
|
||||
|
|
@ -13,6 +13,6 @@ wheel>=0.31.1
|
|||
sphinx>=2.0.1
|
||||
sphinx-rtd-theme>=0.4.3
|
||||
sphinxcontrib-programoutput>=0.14
|
||||
aiohttp-socks>=0.2.2
|
||||
aiohttp-socks>=0.3.3
|
||||
rethinkdb>=2.4.1
|
||||
coverage==4.5.3
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ 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-4.4-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
|
||||
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
|
||||
:target: http://docs.aiogram.dev/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
|
||||
|
|
@ -48,7 +48,7 @@ Official aiogram resources
|
|||
- Community: `@aiogram <https://t.me/aiogram>`_
|
||||
- Russian community: `@aiogram_ru <https://t.me/aiogram_ru>`_
|
||||
- Pip: `aiogram <https://pypi.python.org/pypi/aiogram>`_
|
||||
- Docs: `ReadTheDocs <http://aiogram.readthedocs.io>`_
|
||||
- Docs: `ReadTheDocs <http://docs.aiogram.dev>`_
|
||||
- Source: `Github repo <https://github.com/aiogram/aiogram>`_
|
||||
- Issues/Bug tracker: `Github issues tracker <https://github.com/aiogram/aiogram/issues>`_
|
||||
- Test bot: `@aiogram_bot <https://t.me/aiogram_bot>`_
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Using Pipenv
|
|||
|
||||
Using AUR
|
||||
---------
|
||||
*aiogram* is also available in Arch User Repository, so you can install this library on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram <https://aur.archlinux.org/packages/python-aiogram/>`_ package.
|
||||
*aiogram* is also available in Arch User Repository, so you can install this framework on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram <https://aur.archlinux.org/packages/python-aiogram/>`_ package.
|
||||
|
||||
From sources
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Also you can bind your own filters for using as keyword arguments:
|
|||
|
||||
async def check(self, message: types.Message):
|
||||
member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
return member.is_admin()
|
||||
return member.is_chat_admin()
|
||||
|
||||
dp.filters_factory.bind(MyFilter)
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ Example:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
URL = 'https://aiogram.readthedocs.io/en/dev-2.x/_static/logo.png'
|
||||
URL = 'https://docs.aiogram.dev/en/dev-2.x/_static/logo.png'
|
||||
|
||||
|
||||
@dp.message_handler(commands=['image, img'])
|
||||
|
|
|
|||
6
docs/source/utils/deep_linking.rst
Normal file
6
docs/source/utils/deep_linking.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
============
|
||||
Deep linking
|
||||
============
|
||||
|
||||
.. automodule:: aiogram.utils.deep_linking
|
||||
:members:
|
||||
|
|
@ -46,7 +46,7 @@ async def echo(message: types.Message):
|
|||
# old style:
|
||||
# await bot.send_message(message.chat.id, message.text)
|
||||
|
||||
await message.reply(message.text, reply=False)
|
||||
await message.answer(message.text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ async def cmd_start(message: types.Message):
|
|||
|
||||
@dp.message_handler(commands='lang')
|
||||
async def cmd_lang(message: types.Message, locale):
|
||||
# For setting custom lang you have to modify i18n middleware, like this:
|
||||
# https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py
|
||||
# For setting custom lang you have to modify i18n middleware
|
||||
await message.reply(_('Your current language: <i>{language}</i>').format(language=locale))
|
||||
|
||||
# If you care about pluralization, here's small handler
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ async def handler2(msg: types.Message):
|
|||
|
||||
@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required)
|
||||
async def handler3(msg: types.Message):
|
||||
await msg.reply("Hello from user= & chat_id=", reply=False)
|
||||
await msg.answer("Hello from user= & chat_id=")
|
||||
|
||||
|
||||
@dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here
|
||||
async def handler4(msg: types.Message):
|
||||
await msg.reply("Checked user_id with list!", reply=False)
|
||||
await msg.answer("Checked user_id with list!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
38
setup.py
38
setup.py
|
|
@ -5,11 +5,6 @@ import sys
|
|||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
try:
|
||||
from pip.req import parse_requirements
|
||||
except ImportError: # pip >= 10.0.0
|
||||
from pip._internal.req import parse_requirements
|
||||
|
||||
WORK_DIR = pathlib.Path(__file__).parent
|
||||
|
||||
# Check python version
|
||||
|
|
@ -42,22 +37,6 @@ def get_description():
|
|||
return f.read()
|
||||
|
||||
|
||||
def get_requirements(filename=None):
|
||||
"""
|
||||
Read requirements from 'requirements txt'
|
||||
|
||||
:return: requirements
|
||||
:rtype: list
|
||||
"""
|
||||
if filename is None:
|
||||
filename = 'requirements.txt'
|
||||
|
||||
file = WORK_DIR / filename
|
||||
|
||||
install_reqs = parse_requirements(str(file), session='hack')
|
||||
return [str(ir.req) for ir in install_reqs]
|
||||
|
||||
|
||||
setup(
|
||||
name='aiogram',
|
||||
version=get_version(),
|
||||
|
|
@ -77,9 +56,22 @@ setup(
|
|||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
||||
],
|
||||
install_requires=get_requirements(),
|
||||
package_data={'': ['requirements.txt']},
|
||||
install_requires=[
|
||||
'aiohttp>=3.5.4,<4.0.0',
|
||||
'Babel>=2.6.0',
|
||||
'certifi>=2019.3.9',
|
||||
],
|
||||
extras_require={
|
||||
'proxy': [
|
||||
'aiohttp-socks>=3.3,<4.0.0',
|
||||
],
|
||||
'fast': [
|
||||
'uvloop>=0.14.0,<0.15.0',
|
||||
'ujson>=1.35',
|
||||
],
|
||||
},
|
||||
include_package_data=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,32 @@
|
|||
import pytest
|
||||
from aiogram.bot.api import check_token
|
||||
|
||||
from aiogram.bot.api import check_token
|
||||
from aiogram.utils.exceptions import ValidationError
|
||||
|
||||
|
||||
VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
|
||||
INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length
|
||||
INVALID_TOKENS = [
|
||||
'123456789:AABBCCDDEEFFaabbccddeeff 123456789', # space is exists
|
||||
'ABC:AABBCCDDEEFFaabbccddeeff123456789', # left part is not digit
|
||||
':AABBCCDDEEFFaabbccddeeff123456789', # there is no left part
|
||||
'123456789:', # there is no right part
|
||||
'ABC AABBCCDDEEFFaabbccddeeff123456789', # there is no ':' separator
|
||||
None, # is None
|
||||
12345678, # is digit
|
||||
{}, # is dict
|
||||
[], # is dict
|
||||
]
|
||||
|
||||
|
||||
class Test_check_token:
|
||||
@pytest.fixture(params=INVALID_TOKENS, name='invalid_token')
|
||||
def invalid_token_fixture(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestCheckToken:
|
||||
|
||||
def test_valid(self):
|
||||
assert check_token(VALID_TOKEN) is True
|
||||
|
||||
def test_invalid_token(self):
|
||||
def test_invalid_token(self, invalid_token):
|
||||
with pytest.raises(ValidationError):
|
||||
check_token(INVALID_TOKEN)
|
||||
check_token(invalid_token)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import re
|
||||
from typing import Match
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram.dispatcher.filters import Text
|
||||
from aiogram.dispatcher.filters import Text, CommandStart
|
||||
from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
|
||||
|
||||
# enable asyncio mode
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
def data_sample_1():
|
||||
return [
|
||||
|
|
@ -22,15 +28,16 @@ def data_sample_1():
|
|||
('EXample_string', 'not_example_string'),
|
||||
]
|
||||
|
||||
|
||||
class TestTextFilter:
|
||||
|
||||
async def _run_check(self, check, test_text):
|
||||
@staticmethod
|
||||
async def _run_check(check, test_text):
|
||||
assert await check(Message(text=test_text))
|
||||
assert await check(CallbackQuery(data=test_text))
|
||||
assert await check(InlineQuery(query=test_text))
|
||||
assert await check(Poll(question=test_text))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_prefix, test_text", data_sample_1())
|
||||
async def test_startswith(self, test_prefix, test_text, ignore_case):
|
||||
|
|
@ -49,7 +56,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_prefix_list, test_text", [
|
||||
(['not_example', ''], ''),
|
||||
|
|
@ -83,7 +89,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_postfix, test_text", data_sample_1())
|
||||
async def test_endswith(self, test_postfix, test_text, ignore_case):
|
||||
|
|
@ -102,7 +107,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_postfix_list, test_text", [
|
||||
(['', 'not_example'], ''),
|
||||
|
|
@ -133,9 +137,9 @@ class TestTextFilter:
|
|||
_test_text = test_text
|
||||
|
||||
return result is any(map(_test_text.endswith, _test_postfix_list))
|
||||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_string, test_text", [
|
||||
('', ''),
|
||||
|
|
@ -169,7 +173,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_filter_list, test_text", [
|
||||
(['a', 'ab', 'abc'], 'A'),
|
||||
|
|
@ -193,7 +196,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_filter_text, test_text", [
|
||||
('', ''),
|
||||
|
|
@ -222,7 +224,6 @@ class TestTextFilter:
|
|||
|
||||
await self._run_check(check, test_text)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('ignore_case', (True, False))
|
||||
@pytest.mark.parametrize("test_filter_list, test_text", [
|
||||
(['new_string', ''], ''),
|
||||
|
|
@ -261,3 +262,50 @@ class TestTextFilter:
|
|||
await check(CallbackQuery(data=test_text))
|
||||
await check(InlineQuery(query=test_text))
|
||||
await check(Poll(question=test_text))
|
||||
|
||||
|
||||
class TestCommandStart:
|
||||
START = '/start'
|
||||
GOOD = 'foo'
|
||||
BAD = 'bar'
|
||||
GOOD_PATTERN = re.compile(r'^f..$')
|
||||
BAD_PATTERN = re.compile(r'^b..$')
|
||||
ENCODED = 'Zm9v'
|
||||
|
||||
async def test_start_command_without_payload(self):
|
||||
test_filter = CommandStart() # empty filter
|
||||
message = Message(text=self.START)
|
||||
result = await test_filter.check(message)
|
||||
assert result == {'deep_link': None}
|
||||
|
||||
async def test_start_command_payload_is_matched(self):
|
||||
test_filter = CommandStart(deep_link=self.GOOD)
|
||||
message = Message(text=f'{self.START} {self.GOOD}')
|
||||
result = await test_filter.check(message)
|
||||
assert result == {'deep_link': self.GOOD}
|
||||
|
||||
async def test_start_command_payload_is_not_matched(self):
|
||||
test_filter = CommandStart(deep_link=self.GOOD)
|
||||
message = Message(text=f'{self.START} {self.BAD}')
|
||||
result = await test_filter.check(message)
|
||||
assert result is False
|
||||
|
||||
async def test_start_command_payload_pattern_is_matched(self):
|
||||
test_filter = CommandStart(deep_link=self.GOOD_PATTERN)
|
||||
message = Message(text=f'{self.START} {self.GOOD}')
|
||||
result = await test_filter.check(message)
|
||||
assert isinstance(result, dict)
|
||||
match = result.get('deep_link')
|
||||
assert isinstance(match, Match)
|
||||
|
||||
async def test_start_command_payload_pattern_is_not_matched(self):
|
||||
test_filter = CommandStart(deep_link=self.BAD_PATTERN)
|
||||
message = Message(text=f'{self.START} {self.GOOD}')
|
||||
result = await test_filter.check(message)
|
||||
assert result is False
|
||||
|
||||
async def test_start_command_payload_is_encoded(self):
|
||||
test_filter = CommandStart(deep_link=self.GOOD, encoded=True)
|
||||
message = Message(text=f'{self.START} {self.ENCODED}')
|
||||
result = await test_filter.check(message)
|
||||
assert result == {'deep_link': self.GOOD}
|
||||
|
|
|
|||
74
tests/test_utils/test_deep_linking.py
Normal file
74
tests/test_utils/test_deep_linking.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.utils.deep_linking import decode_payload, encode_payload, filter_payload
|
||||
from aiogram.utils.deep_linking import get_start_link, get_startgroup_link
|
||||
from tests.types import dataset
|
||||
|
||||
# enable asyncio mode
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
PAYLOADS = [
|
||||
'foo',
|
||||
'AAbbCCddEEff1122334455',
|
||||
'aaBBccDDeeFF5544332211',
|
||||
-12345678901234567890,
|
||||
12345678901234567890,
|
||||
]
|
||||
|
||||
WRONG_PAYLOADS = [
|
||||
'@BotFather',
|
||||
'spaces spaces spaces',
|
||||
1234567890123456789.0,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(params=PAYLOADS, name='payload')
|
||||
def payload_fixture(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=WRONG_PAYLOADS, name='wrong_payload')
|
||||
def wrong_payload_fixture(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def get_bot_user_fixture(monkeypatch):
|
||||
""" Monkey patching of bot.me calling. """
|
||||
from aiogram.utils import deep_linking
|
||||
|
||||
async def get_bot_user_mock():
|
||||
from aiogram.types import User
|
||||
return User(**dataset.USER)
|
||||
|
||||
monkeypatch.setattr(deep_linking, '_get_bot_user', get_bot_user_mock)
|
||||
|
||||
|
||||
class TestDeepLinking:
|
||||
async def test_get_start_link(self, payload):
|
||||
link = await get_start_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={payload}'
|
||||
|
||||
async def test_wrong_symbols(self, wrong_payload):
|
||||
with pytest.raises(ValueError):
|
||||
await get_start_link(wrong_payload)
|
||||
|
||||
async def test_get_startgroup_link(self, payload):
|
||||
link = await get_startgroup_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?startgroup={payload}'
|
||||
|
||||
async def test_filter_encode_and_decode(self, payload):
|
||||
_payload = filter_payload(payload)
|
||||
encoded = encode_payload(_payload)
|
||||
decoded = decode_payload(encoded)
|
||||
assert decoded == str(payload)
|
||||
|
||||
async def test_get_start_link_with_encoding(self, payload):
|
||||
# define link
|
||||
link = await get_start_link(payload, encode=True)
|
||||
|
||||
# define reference link
|
||||
payload = filter_payload(payload)
|
||||
encoded_payload = encode_payload(payload)
|
||||
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}'
|
||||
Loading…
Add table
Add a link
Reference in a new issue