mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x' into private-chat-links
This commit is contained in:
commit
352dc1654a
54 changed files with 1004 additions and 234 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: [aiogram site](https://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.3.dev1'
|
||||
__api_version__ = '4.4'
|
||||
__version__ = '2.5.1dev1'
|
||||
__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):
|
||||
|
|
|
|||
|
|
@ -863,6 +863,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
async def send_poll(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
question: base.String,
|
||||
options: typing.List[base.String],
|
||||
is_anonymous: typing.Optional[base.Boolean] = None,
|
||||
type: typing.Optional[base.String] = None,
|
||||
allows_multiple_answers: typing.Optional[base.Boolean] = None,
|
||||
correct_option_id: typing.Optional[base.Integer] = None,
|
||||
is_closed: typing.Optional[base.Boolean] = None,
|
||||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
|
|
@ -881,6 +886,16 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:type question: :obj:`base.String`
|
||||
:param options: List of answer options, 2-10 strings 1-100 characters each
|
||||
:param options: :obj:`typing.List[base.String]`
|
||||
:param is_anonymous: True, if the poll needs to be anonymous, defaults to True
|
||||
:param is_anonymous: :obj:`typing.Optional[base.Boolean]`
|
||||
:param type: Poll type, “quiz” or “regular”, defaults to “regular”
|
||||
:param type: :obj:`typing.Optional[base.String]`
|
||||
:param allows_multiple_answers: True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False
|
||||
:param allows_multiple_answers: :obj:`typing.Optional[base.Boolean]`
|
||||
:param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode
|
||||
:param correct_option_id: :obj:`typing.Optional[base.Integer]`
|
||||
:param is_closed: Pass True, if the poll needs to be immediately closed
|
||||
:param is_closed: :obj:`typing.Optional[base.Boolean]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Optional[Boolean]`
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
|
|
@ -1118,6 +1133,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 +1858,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
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,20 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
if timeout > 0:
|
||||
self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)")
|
||||
|
||||
async def on_pre_process_poll(self, poll, data):
|
||||
self.logger.info(f"Received poll [ID:{poll.id}]")
|
||||
|
||||
async def on_post_process_poll(self, poll, results, data):
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll [ID:{poll.id}]")
|
||||
|
||||
async def on_pre_process_poll_answer(self, poll_answer, data):
|
||||
self.logger.info(f"Received poll answer [ID:{poll_answer.poll_id}] "
|
||||
f"from user [ID:{poll_answer.user.id}]")
|
||||
|
||||
async def on_post_process_poll_answer(self, poll_answer, results, data):
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll answer [ID:{poll_answer.poll_id}] "
|
||||
f"from user [ID:{poll_answer.user.id}]")
|
||||
|
||||
|
||||
class LoggingFilter(logging.Filter):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from aiohttp.helpers import sentinel
|
|||
from aiogram.utils.deprecated import renamed_argument
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
|
||||
RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter
|
||||
from .filters.builtin import IsSenderContact
|
||||
from .handler import Handler
|
||||
from .middlewares import MiddlewareManager
|
||||
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
|
||||
|
|
@ -69,6 +70,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.shipping_query_handlers = Handler(self, middleware_key='shipping_query')
|
||||
self.pre_checkout_query_handlers = Handler(self, middleware_key='pre_checkout_query')
|
||||
self.poll_handlers = Handler(self, middleware_key='poll')
|
||||
self.poll_answer_handlers = Handler(self, middleware_key='poll_answer')
|
||||
self.errors_handlers = Handler(self, once=False, middleware_key='error')
|
||||
|
||||
self.middleware = MiddlewareManager(self)
|
||||
|
|
@ -87,6 +89,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(StateFilter, exclude_event_handlers=[
|
||||
self.errors_handlers,
|
||||
self.poll_handlers,
|
||||
self.poll_answer_handlers,
|
||||
])
|
||||
filters_factory.bind(ContentTypeFilter, event_handlers=[
|
||||
self.message_handlers,
|
||||
|
|
@ -151,6 +154,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.channel_post_handlers,
|
||||
self.edited_channel_post_handlers,
|
||||
])
|
||||
filters_factory.bind(IsSenderContact, event_handlers=[
|
||||
self.message_handlers,
|
||||
self.edited_message_handlers,
|
||||
self.channel_post_handlers,
|
||||
self.edited_channel_post_handlers,
|
||||
])
|
||||
|
||||
def __del__(self):
|
||||
self.stop_polling()
|
||||
|
|
@ -226,6 +235,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query)
|
||||
if update.poll:
|
||||
return await self.poll_handlers.notify(update.poll)
|
||||
if update.poll_answer:
|
||||
return await self.poll_answer_handlers.notify(update.poll_answer)
|
||||
except Exception as e:
|
||||
err = await self.errors_handlers.notify(update, e)
|
||||
if err:
|
||||
|
|
@ -853,18 +864,90 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
return decorator
|
||||
|
||||
def register_poll_handler(self, callback, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Register handler for poll
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
dp.register_poll_handler(some_poll_handler)
|
||||
|
||||
:param callback:
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
filters_set = self.filters_factory.resolve(self.poll_handlers,
|
||||
*custom_filters,
|
||||
**kwargs)
|
||||
self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set)
|
||||
|
||||
def poll_handler(self, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Decorator for poll handler
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@dp.poll_handler()
|
||||
async def some_poll_handler(poll: types.Poll)
|
||||
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
|
||||
def decorator(callback):
|
||||
self.register_poll_handler(callback, *custom_filters, run_task=run_task,
|
||||
**kwargs)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
def register_poll_answer_handler(self, callback, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Register handler for poll_answer
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
dp.register_poll_answer_handler(some_poll_answer_handler)
|
||||
|
||||
:param callback:
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
filters_set = self.filters_factory.resolve(self.poll_answer_handlers,
|
||||
*custom_filters,
|
||||
**kwargs)
|
||||
self.poll_answer_handlers.register(self._wrap_async_task(callback, run_task), filters_set)
|
||||
|
||||
def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs):
|
||||
"""
|
||||
Decorator for poll_answer handler
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@dp.poll_answer_handler()
|
||||
async def some_poll_answer_handler(poll: types.Poll)
|
||||
|
||||
:param custom_filters:
|
||||
:param run_task: run callback in task (no wait results)
|
||||
:param kwargs:
|
||||
"""
|
||||
|
||||
def decorator(callback):
|
||||
self.register_poll_answer_handler(callback, *custom_filters, run_task=run_task,
|
||||
**kwargs)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
|
||||
Text, IDFilter, AdminFilter, IsReplyFilter
|
||||
Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact
|
||||
from .factory import FiltersFactory
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
|
||||
check_filters, get_filter_spec, get_filters_spec
|
||||
|
|
@ -26,6 +26,7 @@ __all__ = [
|
|||
'Text',
|
||||
'IDFilter',
|
||||
'IsReplyFilter',
|
||||
'IsSenderContact',
|
||||
'AdminFilter',
|
||||
'get_filter_spec',
|
||||
'get_filters_spec',
|
||||
|
|
|
|||
|
|
@ -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,13 +166,16 @@ 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -437,6 +444,8 @@ class ContentTypeFilter(BoundFilter):
|
|||
default = types.ContentTypes.TEXT
|
||||
|
||||
def __init__(self, content_types):
|
||||
if isinstance(content_types, str):
|
||||
content_types = (content_types,)
|
||||
self.content_types = content_types
|
||||
|
||||
async def check(self, message):
|
||||
|
|
@ -444,6 +453,28 @@ class ContentTypeFilter(BoundFilter):
|
|||
message.content_type in self.content_types
|
||||
|
||||
|
||||
class IsSenderContact(BoundFilter):
|
||||
"""
|
||||
Filter check that the contact matches the sender
|
||||
|
||||
`is_sender_contact=True` - contact matches the sender
|
||||
`is_sender_contact=False` - result will be inverted
|
||||
"""
|
||||
key = 'is_sender_contact'
|
||||
|
||||
def __init__(self, is_sender_contact: bool):
|
||||
self.is_sender_contact = is_sender_contact
|
||||
|
||||
async def check(self, message: types.Message) -> bool:
|
||||
if not message.contact:
|
||||
return False
|
||||
is_sender_contact = message.contact.user_id == message.from_user.id
|
||||
if self.is_sender_contact:
|
||||
return is_sender_contact
|
||||
else:
|
||||
return not is_sender_contact
|
||||
|
||||
|
||||
class StateFilter(BoundFilter):
|
||||
"""
|
||||
Check user state
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa
|
|||
PassportElementErrorSelfie
|
||||
from .passport_file import PassportFile
|
||||
from .photo_size import PhotoSize
|
||||
from .poll import PollOption, Poll
|
||||
from .poll import PollOption, Poll, PollAnswer, PollType
|
||||
from .pre_checkout_query import PreCheckoutQuery
|
||||
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
|
||||
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType
|
||||
from .response_parameters import ResponseParameters
|
||||
from .shipping_address import ShippingAddress
|
||||
from .shipping_option import ShippingOption
|
||||
|
|
@ -126,6 +126,7 @@ __all__ = (
|
|||
'InputVenueMessageContent',
|
||||
'Invoice',
|
||||
'KeyboardButton',
|
||||
'KeyboardButtonPollType',
|
||||
'LabeledPrice',
|
||||
'Location',
|
||||
'LoginUrl',
|
||||
|
|
@ -147,7 +148,9 @@ __all__ = (
|
|||
'PassportFile',
|
||||
'PhotoSize',
|
||||
'Poll',
|
||||
'PollAnswer',
|
||||
'PollOption',
|
||||
'PollType',
|
||||
'PreCheckoutQuery',
|
||||
'ReplyKeyboardMarkup',
|
||||
'ReplyKeyboardRemove',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
return getattr(self, ALIASES_ATTR_NAME, {})
|
||||
|
||||
@property
|
||||
def values(self) -> typing.Tuple[str]:
|
||||
def values(self) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Get values
|
||||
|
||||
|
|
@ -161,9 +161,10 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
|
||||
:return:
|
||||
"""
|
||||
self.clean()
|
||||
result = {}
|
||||
for name, value in self.values.items():
|
||||
if value is None:
|
||||
continue
|
||||
if name in self.props:
|
||||
value = self.props[name].export(self)
|
||||
if isinstance(value, TelegramObject):
|
||||
|
|
@ -191,7 +192,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
return json.dumps(self.to_python())
|
||||
|
||||
@classmethod
|
||||
def create(cls: Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
|
||||
def create(cls: typing.Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
|
||||
raise NotImplemented
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -225,15 +226,15 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
return self.props[key].set_value(self, value, self.conf.get('parent', None))
|
||||
raise KeyError(key)
|
||||
|
||||
def __contains__(self, item: typing.Dict[str, typing.Any]) -> bool:
|
||||
def __contains__(self, item: str) -> bool:
|
||||
"""
|
||||
Check key contains in that object
|
||||
|
||||
:param item:
|
||||
:return:
|
||||
"""
|
||||
self.clean()
|
||||
return item in self.values
|
||||
# self.clean()
|
||||
return bool(self.values.get(item, None))
|
||||
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
"""
|
||||
|
|
@ -263,7 +264,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
yield value
|
||||
|
||||
def __hash__(self) -> int:
|
||||
def _hash(obj)-> int:
|
||||
def _hash(obj) -> int:
|
||||
buf: int = 0
|
||||
if isinstance(obj, list):
|
||||
for item in obj:
|
||||
|
|
|
|||
|
|
@ -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,7 +58,7 @@ 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!')
|
||||
|
||||
|
|
@ -72,17 +73,17 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
if self.type == ChatType.PRIVATE:
|
||||
raise TypeError('`shifted_id` property is not available for private chats')
|
||||
shift = -1000000000000
|
||||
shift = -1_000_000_000_000
|
||||
return shift - self.id
|
||||
|
||||
def get_mention(self, name=None, as_html=True):
|
||||
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.
|
||||
|
|
@ -113,7 +114,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.
|
||||
|
|
@ -130,7 +131,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.
|
||||
|
|
@ -145,7 +146,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.
|
||||
|
|
@ -162,7 +163,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.
|
||||
|
|
@ -177,7 +178,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
|
||||
|
|
@ -200,7 +201,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.
|
||||
|
|
@ -308,7 +309,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.
|
||||
|
|
@ -325,7 +353,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.
|
||||
|
|
@ -337,7 +365,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.
|
||||
|
||||
|
|
@ -348,7 +376,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.
|
||||
|
||||
|
|
@ -362,7 +390,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.
|
||||
|
||||
|
|
@ -373,7 +401,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.
|
||||
|
||||
|
|
@ -386,7 +414,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
|
||||
|
|
@ -404,7 +464,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()
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class MediaGroup(base.TelegramObject):
|
|||
|
||||
:return:
|
||||
"""
|
||||
self.clean()
|
||||
# self.clean()
|
||||
result = []
|
||||
for obj in self.media:
|
||||
if isinstance(obj, base.TelegramObject):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -1805,4 +1777,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):
|
||||
|
|
@ -17,6 +18,7 @@ class MessageEntity(base.TelegramObject):
|
|||
length: base.Integer = fields.Field()
|
||||
url: base.String = fields.Field()
|
||||
user: User = fields.Field(base=User)
|
||||
language: base.String = fields.Field()
|
||||
|
||||
def get_text(self, text):
|
||||
"""
|
||||
|
|
@ -36,6 +38,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 +90,8 @@ class MessageEntityType(helper.Helper):
|
|||
:key: ITALIC
|
||||
:key: CODE
|
||||
:key: PRE
|
||||
:key: UNDERLINE
|
||||
:key: STRIKETHROUGH
|
||||
:key: TEXT_LINK
|
||||
:key: TEXT_MENTION
|
||||
"""
|
||||
|
|
@ -101,7 +106,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()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,53 @@
|
|||
import typing
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from ..utils import helper
|
||||
from . import base, fields
|
||||
from .user import User
|
||||
|
||||
|
||||
class PollOption(base.TelegramObject):
|
||||
"""
|
||||
This object contains information about one answer option in a poll.
|
||||
|
||||
https://core.telegram.org/bots/api#polloption
|
||||
"""
|
||||
|
||||
text: base.String = fields.Field()
|
||||
voter_count: base.Integer = fields.Field()
|
||||
|
||||
|
||||
class PollAnswer(base.TelegramObject):
|
||||
"""
|
||||
This object represents an answer of a user in a non-anonymous poll.
|
||||
|
||||
https://core.telegram.org/bots/api#pollanswer
|
||||
"""
|
||||
|
||||
poll_id: base.String = fields.Field()
|
||||
user: User = fields.Field(base=User)
|
||||
option_ids: typing.List[base.Integer] = fields.ListField()
|
||||
|
||||
|
||||
class Poll(base.TelegramObject):
|
||||
"""
|
||||
This object contains information about a poll.
|
||||
|
||||
https://core.telegram.org/bots/api#poll
|
||||
"""
|
||||
|
||||
id: base.String = fields.Field()
|
||||
question: base.String = fields.Field()
|
||||
options: typing.List[PollOption] = fields.ListField(base=PollOption)
|
||||
total_voter_count: base.Integer = fields.Field()
|
||||
is_closed: base.Boolean = fields.Field()
|
||||
is_anonymous: base.Boolean = fields.Field()
|
||||
type: base.String = fields.Field()
|
||||
allows_multiple_answers: base.Boolean = fields.Field()
|
||||
correct_option_id: base.Integer = fields.Field()
|
||||
|
||||
|
||||
class PollType(helper.Helper):
|
||||
mode = helper.HelperMode.snake_case
|
||||
|
||||
REGULAR = helper.Item()
|
||||
QUIZ = helper.Item()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,18 @@ from . import base
|
|||
from . import fields
|
||||
|
||||
|
||||
class KeyboardButtonPollType(base.TelegramObject):
|
||||
"""
|
||||
This object represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed.
|
||||
|
||||
https://core.telegram.org/bots/api#keyboardbuttonpolltype
|
||||
"""
|
||||
type: base.String = fields.Field()
|
||||
|
||||
def __init__(self, type: typing.Optional[base.String] = None):
|
||||
super(KeyboardButtonPollType, self).__init__(type=type)
|
||||
|
||||
|
||||
class ReplyKeyboardMarkup(base.TelegramObject):
|
||||
"""
|
||||
This object represents a custom keyboard with reply options (see Introduction to bots for details and examples).
|
||||
|
|
@ -81,21 +93,29 @@ class ReplyKeyboardMarkup(base.TelegramObject):
|
|||
|
||||
class KeyboardButton(base.TelegramObject):
|
||||
"""
|
||||
This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields are mutually exclusive.
|
||||
Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
|
||||
This object represents one button of the reply keyboard.
|
||||
For simple text buttons String can be used instead of this object to specify text of the button.
|
||||
Optional fields request_contact, request_location, and request_poll are mutually exclusive.
|
||||
Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016.
|
||||
Older clients will ignore them.
|
||||
Note: request_poll option will only work in Telegram versions released after 23 January, 2020.
|
||||
Older clients will receive unsupported message.
|
||||
|
||||
https://core.telegram.org/bots/api#keyboardbutton
|
||||
"""
|
||||
text: base.String = fields.Field()
|
||||
request_contact: base.Boolean = fields.Field()
|
||||
request_location: base.Boolean = fields.Field()
|
||||
request_poll: KeyboardButtonPollType = fields.Field()
|
||||
|
||||
def __init__(self, text: base.String,
|
||||
request_contact: base.Boolean = None,
|
||||
request_location: base.Boolean = None):
|
||||
request_location: base.Boolean = None,
|
||||
request_poll: KeyboardButtonPollType = None):
|
||||
super(KeyboardButton, self).__init__(text=text,
|
||||
request_contact=request_contact,
|
||||
request_location=request_location)
|
||||
request_location=request_location,
|
||||
request_poll=request_poll)
|
||||
|
||||
|
||||
class ReplyKeyboardRemove(base.TelegramObject):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from .callback_query import CallbackQuery
|
|||
from .chosen_inline_result import ChosenInlineResult
|
||||
from .inline_query import InlineQuery
|
||||
from .message import Message
|
||||
from .poll import Poll
|
||||
from .poll import Poll, PollAnswer
|
||||
from .pre_checkout_query import PreCheckoutQuery
|
||||
from .shipping_query import ShippingQuery
|
||||
from ..utils import helper
|
||||
|
|
@ -30,6 +30,7 @@ class Update(base.TelegramObject):
|
|||
shipping_query: ShippingQuery = fields.Field(base=ShippingQuery)
|
||||
pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery)
|
||||
poll: Poll = fields.Field(base=Poll)
|
||||
poll_answer: PollAnswer = fields.Field(base=PollAnswer)
|
||||
|
||||
def __hash__(self):
|
||||
return self.update_id
|
||||
|
|
@ -58,3 +59,5 @@ class AllowedUpdates(helper.Helper):
|
|||
CALLBACK_QUERY = helper.ListItem() # callback_query
|
||||
SHIPPING_QUERY = helper.ListItem() # shipping_query
|
||||
PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query
|
||||
POLL = helper.ListItem() # poll
|
||||
POLL_ANSWER = helper.ListItem() # poll_answer
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -21,6 +22,9 @@ class User(base.TelegramObject):
|
|||
last_name: base.String = fields.Field()
|
||||
username: base.String = fields.Field()
|
||||
language_code: base.String = fields.Field()
|
||||
can_join_groups: base.Boolean = fields.Field()
|
||||
can_read_all_group_messages: base.Boolean = fields.Field()
|
||||
supports_inline_queries: base.Boolean = fields.Field()
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
|
|
@ -73,9 +77,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()
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class CallbackData:
|
|||
raise TypeError('Too many arguments were passed!')
|
||||
|
||||
callback_data = self.sep.join(data)
|
||||
if len(callback_data) > 64:
|
||||
if len(callback_data.encode()) > 64:
|
||||
raise ValueError('Resulted callback data is too long!')
|
||||
|
||||
return callback_data
|
||||
|
|
|
|||
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
|
||||
|
|
@ -102,14 +102,18 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
|
|||
is_coroutine = asyncio.iscoroutinefunction(func)
|
||||
|
||||
def _handling(kwargs):
|
||||
"""
|
||||
Returns updated version of 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 = kwargs.copy()
|
||||
kwargs.update({new_name: kwargs.pop(old_name)})
|
||||
return kwargs
|
||||
return kwargs
|
||||
|
||||
if is_coroutine:
|
||||
@functools.wraps(func)
|
||||
|
|
|
|||
|
|
@ -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,42 +1,24 @@
|
|||
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=' '):
|
||||
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):
|
||||
def quote_html(*content, sep=" "):
|
||||
"""
|
||||
Quote HTML symbols
|
||||
|
||||
|
|
@ -44,16 +26,31 @@ def quote_html(content):
|
|||
an HTML entity must be replaced with the corresponding HTML entities
|
||||
(< with < > with > & with & and " with ").
|
||||
|
||||
:param content: str
|
||||
:return: str
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
new_content = ''
|
||||
for symbol in content:
|
||||
new_content += HTML_QUOTES_MAP[symbol] if symbol in _HQS else symbol
|
||||
return new_content
|
||||
return html_decoration.quote(_join(*content, sep=sep))
|
||||
|
||||
|
||||
def text(*content, sep=' '):
|
||||
def escape_md(*content, sep=" "):
|
||||
"""
|
||||
Escape markdown text
|
||||
|
||||
E.g. for usernames
|
||||
|
||||
:param content:
|
||||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return markdown_decoration.quote(_join(*content, sep=sep))
|
||||
|
||||
|
||||
def _join(*content, sep=" "):
|
||||
return sep.join(map(str, content))
|
||||
|
||||
|
||||
def text(*content, sep=" "):
|
||||
"""
|
||||
Join all elements with a separator
|
||||
|
||||
|
|
@ -64,7 +61,7 @@ def text(*content, sep=' '):
|
|||
return _join(*content, sep=sep)
|
||||
|
||||
|
||||
def bold(*content, sep=' '):
|
||||
def bold(*content, sep=" "):
|
||||
"""
|
||||
Make bold text (Markdown)
|
||||
|
||||
|
|
@ -72,10 +69,12 @@ 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 +82,12 @@ 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 +95,12 @@ 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 +108,12 @@ 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 +121,12 @@ 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 +134,12 @@ 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 +147,12 @@ 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 +160,64 @@ 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 +225,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 +236,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.4
|
||||
rethinkdb>=2.4.1
|
||||
coverage==4.5.3
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@ ContentTypeFilter
|
|||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
IsSenderContact
|
||||
---------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
StateFilter
|
||||
-----------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -84,7 +83,6 @@ def get_likes() -> int:
|
|||
def increase_likes() -> int:
|
||||
LIKES_STORAGE['count'] += 1
|
||||
return get_likes()
|
||||
#
|
||||
|
||||
|
||||
@dp.message_handler(commands='like')
|
||||
|
|
|
|||
|
|
@ -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>=0.3.4,<0.4.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
|
||||
|
||||
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