Merge pull request #1 from aiogram/dev-2.x

update original
This commit is contained in:
unintended 2020-06-20 13:24:23 +03:00 committed by GitHub
commit c874fa8e35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 4606 additions and 1455 deletions

9
.github/FUNDING.yml vendored
View file

@ -1,8 +1 @@
# These are supported funding model platforms
github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: aiogram # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL
open_collective: aiogram

View file

@ -8,7 +8,7 @@ Fixes # (issue)
Please delete options that are not relevant.
- [ ] Documentstion (typos, code examples or any documentation update)
- [ ] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

3
.gitignore vendored
View file

@ -60,3 +60,6 @@ docs/html
# i18n/l10n
*.mo
# pynev
.python-version

View file

@ -1,18 +1,19 @@
# AIOGram
[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?style=flat-square)](https://opencollective.com/aiogram)
[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live)
[![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT)
**aiogram** is a pretty simple and fully asynchronous library 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.
**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:
@ -20,7 +21,37 @@ 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)
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)].
<a href="https://github.com/aiogram/aiogram/graphs/contributors"><img src="https://opencollective.com/aiogram/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)]
#### Individuals
<a href="https://opencollective.com/aiogram"><img src="https://opencollective.com/aiogram/individuals.svg?width=890"></a>
#### Organizations
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.png"></a>
<a href="https://opencollective.com/aiogram/organization/1/website"><img src="https://opencollective.com/aiogram/organization/1/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/2/website"><img src="https://opencollective.com/aiogram/organization/2/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/3/website"><img src="https://opencollective.com/aiogram/organization/3/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/4/website"><img src="https://opencollective.com/aiogram/organization/4/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/5/website"><img src="https://opencollective.com/aiogram/organization/5/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/6/website"><img src="https://opencollective.com/aiogram/organization/6/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/7/website"><img src="https://opencollective.com/aiogram/organization/7/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/8/website"><img src="https://opencollective.com/aiogram/organization/8/avatar.png"></a>
<a href="https://opencollective.com/aiogram/organization/9/website"><img src="https://opencollective.com/aiogram/organization/9/avatar.png"></a>

View file

@ -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.3-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
.. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
: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
@ -38,9 +38,9 @@ AIOGramBot
:alt: MIT License
**aiogram** is a pretty simple and fully asynchronous library 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.
**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>`_

View file

@ -1,3 +1,8 @@
import sys
if sys.version_info < (3, 7):
raise ImportError('Your Python version {0} is not supported by aiogram, please install '
'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3]))))
import asyncio
import os
@ -38,5 +43,5 @@ __all__ = [
'utils'
]
__version__ = '2.2.dev1'
__api_version__ = '4.3'
__version__ = '2.9.1'
__api_version__ = '4.9'

View file

@ -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.3
List is updated to Bot API 4.9
"""
mode = HelperMode.lowerCamelCase
@ -175,6 +181,7 @@ class Methods(Helper):
SEND_VENUE = Item() # sendVenue
SEND_CONTACT = Item() # sendContact
SEND_POLL = Item() # sendPoll
SEND_DICE = Item() # sendDice
SEND_CHAT_ACTION = Item() # sendChatAction
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
GET_FILE = Item() # getFile
@ -182,6 +189,8 @@ 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
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
@ -197,6 +206,8 @@ class Methods(Helper):
SET_CHAT_STICKER_SET = Item() # setChatStickerSet
DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
SET_MY_COMMANDS = Item() # setMyCommands
GET_MY_COMMANDS = Item() # getMyCommands
# Updating messages
EDIT_MESSAGE_TEXT = Item() # editMessageText
@ -214,6 +225,7 @@ class Methods(Helper):
ADD_STICKER_TO_SET = Item() # addStickerToSet
SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet
DELETE_STICKER_FROM_SET = Item() # deleteStickerFromSet
SET_STICKER_SET_THUMB = Item() # setStickerSetThumb
# Inline mode
ANSWER_INLINE_QUERY = Item() # answerInlineQuery

View file

@ -3,8 +3,9 @@ import contextlib
import io
import ssl
import typing
import warnings
from contextvars import ContextVar
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional, Union, Type
import aiohttp
import certifi
@ -13,7 +14,7 @@ from aiohttp.helpers import sentinel
from . import api
from ..types import ParseMode, base
from ..utils import json
from ..utils.auth_widget import check_token
from ..utils.auth_widget import check_integrity
class BaseBot:
@ -21,6 +22,7 @@ class BaseBot:
Base class for bot. It's raw bot.
"""
_ctx_timeout = ContextVar('TelegramRequestTimeout')
_ctx_token = ContextVar('BotDifferentToken')
def __init__(
self,
@ -57,7 +59,9 @@ class BaseBot:
# Authentication
if validate_token:
api.check_token(token)
self._token = None
self.__token = token
self.id = int(token.split(sep=':')[0])
self.proxy = proxy
self.proxy_auth = proxy_auth
@ -70,33 +74,49 @@ class BaseBot:
# aiohttp main session
ssl_context = ssl.create_default_context(cafile=certifi.where())
self._session: Optional[aiohttp.ClientSession] = None
self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector
self._connector_init = dict(
limit=connections_limit, ssl=ssl_context, loop=self.loop
)
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
if not password:
password = proxy_auth.password
connector = SocksConnector(socks_ver=socks_ver, host=host, port=port,
username=username, password=password,
limit=connections_limit, ssl_context=ssl_context,
rdns=True, loop=self.loop)
self._connector_class = SocksConnector
self._connector_init.update(
socks_ver=socks_ver, host=host, port=port,
username=username, password=password, rdns=True,
)
self.proxy = None
self.proxy_auth = None
else:
connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop)
self._timeout = None
self.timeout = timeout
self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps)
self.parse_mode = parse_mode
def get_new_session(self) -> aiohttp.ClientSession:
return aiohttp.ClientSession(
connector=self._connector_class(**self._connector_init),
loop=self.loop,
json_serialize=json.dumps
)
@property
def session(self) -> Optional[aiohttp.ClientSession]:
if self._session is None or self._session.closed:
self._session = self.get_new_session()
return self._session
@staticmethod
def _prepare_timeout(
value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]
@ -136,6 +156,24 @@ class BaseBot:
finally:
self._ctx_timeout.reset(token)
@property
def __token(self):
return self._ctx_token.get(self._token)
@__token.setter
def __token(self, value):
self._token = value
@contextlib.contextmanager
def with_token(self, bot_token: base.String, validate_token: Optional[base.Boolean] = True):
if validate_token:
api.check_token(bot_token)
token = self._ctx_token.set(bot_token)
try:
yield
finally:
self._ctx_token.reset(token)
async def close(self):
"""
Close all client sessions
@ -240,10 +278,14 @@ 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):
self.parse_mode = None
def check_auth_widget(self, data):
return check_token(data, self.__token)
return check_integrity(self.__token, data)

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import datetime
import typing
import warnings
from .base import BaseBot, api
from .. import types
@ -208,6 +210,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.parse_mode:
@ -337,12 +340,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`types.Message`
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=['audio'])
payload = generate_payload(**locals(), exclude=['audio', 'thumb'])
if self.parse_mode:
payload.setdefault('parse_mode', self.parse_mode)
files = {}
prepare_file(payload, files, 'audio', audio)
prepare_attachment(payload, files, 'thumb', thumb)
result = await self.request(api.Methods.SEND_AUDIO, payload, files)
return types.Message(**result)
@ -519,6 +523,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=["animation", "thumb"])
if self.parse_mode:
payload.setdefault('parse_mode', self.parse_mode)
files = {}
prepare_file(payload, files, 'animation', animation)
@ -860,8 +866,18 @@ 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],
disable_notification: typing.Optional[base.Boolean],
reply_to_message_id: typing.Union[base.Integer, None],
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,
explanation: typing.Optional[base.String] = None,
explanation_parse_mode: typing.Optional[base.String] = None,
open_period: typing.Union[base.Integer, None] = None,
close_date: typing.Union[
base.Integer, datetime.datetime, datetime.timedelta, None] = None,
is_closed: typing.Optional[base.Boolean] = None,
disable_notification: typing.Optional[base.Boolean] = None,
reply_to_message_id: typing.Optional[base.Integer] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
@ -877,7 +893,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:param question: Poll question, 1-255 characters
: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]`
:type options: :obj:`typing.List[base.String]`
:param is_anonymous: True, if the poll needs to be anonymous, defaults to True
:type is_anonymous: :obj:`typing.Optional[base.Boolean]`
:param type: Poll type, quiz or regular, defaults to regular
:type 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
:type 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
:type correct_option_id: :obj:`typing.Optional[base.Integer]`
:param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing
:type explanation: :obj:`typing.Optional[base.String]`
:param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details.
:type explanation_parse_mode: :obj:`typing.Optional[base.String]`
:param open_period: Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date.
:type open_period: :obj:`typing.Union[base.Integer, None]`
:param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period.
:type close_date: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]`
:param is_closed: Pass True, if the poll needs to be immediately closed
:type 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
@ -890,11 +924,53 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`types.Message`
"""
options = prepare_arg(options)
open_period = prepare_arg(open_period)
close_date = prepare_arg(close_date)
payload = generate_payload(**locals())
if self.parse_mode:
payload.setdefault('explanation_parse_mode', self.parse_mode)
result = await self.request(api.Methods.SEND_POLL, payload)
return types.Message(**result)
async def send_dice(self, chat_id: typing.Union[base.Integer, base.String],
disable_notification: typing.Union[base.Boolean, None] = None,
emoji: typing.Union[base.String, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
types.ForceReply, None] = None) -> types.Message:
"""
Use this method to send a dice, which will have a random value from 1 to 6.
On success, the sent Message is returned.
(Yes, we're aware of the “proper” singular of die.
But it's awkward, and we decided to help it change. One dice at a time!)
Source: https://core.telegram.org/bots/api#senddice
:param chat_id: Unique identifier for the target chat or username of the target channel
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param emoji: Emoji on which the dice throw animation is based. Currently, must be one of 🎲 or 🎯. Defauts to 🎲
:type emoji: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_to_message_id: If the message is a reply, ID of the original message
:type reply_to_message_id: :obj:`typing.Union[base.Integer, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
result = await self.request(api.Methods.SEND_DICE, payload)
return types.Message(**result)
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
action: base.String) -> base.Boolean:
"""
@ -961,7 +1037,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return types.File(**result)
async def kick_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer,
until_date: typing.Union[base.Integer, None] = None) -> base.Boolean:
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
@ -1014,7 +1091,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
user_id: base.Integer,
until_date: typing.Union[base.Integer, None] = None,
permissions: typing.Optional[types.ChatPermissions] = None,
# permissions argument need to be required after removing other `can_*` arguments
until_date: typing.Union[
base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
@ -1030,6 +1110,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
:param permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time
:type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@ -1047,8 +1129,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
until_date = prepare_arg(until_date)
permissions = prepare_arg(permissions)
payload = generate_payload(**locals())
for permission in ['can_send_messages',
'can_send_media_messages',
'can_send_other_messages',
'can_add_web_page_previews']:
if permission in payload:
warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions "
f"in a single argument of the type ChatPermissions instead of "
f"passing regular argument {payload[permission]}",
DeprecationWarning, stacklevel=2)
result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload)
return result
@ -1099,6 +1192,44 @@ 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:
"""
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 chat_id: Unique identifier for the target chat or username of the target supergroup
:param permissions: New default chat permissions
:return: True on success.
"""
permissions = prepare_arg(permissions)
payload = generate_payload(**locals())
result = await self.request(api.Methods.SET_CHAT_PERMISSIONS, payload)
return result
async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String:
"""
Use this method to generate a new invite link for a chat; any previously generated link is revoked.
@ -1411,6 +1542,37 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload)
return result
async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean:
"""
Use this method to change the list of the bot's commands.
Source: https://core.telegram.org/bots/api#setmycommands
:param commands: A JSON-serialized list of bot commands to be set as the list of the bot's commands.
At most 100 commands can be specified.
:type commands: :obj: `typing.List[types.BotCommand]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
commands = prepare_arg(commands)
payload = generate_payload(**locals())
result = await self.request(api.Methods.SET_MY_COMMANDS, payload)
return result
async def get_my_commands(self) -> typing.List[types.BotCommand]:
"""
Use this method to get the current list of the bot's commands.
Source: https://core.telegram.org/bots/api#getmycommands
:return: Returns Array of BotCommand on success.
:rtype: :obj:`typing.List[types.BotCommand]`
"""
payload = generate_payload(**locals())
result = await self.request(api.Methods.GET_MY_COMMANDS, payload)
return [types.BotCommand(**bot_command_data) for bot_command_data in result]
async def edit_message_text(self, text: base.String,
chat_id: typing.Union[base.Integer, base.String, None] = None,
message_id: typing.Union[base.Integer, None] = None,
@ -1697,24 +1859,40 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files)
return types.File(**result)
async def create_new_sticker_set(self, user_id: base.Integer, name: base.String, title: base.String,
png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String,
async def create_new_sticker_set(self,
user_id: base.Integer,
name: base.String,
title: base.String,
emojis: base.String,
png_sticker: typing.Union[base.InputFile, base.String] = None,
tgs_sticker: base.InputFile = None,
contains_masks: typing.Union[base.Boolean, None] = None,
mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean:
"""
Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set.
Use this method to create a new sticker set owned by a user.
The bot will be able to edit the sticker set thus created.
You must use exactly one of the fields png_sticker or tgs_sticker.
Source: https://core.telegram.org/bots/api#createnewstickerset
:param user_id: User identifier of created sticker set owner
:type user_id: :obj:`base.Integer`
:param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals)
:param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals).
Can contain only english letters, digits and underscores.
Must begin with a letter, can't contain consecutive underscores and must end in “_by_<bot username>”.
<bot_username> is case insensitive. 1-64 characters.
:type name: :obj:`base.String`
:param title: Sticker set title, 1-64 characters
:type title: :obj:`base.String`
:param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size,
:param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size,
dimensions must not exceed 512px, and either width or height must be exactly 512px.
Pass a file_id as a String to send a file that already exists on the Telegram servers,
pass an HTTP URL as a String for Telegram to get a file from the Internet, or
upload a new one using multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files
:type png_sticker: :obj:`typing.Union[base.InputFile, base.String]`
:param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data.
See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements
:type tgs_sticker: :obj:`base.InputFile`
:param emojis: One or more emoji corresponding to the sticker
:type emojis: :obj:`base.String`
:param contains_masks: Pass True, if a set of mask stickers should be created
@ -1725,19 +1903,28 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
mask_position = prepare_arg(mask_position)
payload = generate_payload(**locals(), exclude=['png_sticker'])
payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker'])
files = {}
prepare_file(payload, files, 'png_sticker', png_sticker)
prepare_file(payload, files, 'tgs_sticker', tgs_sticker)
result = await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files)
return result
async def add_sticker_to_set(self, user_id: base.Integer, name: base.String,
png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String,
async def add_sticker_to_set(self,
user_id: base.Integer,
name: base.String,
emojis: base.String,
png_sticker: typing.Union[base.InputFile, base.String] = None,
tgs_sticker: base.InputFile = None,
mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean:
"""
Use this method to add a new sticker to a set created by the bot.
You must use exactly one of the fields png_sticker or tgs_sticker.
Animated stickers can be added to animated sticker sets and only to them.
Animated sticker sets can have up to 50 stickers.
Static sticker sets can have up to 120 stickers.
Source: https://core.telegram.org/bots/api#addstickertoset
@ -1745,9 +1932,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type user_id: :obj:`base.Integer`
:param name: Sticker set name
:type name: :obj:`base.String`
:param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size,
:param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size,
dimensions must not exceed 512px, and either width or height must be exactly 512px.
Pass a file_id as a String to send a file that already exists on the Telegram servers,
pass an HTTP URL as a String for Telegram to get a file from the Internet, or
upload a new one using multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files
:type png_sticker: :obj:`typing.Union[base.InputFile, base.String]`
:param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data.
See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements
:type tgs_sticker: :obj:`base.InputFile`
:param emojis: One or more emoji corresponding to the sticker
:type emojis: :obj:`base.String`
:param mask_position: A JSON-serialized object for position where the mask should be placed on faces
@ -1756,10 +1949,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
mask_position = prepare_arg(mask_position)
payload = generate_payload(**locals(), exclude=['png_sticker'])
payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker'])
files = {}
prepare_file(payload, files, 'png_sticker', png_sticker)
prepare_file(payload, files, 'tgs_sticker', png_sticker)
result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files)
return result
@ -1786,8 +1980,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
@ -1800,6 +1992,39 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload)
return result
async def set_sticker_set_thumb(self,
name: base.String,
user_id: base.Integer,
thumb: typing.Union[base.InputFile, base.String] = None) -> base.Boolean:
"""
Use this method to set the thumbnail of a sticker set.
Animated thumbnails can be set for animated sticker sets only.
Source: https://core.telegram.org/bots/api#setstickersetthumb
:param name: Sticker set name
:type name: :obj:`base.String`
:param user_id: User identifier of the sticker set owner
:type user_id: :obj:`base.Integer`
:param thumb: A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and height
exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size;
see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical
requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers,
pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using
multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files.
Animated sticker set thumbnail can't be uploaded via HTTP URL.
:type thumb: :obj:`typing.Union[base.InputFile, base.String]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
payload = generate_payload(**locals(), exclude=['thumb'])
files = {}
prepare_file(payload, files, 'thumb', thumb)
result = await self.request(api.Methods.SET_STICKER_SET_THUMB, payload, files)
return result
async def answer_inline_query(self, inline_query_id: base.String,
results: typing.List[types.InlineQueryResult],
cache_time: typing.Union[base.Integer, None] = None,
@ -1860,6 +2085,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
need_phone_number: typing.Union[base.Boolean, None] = None,
need_email: typing.Union[base.Boolean, None] = None,
need_shipping_address: typing.Union[base.Boolean, None] = None,
send_phone_number_to_provider: typing.Union[base.Boolean, None] = None,
send_email_to_provider: typing.Union[base.Boolean, None] = None,
is_flexible: typing.Union[base.Boolean, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
@ -1906,6 +2133,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type need_email: :obj:`typing.Union[base.Boolean, None]`
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
:type need_shipping_address: :obj:`typing.Union[base.Boolean, None]`
:param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
:type send_phone_number_to_provider: :obj:`typing.Union[base.Boolean, None]`
:param send_email_to_provider: Pass True, if user's email address should be sent to provider
:type send_email_to_provider: :obj:`typing.Union[base.Boolean, None]`
:param is_flexible: Pass True, if the final price depends on the shipping method
:type is_flexible: :obj:`typing.Union[base.Boolean, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound

View file

@ -20,7 +20,8 @@ class _FileStorage(MemoryStorage):
pass
async def close(self):
self.write(self.path)
if self.data:
self.write(self.path)
await super(_FileStorage, self).close()
def read(self, path: pathlib.Path):

View file

@ -0,0 +1,200 @@
"""
This module has mongo storage for finite-state machine
based on `aiomongo <https://github.com/ZeoAlliance/aiomongo`_ driver
"""
from typing import Union, Dict, Optional, List, Tuple, AnyStr
import aiomongo
from aiomongo import AioMongoClient, Database
from ...dispatcher.storage import BaseStorage
STATE = 'aiogram_state'
DATA = 'aiogram_data'
BUCKET = 'aiogram_bucket'
COLLECTIONS = (STATE, DATA, BUCKET)
class MongoStorage(BaseStorage):
"""
Mongo-based storage for FSM.
Usage:
.. code-block:: python3
storage = MongoStorage(host='localhost', port=27017, db_name='aiogram_fsm')
dp = Dispatcher(bot, storage=storage)
And need to close Mongo client connections when shutdown
.. code-block:: python3
await dp.storage.close()
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm',
username=None, password=None, index=True, **kwargs):
self._host = host
self._port = port
self._db_name: str = db_name
self._username = username
self._password = password
self._kwargs = kwargs
self._mongo: Union[AioMongoClient, None] = None
self._db: Union[Database, None] = None
self._index = index
async def get_client(self) -> AioMongoClient:
if isinstance(self._mongo, AioMongoClient):
return self._mongo
uri = 'mongodb://'
# set username + password
if self._username and self._password:
uri += f'{self._username}:{self._password}@'
# set host and port (optional)
uri += f'{self._host}:{self._port}' if self._host else f'localhost:{self._port}'
# define and return client
self._mongo = await aiomongo.create_client(uri)
return self._mongo
async def get_db(self) -> Database:
"""
Get Mongo db
This property is awaitable.
"""
if isinstance(self._db, Database):
return self._db
mongo = await self.get_client()
self._db = mongo.get_database(self._db_name)
if self._index:
await self.apply_index(self._db)
return self._db
@staticmethod
async def apply_index(db):
for collection in COLLECTIONS:
await db[collection].create_index(keys=[('chat', 1), ('user', 1)],
name="chat_user_idx", unique=True, background=True)
async def close(self):
if self._mongo:
self._mongo.close()
async def wait_closed(self):
if self._mongo:
return await self._mongo.wait_closed()
return True
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
state: Optional[AnyStr] = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
if state is None:
await db[STATE].delete_one(filter={'chat': chat, 'user': user})
else:
await db[STATE].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'state': state}}, upsert=True)
async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[str] = None) -> Optional[str]:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
return result.get('state') if result else default
async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
data: Dict = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
await db[DATA].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'data': data}}, upsert=True)
async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[dict] = None) -> Dict:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[DATA].find_one(filter={'chat': chat, 'user': user})
return result.get('data') if result else default or {}
async def update_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
data: Dict = None, **kwargs):
if data is None:
data = {}
temp_data = await self.get_data(chat=chat, user=user, default={})
temp_data.update(data, **kwargs)
await self.set_data(chat=chat, user=user, data=temp_data)
def has_bucket(self):
return True
async def get_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[dict] = None) -> Dict:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[BUCKET].find_one(filter={'chat': chat, 'user': user})
return result.get('bucket') if result else default or {}
async def set_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
bucket: Dict = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
await db[BUCKET].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'bucket': bucket}}, upsert=True)
async def update_bucket(self, *, chat: Union[str, int, None] = None,
user: Union[str, int, None] = None,
bucket: Dict = None, **kwargs):
if bucket is None:
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
Reset states in DB
:param full: clean DB or clean only states
:return:
"""
db = await self.get_db()
await db[STATE].drop()
if full:
await db[DATA].drop()
await db[BUCKET].drop()
async def get_states_list(self) -> List[Tuple[int, int]]:
"""
Get list of all stored chat's and user's
:return: list of tuples where first element is chat id and second is user id
"""
db = await self.get_db()
result = []
items = await db[STATE].find().to_list()
for item in items:
result.append(
(int(item['chat']), int(item['user']))
)
return result

View file

@ -35,7 +35,6 @@ class RedisStorage(BaseStorage):
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
self._host = host
self._port = port
@ -45,29 +44,27 @@ class RedisStorage(BaseStorage):
self._loop = loop or asyncio.get_event_loop()
self._kwargs = kwargs
self._redis: aioredis.RedisConnection = None
self._redis: typing.Optional[aioredis.RedisConnection] = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def close(self):
if self._redis and not self._redis.closed:
self._redis.close()
del self._redis
self._redis = None
async with self._connection_lock:
if self._redis and not self._redis.closed:
self._redis.close()
async def wait_closed(self):
if self._redis:
return await self._redis.wait_closed()
return True
async with self._connection_lock:
if self._redis:
return await self._redis.wait_closed()
return True
async def redis(self) -> aioredis.RedisConnection:
"""
Get Redis connection
This property is awaitable.
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
if self._redis is None:
if self._redis is None or self._redis.closed:
self._redis = await aioredis.create_connection((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
loop=self._loop,
@ -147,7 +144,7 @@ class RedisStorage(BaseStorage):
record_data.update(data, **kwargs)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_data)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
"""
Get list of all stored chat's and user's
@ -222,9 +219,12 @@ class RedisStorage2(BaseStorage):
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None,
pool_size=10, loop=None, prefix='fsm', **kwargs):
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
ssl=None, pool_size=10, loop=None, prefix='fsm',
state_ttl: int = 0,
data_ttl: int = 0,
bucket_ttl: int = 0,
**kwargs):
self._host = host
self._port = port
self._db = db
@ -235,18 +235,20 @@ class RedisStorage2(BaseStorage):
self._kwargs = kwargs
self._prefix = (prefix,)
self._redis: aioredis.RedisConnection = None
self._state_ttl = state_ttl
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
self._redis: typing.Optional[aioredis.RedisConnection] = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def redis(self) -> aioredis.Redis:
"""
Get Redis connection
This property is awaitable.
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
if self._redis is None:
if self._redis is None or self._redis.closed:
self._redis = await aioredis.create_redis_pool((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
minsize=1, maxsize=self._pool_size,
@ -260,8 +262,6 @@ class RedisStorage2(BaseStorage):
async with self._connection_lock:
if self._redis and not self._redis.closed:
self._redis.close()
del self._redis
self._redis = None
async def wait_closed(self):
async with self._connection_lock:
@ -294,14 +294,14 @@ class RedisStorage2(BaseStorage):
if state is None:
await redis.delete(key)
else:
await redis.set(key, state)
await redis.set(key, state, expire=self._state_ttl)
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
await redis.set(key, json.dumps(data))
await redis.set(key, json.dumps(data), expire=self._data_ttl)
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
@ -329,7 +329,7 @@ class RedisStorage2(BaseStorage):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
redis = await self.redis()
await redis.set(key, json.dumps(bucket))
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
@ -338,7 +338,7 @@ class RedisStorage2(BaseStorage):
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
await self.set_bucket(chat=chat, user=user, data=temp_bucket)
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
@ -355,7 +355,7 @@ class RedisStorage2(BaseStorage):
keys = await conn.keys(self.generate_key('*'))
await conn.delete(*keys)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
"""
Get list of all stored chat's and user's

View file

@ -95,19 +95,17 @@ class I18nMiddleware(BaseMiddleware):
locale = self.ctx_locale.get()
if locale not in self.locales:
if n is 1:
if n == 1:
return singular
else:
return plural
return plural
translator = self.locales[locale]
if plural is None:
return translator.gettext(singular)
else:
return translator.ngettext(singular, plural, n)
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=False) -> LazyProxy:
"""
Lazy get text
@ -115,9 +113,10 @@ class I18nMiddleware(BaseMiddleware):
:param plural:
:param n:
:param locale:
:param enable_cache:
:return:
"""
return LazyProxy(self.gettext, singular, plural, n, locale)
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:

View file

@ -89,34 +89,39 @@ class LoggingMiddleware(BaseMiddleware):
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
if callback_query.message:
text = (f"Received callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
f"from user [ID:{callback_query.message.from_user.id}]")
else:
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
f"from inline message [ID:{callback_query.inline_message_id}] "
f"from user [ID:{callback_query.from_user.id}]")
f"from user [ID:{callback_query.from_user.id}] "
f"for inline message [ID:{callback_query.inline_message_id}] ")
async def on_post_process_callback_query(self, callback_query, results, data: dict):
if callback_query.message:
text = (f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
f"from user [ID:{callback_query.message.from_user.id}]")
else:
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from inline message [ID:{callback_query.inline_message_id}] "
f"from user [ID:{callback_query.from_user.id}]")
f"from user [ID:{callback_query.from_user.id}]"
f"from inline message [ID:{callback_query.inline_message_id}]")
async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict):
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
@ -141,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):
"""

View file

@ -8,8 +8,10 @@ import typing
import aiohttp
from aiohttp.helpers import sentinel
from aiogram.utils.deprecated import renamed_argument
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
RegexpCommandsFilter, StateFilter, Text
RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter
from .filters.builtin import IsSenderContact
from .handler import Handler
from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@ -68,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)
@ -85,34 +88,83 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(StateFilter, exclude_event_handlers=[
self.errors_handlers,
self.poll_handlers
self.poll_handlers,
self.poll_answer_handlers,
])
filters_factory.bind(ContentTypeFilter, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers,
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
]),
filters_factory.bind(Command, event_handlers=[
self.message_handlers, self.edited_message_handlers
self.message_handlers,
self.edited_message_handlers
])
filters_factory.bind(Text, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers,
self.callback_query_handlers, self.poll_handlers
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.poll_handlers,
self.inline_query_handlers,
])
filters_factory.bind(HashTag, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
])
filters_factory.bind(Regexp, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers,
self.callback_query_handlers, self.poll_handlers
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.poll_handlers,
self.inline_query_handlers,
])
filters_factory.bind(RegexpCommandsFilter, event_handlers=[
self.message_handlers, self.edited_message_handlers
self.message_handlers,
self.edited_message_handlers,
])
filters_factory.bind(ExceptionsFilter, event_handlers=[
self.errors_handlers
self.errors_handlers,
])
filters_factory.bind(AdminFilter, event_handlers=[
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
])
filters_factory.bind(IDFilter, event_handlers=[
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
])
filters_factory.bind(IsReplyFilter, event_handlers=[
self.message_handlers,
self.edited_message_handlers,
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,
])
filters_factory.bind(ForwardedMessageFilter, event_handlers=[
self.message_handlers,
self.edited_channel_post_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers
])
def __del__(self):
@ -189,6 +241,9 @@ 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:
types.User.set_current(update.poll_answer.user)
return await self.poll_answer_handlers.notify(update.poll_answer)
except Exception as e:
err = await self.errors_handlers.notify(update, e)
if err:
@ -253,6 +308,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
try:
with self.bot.request_timeout(request_timeout):
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
except asyncio.CancelledError:
break
except:
log.exception('Cause exception while getting updates.')
await asyncio.sleep(error_sleep)
@ -268,7 +325,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
await asyncio.sleep(relax)
finally:
self._close_waiter._set_result(None)
self._close_waiter.set_result(None)
log.warning('Polling is stopped.')
async def _process_polling_updates(self, updates, fast: typing.Optional[bool] = True):
@ -814,18 +871,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_answer: types.PollAnswer)
: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):
"""
@ -884,15 +1013,17 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
return FSMContext(storage=self.storage, chat=chat, user=user)
async def throttle(self, key, *, rate=None, user=None, chat=None, no_error=None) -> bool:
@renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
@renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
async def throttle(self, key, *, rate=None, user_id=None, chat_id=None, no_error=None) -> bool:
"""
Execute throttling manager.
Returns True if limit has not exceeded otherwise raises ThrottleError or returns False
:param key: key in storage
:param rate: limit (by default is equal to default rate limit)
:param user: user id
:param chat: chat id
:param user_id: user id
:param chat_id: chat id
:param no_error: return boolean value instead of raising error
:return: bool
"""
@ -903,14 +1034,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
no_error = self.no_throttle_error
if rate is None:
rate = self.throttling_rate_limit
if user is None and chat is None:
user = types.User.get_current()
chat = types.Chat.get_current()
if user_id is None and chat_id is None:
user_id = types.User.get_current().id
chat_id = types.Chat.get_current().id
# Detect current time
now = time.time()
bucket = await self.storage.get_bucket(chat=chat, user=user)
bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
# Fix bucket
if bucket is None:
@ -934,53 +1065,57 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
else:
data[EXCEEDED_COUNT] = 1
bucket[key].update(data)
await self.storage.set_bucket(chat=chat, user=user, bucket=bucket)
await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket)
if not result and not no_error:
# Raise if it is allowed
raise Throttled(key=key, chat=chat, user=user, **data)
raise Throttled(key=key, chat=chat_id, user=user_id, **data)
return result
async def check_key(self, key, chat=None, user=None):
@renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
@renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
async def check_key(self, key, chat_id=None, user_id=None):
"""
Get information about key in bucket
:param key:
:param chat:
:param user:
:param chat_id:
:param user_id:
:return:
"""
if not self.storage.has_bucket():
raise RuntimeError('This storage does not provide Leaky Bucket')
if user is None and chat is None:
user = types.User.get_current()
chat = types.Chat.get_current()
if user_id is None and chat_id is None:
user_id = types.User.get_current()
chat_id = types.Chat.get_current()
bucket = await self.storage.get_bucket(chat=chat, user=user)
bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
data = bucket.get(key, {})
return Throttled(key=key, chat=chat, user=user, **data)
return Throttled(key=key, chat=chat_id, user=user_id, **data)
async def release_key(self, key, chat=None, user=None):
@renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
@renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
async def release_key(self, key, chat_id=None, user_id=None):
"""
Release blocked key
:param key:
:param chat:
:param user:
:param chat_id:
:param user_id:
:return:
"""
if not self.storage.has_bucket():
raise RuntimeError('This storage does not provide Leaky Bucket')
if user is None and chat is None:
user = types.User.get_current()
chat = types.Chat.get_current()
if user_id is None and chat_id is None:
user_id = types.User.get_current()
chat_id = types.Chat.get_current()
bucket = await self.storage.get_bucket(chat=chat, user=user)
bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
if bucket and key in bucket:
del bucket['key']
await self.storage.set_bucket(chat=chat, user=user, bucket=bucket)
await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket)
return True
return False
@ -1025,3 +1160,64 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
if run_task:
return self.async_task(callback)
return callback
def throttled(self, on_throttled: typing.Optional[typing.Callable] = None,
key=None, rate=None,
user_id=None, chat_id=None):
"""
Meta-decorator for throttling.
Invokes on_throttled if the handler was throttled.
Example:
.. code-block:: python3
async def handler_throttled(message: types.Message, **kwargs):
await message.answer("Throttled!")
@dp.throttled(handler_throttled)
async def some_handler(message: types.Message):
await message.answer("Didn't throttled!")
:param on_throttled: the callable object that should be either a function or return a coroutine
:param key: key in storage
:param rate: limit (by default is equal to default rate limit)
:param user_id: user id
:param chat_id: chat id
:return: decorator
"""
def decorator(func):
@functools.wraps(func)
async def wrapped(*args, **kwargs):
is_not_throttled = await self.throttle(key if key is not None else func.__name__,
rate=rate,
user_id=user_id, chat_id=chat_id,
no_error=True)
if is_not_throttled:
return await func(*args, **kwargs)
else:
kwargs.update(
{
'rate': rate,
'key': key,
'user_id': user_id,
'chat_id': chat_id
}
) # update kwargs with parameters which were given to throttled
if on_throttled:
if asyncio.iscoroutinefunction(on_throttled):
await on_throttled(*args, **kwargs)
else:
kwargs.update(
{
'loop': asyncio.get_running_loop()
}
)
partial_func = functools.partial(on_throttled, *args, **kwargs)
asyncio.get_running_loop().run_in_executor(None,
partial_func
)
return wrapped
return decorator

View file

@ -1,5 +1,6 @@
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
@ -23,8 +24,13 @@ __all__ = [
'Regexp',
'StateFilter',
'Text',
'IDFilter',
'IsReplyFilter',
'IsSenderContact',
'AdminFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
'check_filters'
'check_filters',
'ForwardedMessageFilter',
]

View file

@ -9,7 +9,20 @@ from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
def extract_chat_ids(chat_id: ChatIDArgumentType) -> typing.Set[int]:
# since "str" is also an "Iterable", we have to check for it first
if isinstance(chat_id, str):
return {int(chat_id), }
if isinstance(chat_id, Iterable):
return {int(item) for (item) in chat_id}
# the last possible type is a single "int"
return {chat_id, }
class Command(Filter):
@ -79,14 +92,17 @@ class Command(Filter):
@staticmethod
async def check_command(message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False):
if not message.text: # Prevent to use with non-text content types
return False
full_command = message.text.split()[0]
prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@')
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
return False
elif prefix not in prefixes:
if prefix not in prefixes:
return False
elif (command.lower() if ignore_case else command) not in commands:
if (command.lower() if ignore_case else command) not in commands:
return False
return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)}
@ -137,7 +153,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.
@ -148,9 +166,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(CommandStart, self).__init__(['start'])
super().__init__(['start'])
self.deep_link = deep_link
self.encoded = encoded
async def check(self, message: types.Message):
"""
@ -159,13 +179,16 @@ class CommandStart(Command):
:param message:
:return:
"""
check = await super(CommandStart, self).check(message)
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
@ -179,7 +202,7 @@ class CommandHelp(Command):
"""
def __init__(self):
super(CommandHelp, self).__init__(['help'])
super().__init__(['help'])
class CommandSettings(Command):
@ -188,7 +211,7 @@ class CommandSettings(Command):
"""
def __init__(self):
super(CommandSettings, self).__init__(['settings'])
super().__init__(['settings'])
class CommandPrivacy(Command):
@ -197,7 +220,7 @@ class CommandPrivacy(Command):
"""
def __init__(self):
super(CommandPrivacy, self).__init__(['privacy'])
super().__init__(['privacy'])
class Text(Filter):
@ -205,33 +228,44 @@ class Text(Filter):
Simple text filter
"""
_default_params = (
('text', 'equals'),
('text_contains', 'contains'),
('text_startswith', 'startswith'),
('text_endswith', 'endswith'),
)
def __init__(self,
equals: Optional[Union[str, LazyProxy]] = None,
contains: Optional[Union[str, LazyProxy]] = None,
startswith: Optional[Union[str, LazyProxy]] = None,
endswith: Optional[Union[str, LazyProxy]] = None,
equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
ignore_case=False):
"""
Check text for one of pattern. Only one mode can be used in one filter.
In every pattern, a single string is treated as a list with 1 element.
:param equals:
:param contains:
:param startswith:
:param endswith:
:param equals: True if object's text in the list
:param contains: True if object's text contains all strings from the list
:param startswith: True if object's text starts with any of strings from the list
:param endswith: True if object's text ends with any of strings from the list
:param ignore_case: case insensitive
"""
# Only one mode can be used. check it.
check = sum(map(bool, (equals, contains, startswith, endswith)))
check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
if check > 1:
args = "' and '".join([arg[0] for arg in [('equals', equals),
('contains', contains),
('startswith', startswith),
('endswith', endswith)
] if arg[1]])
] if arg[1] is not None])
raise ValueError(f"Arguments '{args}' cannot be used together.")
elif check == 0:
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,
(equals, contains, endswith, startswith))
self.equals = equals
self.contains = contains
self.endswith = endswith
@ -240,16 +274,11 @@ class Text(Filter):
@classmethod
def validate(cls, full_config: Dict[str, Any]):
if 'text' in full_config:
return {'equals': full_config.pop('text')}
elif 'text_contains' in full_config:
return {'contains': full_config.pop('text_contains')}
elif 'text_startswith' in full_config:
return {'startswith': full_config.pop('text_startswith')}
elif 'text_endswith' in full_config:
return {'endswith': full_config.pop('text_endswith')}
for param, key in cls._default_params:
if param in full_config:
return {key: full_config.pop(param)}
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
text = obj.text or obj.caption or ''
if not text and obj.poll:
@ -265,15 +294,26 @@ class Text(Filter):
if self.ignore_case:
text = text.lower()
_pre_process_func = lambda s: str(s).lower()
else:
_pre_process_func = str
if self.equals:
return text == str(self.equals)
elif self.contains:
return str(self.contains) in text
elif self.startswith:
return text.startswith(str(self.startswith))
elif self.endswith:
return text.endswith(str(self.endswith))
# now check
if self.equals is not None:
equals = list(map(_pre_process_func, self.equals))
return text in equals
if self.contains is not None:
contains = list(map(_pre_process_func, self.contains))
return all(map(text.__contains__, contains))
if self.startswith is not None:
startswith = list(map(_pre_process_func, self.startswith))
return any(map(text.startswith, startswith))
if self.endswith is not None:
endswith = list(map(_pre_process_func, self.endswith))
return any(map(text.endswith, endswith))
return False
@ -350,7 +390,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
@ -359,13 +399,17 @@ class Regexp(Filter):
if 'regexp' in full_config:
return {'regexp': full_config.pop('regexp')}
async def check(self, obj: Union[Message, CallbackQuery]):
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
content = obj.text or obj.caption or ''
if not content and obj.poll:
content = obj.poll.question
elif isinstance(obj, CallbackQuery) and obj.data:
content = obj.data
elif isinstance(obj, InlineQuery):
content = obj.query
elif isinstance(obj, Poll):
content = obj.question
else:
return False
@ -413,6 +457,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):
@ -420,6 +466,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
@ -487,3 +555,139 @@ class ExceptionsFilter(BoundFilter):
return True
except:
return False
class IDFilter(Filter):
def __init__(self,
user_id: Optional[ChatIDArgumentType] = None,
chat_id: Optional[ChatIDArgumentType] = None,
):
"""
:param user_id:
:param chat_id:
"""
if user_id is None and chat_id is None:
raise ValueError("Both user_id and chat_id can't be None")
self.user_id: Optional[typing.Set[int]] = None
self.chat_id: Optional[typing.Set[int]] = None
if user_id:
self.user_id = extract_chat_ids(user_id)
if chat_id:
self.chat_id = extract_chat_ids(chat_id)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if 'user_id' in full_config:
result['user_id'] = full_config.pop('user_id')
if 'chat_id' in full_config:
result['chat_id'] = full_config.pop('chat_id')
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
if isinstance(obj, Message):
user_id = obj.from_user.id
chat_id = obj.chat.id
elif isinstance(obj, CallbackQuery):
user_id = obj.from_user.id
chat_id = None
if obj.message is not None:
# if the button was sent with message
chat_id = obj.message.chat.id
elif isinstance(obj, InlineQuery):
user_id = obj.from_user.id
chat_id = None
else:
return False
if self.user_id and self.chat_id:
return user_id in self.user_id and chat_id in self.chat_id
if self.user_id:
return user_id in self.user_id
if self.chat_id:
return chat_id in self.chat_id
return False
class AdminFilter(Filter):
"""
Checks if user is admin in a chat.
If is_chat_admin is not set, the filter will check in the current chat (correct only for messages).
is_chat_admin is required for InlineQuery.
"""
def __init__(self, is_chat_admin: Optional[Union[ChatIDArgumentType, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
if not is_chat_admin:
self._check_current = True
return
if isinstance(is_chat_admin, bool):
self._check_current = is_chat_admin
self._chat_ids = extract_chat_ids(is_chat_admin)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if "is_chat_admin" in full_config:
result["is_chat_admin"] = full_config.pop("is_chat_admin")
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool:
user_id = obj.from_user.id
if self._check_current:
if isinstance(obj, Message):
message = obj
elif isinstance(obj, CallbackQuery) and obj.message:
message = obj.message
else:
return False
if ChatType.is_private(message): # there is no admin in private chats
return False
chat_ids = [message.chat.id]
else:
chat_ids = self._chat_ids
admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)]
return user_id in admins
class IsReplyFilter(BoundFilter):
"""
Check if message is replied and send reply message to handler
"""
key = 'is_reply'
def __init__(self, is_reply):
self.is_reply = is_reply
async def check(self, msg: Message):
if msg.reply_to_message and self.is_reply:
return {'reply': msg.reply_to_message}
elif not msg.reply_to_message and not self.is_reply:
return True
class ForwardedMessageFilter(BoundFilter):
key = 'is_forwarded'
def __init__(self, is_forwarded: bool):
self.is_forwarded = is_forwarded
async def check(self, message: Message):
return bool(getattr(message, "forward_date")) is self.is_forwarded

View file

@ -70,4 +70,4 @@ class FiltersFactory:
yield filter_
if full_config:
raise NameError('Invalid filter name(s): \'' + '\', '.join(full_config.keys()) + '\'')
raise NameError("Invalid filter name(s): '" + "', ".join(full_config.keys()) + "'")

View file

@ -82,7 +82,7 @@ class FilterRecord:
Filters record for factory
"""
def __init__(self, callback: typing.Callable,
def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'],
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None):
@ -202,14 +202,14 @@ class BoundFilter(Filter):
You need to implement ``__init__`` method with single argument related with key attribute
and ``check`` method where you need to implement filter logic.
"""
"""Unique name of the filter argument. You need to override this attribute."""
key = None
"""If :obj:`True` this filter will be added to the all of the registered handlers"""
"""Unique name of the filter argument. You need to override this attribute."""
required = False
"""Default value for configure required filters"""
"""If :obj:`True` this filter will be added to the all of the registered handlers"""
default = None
"""Default value for configure required filters"""
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
"""

View file

@ -25,17 +25,17 @@ class State:
@property
def state(self):
if self._state is None:
return None
elif self._state == '*':
if self._state is None or self._state == '*':
return self._state
elif self._group_name is None and self._group:
if self._group_name is None and self._group:
group = self._group.__full_group_name__
elif self._group_name:
group = self._group_name
else:
group = '@'
return f"{group}:{self._state}"
return f'{group}:{self._state}'
def set_parent(self, group):
if not issubclass(group, StatesGroup):
@ -73,7 +73,6 @@ class StatesGroupMeta(type):
elif inspect.isclass(prop) and issubclass(prop, StatesGroup):
childs.append(prop)
prop._parent = cls
# continue
cls._parent = None
cls._childs = tuple(childs)
@ -83,13 +82,13 @@ class StatesGroupMeta(type):
return cls
@property
def __group_name__(cls):
def __group_name__(cls) -> str:
return cls._group_name
@property
def __full_group_name__(cls):
def __full_group_name__(cls) -> str:
if cls._parent:
return cls._parent.__full_group_name__ + '.' + cls._group_name
return '.'.join((cls._parent.__full_group_name__, cls._group_name))
return cls._group_name
@property
@ -97,7 +96,7 @@ class StatesGroupMeta(type):
return cls._states
@property
def childs(cls):
def childs(cls) -> tuple:
return cls._childs
@property
@ -130,9 +129,9 @@ class StatesGroupMeta(type):
def __contains__(cls, item):
if isinstance(item, str):
return item in cls.all_states_names
elif isinstance(item, State):
if isinstance(item, State):
return item in cls.all_states
elif isinstance(item, StatesGroup):
if isinstance(item, StatesGroup):
return item in cls.all_childs
return False

View file

@ -1,7 +1,7 @@
import inspect
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Optional, Iterable
from typing import Optional, Iterable, List
ctx_data = ContextVar('ctx_handler_data')
current_handler = ContextVar('current_handler')
@ -25,9 +25,8 @@ class CancelHandler(Exception):
def _get_spec(func: callable):
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
func = func.__wrapped__
spec = inspect.getfullargspec(func)
return spec, func
return spec
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
@ -42,11 +41,10 @@ class Handler:
self.dispatcher = dispatcher
self.once = once
self.handlers = []
self.handlers: List[Handler.HandlerObj] = []
self.middleware_key = middleware_key
def register(self, handler, filters=None, index=None):
from .filters import get_filters_spec
"""
Register callback
@ -56,7 +54,9 @@ class Handler:
:param filters: list of filters
:param index: you can reorder handlers
"""
spec, handler = _get_spec(handler)
from .filters import get_filters_spec
spec = _get_spec(handler)
if filters and not isinstance(filters, (list, tuple, set)):
filters = [filters]

View file

@ -5,6 +5,7 @@ import functools
import ipaddress
import itertools
import typing
import logging
from typing import Dict, List, Optional, Union
from aiohttp import web
@ -35,6 +36,8 @@ TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22')
allowed_ips = set()
log = logging.getLogger(__name__)
def _check_ip(ip: str) -> bool:
"""
@ -77,7 +80,7 @@ class WebhookRequestHandler(web.View):
.. code-block:: python3
app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler')
app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler')
But first you need to configure application for getting Dispatcher instance from request handler!
It must always be with key 'BOT_DISPATCHER'
@ -179,7 +182,7 @@ class WebhookRequestHandler(web.View):
try:
try:
await waiter
except asyncio.futures.CancelledError:
except asyncio.CancelledError:
fut.remove_done_callback(cb)
fut.cancel()
raise
@ -258,7 +261,9 @@ class WebhookRequestHandler(web.View):
if self.request.app.get('_check_ip', False):
ip_address, accept = self.check_ip()
if not accept:
log.warning(f"Blocking request from an unauthorized IP: {ip_address}")
raise web.HTTPUnauthorized()
# context.set_value('TELEGRAM_IP', ip_address)
@ -518,7 +523,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio
'disable_web_page_preview': self.disable_web_page_preview,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
def write(self, *text, sep=' '):
@ -637,7 +642,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -699,7 +704,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'title': self.title,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -812,7 +817,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -866,7 +871,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'duration': self.duration,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -919,7 +924,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'length': self.length,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1045,7 +1050,7 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'longitude': self.longitude,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1104,7 +1109,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'foursquare_id': self.foursquare_id,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1155,7 +1160,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'last_name': self.last_name,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1215,7 +1220,7 @@ class KickChatMember(BaseResponse):
return {
'chat_id': self.chat_id,
'user_id': self.user_id,
'until_date': prepare_arg(self.until_date)
'until_date': prepare_arg(self.until_date),
}
@ -1603,7 +1608,7 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin):
'text': self.text,
'parse_mode': self.parse_mode,
'disable_web_page_preview': self.disable_web_page_preview,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1644,7 +1649,7 @@ class EditMessageCaption(BaseResponse):
'message_id': self.message_id,
'inline_message_id': self.inline_message_id,
'caption': self.caption,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1680,7 +1685,7 @@ class EditMessageReplyMarkup(BaseResponse):
'chat_id': self.chat_id,
'message_id': self.message_id,
'inline_message_id': self.inline_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1751,7 +1756,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'sticker': self.sticker,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}
@ -1843,7 +1848,7 @@ class AddStickerToSet(BaseResponse):
'name': self.name,
'png_sticker': self.png_sticker,
'emojis': self.emojis,
'mask_position': prepare_arg(self.mask_position)
'mask_position': prepare_arg(self.mask_position),
}
@ -1962,7 +1967,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
__slots__ = ('chat_id', 'title', 'description', 'payload', 'provider_token', 'start_parameter',
'currency', 'prices', 'photo_url', 'photo_size', 'photo_width', 'photo_height',
'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', 'is_flexible',
'need_name', 'need_phone_number', 'need_email', 'need_shipping_address',
'send_phone_number_to_provider', 'send_email_to_provider', 'is_flexible',
'disable_notification', 'reply_to_message_id', 'reply_markup')
method = api.Methods.SEND_INVOICE
@ -1983,6 +1989,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
need_phone_number: Optional[Boolean] = None,
need_email: Optional[Boolean] = None,
need_shipping_address: Optional[Boolean] = None,
send_phone_number_to_provider: Optional[Boolean] = None,
send_email_to_provider: Optional[Boolean] = None,
is_flexible: Optional[Boolean] = None,
disable_notification: Optional[Boolean] = None,
reply_to_message_id: Optional[Integer] = None,
@ -2011,6 +2019,10 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
:param need_email: Boolean (Optional) - Pass True, if you require the user's email to complete the order
:param need_shipping_address: Boolean (Optional) - Pass True, if you require the user's
shipping address to complete the order
:param send_phone_number_to_provider: Boolean (Optional) - Pass True, if user's phone number should be sent
to provider
:param send_email_to_provider: Boolean (Optional) - Pass True, if user's email address should be sent
to provider
:param is_flexible: Boolean (Optional) - Pass True, if the final price depends on the shipping method
:param disable_notification: Boolean (Optional) - Sends the message silently.
Users will receive a notification with no sound.
@ -2034,6 +2046,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
self.need_phone_number = need_phone_number
self.need_email = need_email
self.need_shipping_address = need_shipping_address
self.send_phone_number_to_provider = send_phone_number_to_provider
self.send_email_to_provider = send_email_to_provider
self.is_flexible = is_flexible
self.disable_notification = disable_notification
self.reply_to_message_id = reply_to_message_id
@ -2057,6 +2071,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'need_phone_number': self.need_phone_number,
'need_email': self.need_email,
'need_shipping_address': self.need_shipping_address,
'send_phone_number_to_provider': self.send_phone_number_to_provider,
'send_email_to_provider': self.send_email_to_provider,
'is_flexible': self.is_flexible,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
@ -2172,5 +2188,5 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'game_short_name': self.game_short_name,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup)
'reply_markup': prepare_arg(self.reply_markup),
}

View file

@ -3,13 +3,16 @@ from . import fields
from .animation import Animation
from .audio import Audio
from .auth_widget_data import AuthWidgetData
from .bot_command import BotCommand
from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
from .chat_member import ChatMember, ChatMemberStatus
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
from .dice import Dice, DiceEmoji
from .document import Document
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
@ -44,9 +47,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
@ -68,6 +71,7 @@ __all__ = (
'Animation',
'Audio',
'AuthWidgetData',
'BotCommand',
'CallbackGame',
'CallbackQuery',
'Chat',
@ -80,6 +84,8 @@ __all__ = (
'Contact',
'ContentType',
'ContentTypes',
'Dice',
'DiceEmoji',
'Document',
'EncryptedCredentials',
'EncryptedPassportElement',
@ -125,6 +131,7 @@ __all__ = (
'InputVenueMessageContent',
'Invoice',
'KeyboardButton',
'KeyboardButtonPollType',
'LabeledPrice',
'Location',
'LoginUrl',
@ -146,7 +153,9 @@ __all__ = (
'PassportFile',
'PhotoSize',
'Poll',
'PollAnswer',
'PollOption',
'PollType',
'PreCheckoutQuery',
'ReplyKeyboardMarkup',
'ReplyKeyboardRemove',

View file

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

View file

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

View file

@ -9,6 +9,8 @@ from babel.support import LazyProxy
from .fields import BaseField
from ..utils import json
from ..utils.mixins import ContextInstanceMixin
if typing.TYPE_CHECKING:
from ..bot.bot import Bot
__all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean')
@ -22,6 +24,7 @@ String = TypeVar('String', bound=str)
Integer = TypeVar('Integer', bound=int)
Float = TypeVar('Float', bound=float)
Boolean = TypeVar('Boolean', bound=bool)
T = TypeVar('T')
class MetaTelegramObject(type):
@ -30,7 +33,7 @@ class MetaTelegramObject(type):
"""
_objects = {}
def __new__(mcs, name, bases, namespace, **kwargs):
def __new__(mcs: typing.Type[T], name: str, bases: typing.Tuple[typing.Type], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any) -> T:
cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace)
props = {}
@ -71,7 +74,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
Abstract class for telegram objects
"""
def __init__(self, conf=None, **kwargs):
def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None:
"""
Deserialize object
@ -117,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, ALIASES_ATTR_NAME, {})
@property
def values(self):
def values(self) -> typing.Dict[str, typing.Any]:
"""
Get values
@ -128,11 +131,11 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, VALUES_ATTR_NAME)
@property
def telegram_types(self):
def telegram_types(self) -> typing.List[TelegramObject]:
return type(self).telegram_types
@classmethod
def to_object(cls, data):
def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T:
"""
Deserialize object
@ -142,7 +145,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return cls(**data)
@property
def bot(self):
def bot(self) -> Bot:
from ..bot.bot import Bot
bot = Bot.get_current()
@ -152,15 +155,16 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"'Bot.set_current(bot_instance)'")
return bot
def to_python(self) -> typing.Dict:
def to_python(self) -> typing.Dict[str, typing.Any]:
"""
Get object as JSON serializable
: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):
@ -170,7 +174,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
result[self.props_aliases.get(name, name)] = value
return result
def clean(self):
def clean(self) -> None:
"""
Remove empty values
"""
@ -188,7 +192,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return json.dumps(self.to_python())
@classmethod
def create(cls, *args, **kwargs):
def create(cls: typing.Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
raise NotImplemented
def __str__(self) -> str:
@ -199,7 +203,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
return self.as_json()
def __getitem__(self, item):
def __getitem__(self, item: typing.Union[str, int]) -> typing.Any:
"""
Item getter (by key)
@ -210,7 +214,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return self.props[item].get_value(self)
raise KeyError(item)
def __setitem__(self, key, value):
def __setitem__(self, key: str, value: typing.Any) -> None:
"""
Item setter (by key)
@ -222,17 +226,17 @@ 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):
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):
def __iter__(self) -> typing.Iterator[str]:
"""
Iterate over items
@ -241,7 +245,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for item in self.to_python().items():
yield item
def iter_keys(self):
def iter_keys(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over keys
@ -250,7 +254,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for key, _ in self:
yield key
def iter_values(self):
def iter_values(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over values
@ -259,9 +263,9 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for _, value in self:
yield value
def __hash__(self):
def _hash(obj):
buf = 0
def __hash__(self) -> int:
def _hash(obj) -> int:
buf: int = 0
if isinstance(obj, list):
for item in obj:
buf += _hash(item)
@ -281,5 +285,5 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return result
def __eq__(self, other):
def __eq__(self, other: TelegramObject) -> bool:
return isinstance(other, self.__class__) and hash(other) == hash(self)

View file

@ -0,0 +1,15 @@
from . import base
from . import fields
class BotCommand(base.TelegramObject):
"""
This object represents a bot command.
https://core.telegram.org/bots/api#botcommand
"""
command: base.String = fields.Field()
description: base.String = fields.Field()
def __init__(self, command: base.String, description: base.String):
super(BotCommand, self).__init__(command=command, description=description)

View file

@ -1,13 +1,15 @@
from __future__ import annotations
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):
@ -27,6 +29,8 @@ class Chat(base.TelegramObject):
description: base.String = fields.Field()
invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
slow_mode_delay: base.Integer = fields.Field()
sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field()
@ -34,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:
@ -43,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
"""
@ -54,20 +58,35 @@ 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):
@property
def shifted_id(self) -> int:
"""
Get shifted id of chat, e.g. for private links
For example: -1001122334455 -> 1122334455
"""
if self.type == ChatType.PRIVATE:
raise TypeError('`shifted_id` property is not available for private chats')
shift = -1_000_000_000_000
return shift - self.id
def get_mention(self, name=None, as_html=True) -> base.String:
if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html':
as_html = True
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.
@ -98,7 +117,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.
@ -115,7 +134,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.
@ -130,7 +149,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.
@ -147,7 +166,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.
@ -159,10 +178,10 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.delete_chat_description(self.id, description)
return await self.bot.set_chat_description(self.id, description)
async def kick(self, user_id: base.Integer,
until_date: typing.Union[base.Integer, 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
@ -185,7 +204,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.
@ -202,7 +221,8 @@ class Chat(base.TelegramObject):
return await self.bot.unban_chat_member(self.id, user_id=user_id)
async def restrict(self, user_id: base.Integer,
until_date: typing.Union[base.Integer, None] = None,
permissions: typing.Optional[ChatPermissions] = None,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
@ -216,6 +236,8 @@ class Chat(base.TelegramObject):
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
:param permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time.
:type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@ -232,7 +254,9 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date,
return await self.bot.restrict_chat_member(self.id, user_id=user_id,
permissions=permissions,
until_date=until_date,
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_other_messages=can_send_other_messages,
@ -288,7 +312,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.
@ -305,7 +356,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.
@ -317,7 +368,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.
@ -328,7 +379,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.
@ -342,7 +393,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.
@ -353,7 +404,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.
@ -366,7 +417,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
@ -384,7 +467,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.

View file

@ -1,5 +1,6 @@
import datetime
import warnings
from typing import Optional
from . import base
from . import fields
@ -15,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()
@ -28,22 +30,17 @@ class ChatMember(base.TelegramObject):
is_member: base.Boolean = fields.Field()
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
def is_admin(self):
warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. '
'This method renamed to `is_chat_admin` and will be available until aiogram 2.2',
DeprecationWarning, stacklevel=2)
return self.is_chat_admin()
def is_chat_admin(self) -> bool:
return ChatMemberStatus.is_chat_admin(self.status)
def is_chat_admin(self):
return ChatMemberStatus.is_admin(self.status)
def is_chat_member(self) -> bool:
return ChatMemberStatus.is_chat_member(self.status)
def is_chat_member(self):
return ChatMemberStatus.is_member(self.status)
def __int__(self):
def __int__(self) -> int:
return self.user.id
@ -51,33 +48,19 @@ class ChatMemberStatus(helper.Helper):
"""
Chat member status
"""
mode = helper.HelperMode.lowercase
CREATOR = helper.Item() # creator
ADMINISTRATOR = helper.Item() # administrator
MEMBER = helper.Item() # member
RESTRICTED = helper.Item() # restricted
LEFT = helper.Item() # left
KICKED = helper.Item() # kicked
@classmethod
def is_admin(cls, role):
warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. '
'This method renamed to `is_chat_admin` and will be available until aiogram 2.2',
DeprecationWarning, stacklevel=2)
return cls.is_chat_admin(role)
@classmethod
def is_member(cls, role):
warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. '
'This method renamed to `is_chat_member` and will be available until aiogram 2.2',
DeprecationWarning, stacklevel=2)
return cls.is_chat_member(role)
@classmethod
def is_chat_admin(cls, role):
def is_chat_admin(cls, role: str) -> bool:
return role in [cls.ADMINISTRATOR, cls.CREATOR]
@classmethod
def is_chat_member(cls, role):
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR]
def is_chat_member(cls, role: str) -> bool:
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED]

View file

@ -0,0 +1,39 @@
from . import base
from . import fields
class ChatPermissions(base.TelegramObject):
"""
Describes actions that a non-administrator user is allowed to take in a chat.
https://core.telegram.org/bots/api#chatpermissions
"""
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
can_invite_users: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()
def __init__(self,
can_send_messages: base.Boolean = None,
can_send_media_messages: base.Boolean = None,
can_send_polls: base.Boolean = None,
can_send_other_messages: base.Boolean = None,
can_add_web_page_previews: base.Boolean = None,
can_change_info: base.Boolean = None,
can_invite_users: base.Boolean = None,
can_pin_messages: base.Boolean = None,
**kwargs):
super(ChatPermissions, self).__init__(
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
)

View file

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

19
aiogram/types/dice.py Normal file
View file

@ -0,0 +1,19 @@
from . import base, fields
class Dice(base.TelegramObject):
"""
This object represents a dice with random value from 1 to 6.
(Yes, we're aware of the “proper” singular of die.
But it's awkward, and we decided to help it change. One dice at a time!)
https://core.telegram.org/bots/api#dice
"""
emoji: base.String = fields.Field()
value: base.Integer = fields.Field()
class DiceEmoji:
DICE = '🎲'
DART = '🎯'
BASKETBALL = '🏀'

View file

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

View file

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

View file

@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult):
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url,
photo_width=photo_width, photo_height=photo_height, title=title,
description=description, caption=caption,
reply_markup=reply_markup,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
@ -117,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult):
gif_height: base.Integer = fields.Field()
gif_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
@ -156,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_height: base.Integer = fields.Field()
mpeg4_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)

View file

@ -26,7 +26,7 @@ class InputMedia(base.TelegramObject):
media: base.String = fields.Field(alias='media', on_change='_media_changed')
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
caption: base.String = fields.Field()
parse_mode: base.Boolean = fields.Field()
parse_mode: base.String = fields.Field()
def __init__(self, *args, **kwargs):
self._thumb_file = None
@ -110,7 +110,7 @@ class InputMediaAnimation(InputMedia):
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.Boolean = None, **kwargs):
parse_mode: base.String = None, **kwargs):
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, conf=kwargs)
@ -124,7 +124,7 @@ class InputMediaDocument(InputMedia):
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia):
https://core.telegram.org/bots/api#inputmediaanimation
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia):
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.Boolean = None, **kwargs):
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode: base.String = None, **kwargs):
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb,
caption=caption, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode, conf=kwargs)
@ -165,7 +162,7 @@ class InputMediaPhoto(InputMedia):
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
@ -186,7 +183,7 @@ class InputMediaVideo(InputMedia):
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.Boolean = None,
parse_mode: base.String = None,
supports_streaming: base.Boolean = None, **kwargs):
super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
@ -277,7 +274,7 @@ class MediaGroup(base.TelegramObject):
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.Boolean = None):
parse_mode: base.String = None):
"""
Attach animation
@ -299,7 +296,7 @@ class MediaGroup(base.TelegramObject):
self.attach(audio)
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None):
caption: base.String = None, parse_mode: base.String = None):
"""
Attach document
@ -349,7 +346,7 @@ class MediaGroup(base.TelegramObject):
:return:
"""
self.clean()
# self.clean()
result = []
for obj in self.media:
if isinstance(obj, base.TelegramObject):

View file

@ -2,7 +2,6 @@ from __future__ import annotations
import datetime
import functools
import sys
import typing
from . import base
@ -11,10 +10,11 @@ from .animation import Animation
from .audio import Audio
from .chat import Chat, ChatType
from .contact import Contact
from .dice import Dice
from .document import Document
from .force_reply import ForceReply
from .game import Game
from .inline_keyboard import InlineKeyboardMarkup, InlineKeyboardButton
from .inline_keyboard import InlineKeyboardMarkup
from .input_media import MediaGroup, InputMedia
from .invoice import Invoice
from .location import Location
@ -32,7 +32,7 @@ from .video_note import VideoNote
from .voice import Voice
from ..utils import helper
from ..utils import markdown as md
from ..utils.deprecated import warn_deprecated
from ..utils.text_decorations import html_decoration, markdown_decoration
class Message(base.TelegramObject):
@ -51,6 +51,7 @@ class Message(base.TelegramObject):
forward_signature: base.String = fields.Field()
forward_date: datetime.datetime = fields.DateTimeField()
reply_to_message: Message = fields.Field(base='Message')
via_bot: User = fields.Field(base=User)
edit_date: datetime.datetime = fields.DateTimeField()
media_group_id: base.String = fields.Field()
author_signature: base.String = fields.Field()
@ -71,6 +72,8 @@ class Message(base.TelegramObject):
contact: Contact = fields.Field(base=Contact)
location: Location = fields.Field(base=Location)
venue: Venue = fields.Field(base=Venue)
poll: Poll = fields.Field(base=Poll)
dice: Dice = fields.Field(base=Dice)
new_chat_members: typing.List[User] = fields.ListField(base=User)
left_chat_member: User = fields.Field(base=User)
new_chat_title: base.String = fields.Field()
@ -86,68 +89,69 @@ class Message(base.TelegramObject):
successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment)
connected_website: base.String = fields.Field()
passport_data: PassportData = fields.Field(base=PassportData)
poll: Poll = fields.Field(base=Poll)
reply_markup: typing.List[typing.List[InlineKeyboardButton]] = fields.ListOfLists(base=InlineKeyboardButton)
reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup)
@property
@functools.lru_cache()
def content_type(self):
if self.text:
return ContentType.TEXT
elif self.audio:
if self.audio:
return ContentType.AUDIO
elif self.animation:
if self.animation:
return ContentType.ANIMATION
elif self.document:
if self.document:
return ContentType.DOCUMENT
elif self.game:
if self.game:
return ContentType.GAME
elif self.photo:
if self.photo:
return ContentType.PHOTO
elif self.sticker:
if self.sticker:
return ContentType.STICKER
elif self.video:
if self.video:
return ContentType.VIDEO
elif self.video_note:
if self.video_note:
return ContentType.VIDEO_NOTE
elif self.voice:
if self.voice:
return ContentType.VOICE
elif self.contact:
if self.contact:
return ContentType.CONTACT
elif self.venue:
if self.venue:
return ContentType.VENUE
elif self.location:
if self.location:
return ContentType.LOCATION
elif self.new_chat_members:
return ContentType.NEW_CHAT_MEMBERS
elif self.left_chat_member:
return ContentType.LEFT_CHAT_MEMBER
elif self.invoice:
return ContentType.INVOICE
elif self.successful_payment:
return ContentType.SUCCESSFUL_PAYMENT
elif self.connected_website:
return ContentType.CONNECTED_WEBSITE
elif self.migrate_from_chat_id:
return ContentType.MIGRATE_FROM_CHAT_ID
elif self.migrate_to_chat_id:
return ContentType.MIGRATE_TO_CHAT_ID
elif self.pinned_message:
return ContentType.PINNED_MESSAGE
elif self.new_chat_title:
return ContentType.NEW_CHAT_TITLE
elif self.new_chat_photo:
return ContentType.NEW_CHAT_PHOTO
elif self.delete_chat_photo:
return ContentType.DELETE_CHAT_PHOTO
elif self.group_chat_created:
return ContentType.GROUP_CHAT_CREATED
elif self.passport_data:
return ContentType.PASSPORT_DATA
elif self.poll:
if self.poll:
return ContentType.POLL
else:
return ContentType.UNKNOWN
if self.dice:
return ContentType.DICE
if self.new_chat_members:
return ContentType.NEW_CHAT_MEMBERS
if self.left_chat_member:
return ContentType.LEFT_CHAT_MEMBER
if self.invoice:
return ContentType.INVOICE
if self.successful_payment:
return ContentType.SUCCESSFUL_PAYMENT
if self.connected_website:
return ContentType.CONNECTED_WEBSITE
if self.migrate_from_chat_id:
return ContentType.MIGRATE_FROM_CHAT_ID
if self.migrate_to_chat_id:
return ContentType.MIGRATE_TO_CHAT_ID
if self.pinned_message:
return ContentType.PINNED_MESSAGE
if self.new_chat_title:
return ContentType.NEW_CHAT_TITLE
if self.new_chat_photo:
return ContentType.NEW_CHAT_PHOTO
if self.delete_chat_photo:
return ContentType.DELETE_CHAT_PHOTO
if self.group_chat_created:
return ContentType.GROUP_CHAT_CREATED
if self.passport_data:
return ContentType.PASSPORT_DATA
return ContentType.UNKNOWN
def is_command(self):
"""
@ -164,7 +168,8 @@ class Message(base.TelegramObject):
:return: tuple of (command, args)
"""
if self.is_command():
command, _, args = self.text.partition(' ')
command, *args = self.text.split(maxsplit=1)
args = args[0] if args else None
return command, args
def get_command(self, pure=False):
@ -188,7 +193,7 @@ class Message(base.TelegramObject):
"""
command = self.get_full_command()
if command:
return command[1].strip()
return command[1]
def parse_entities(self, as_html=True):
"""
@ -201,38 +206,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:
@ -259,12 +236,19 @@ class Message(base.TelegramObject):
:return: str
"""
if self.chat.type not in [ChatType.SUPER_GROUP, ChatType.CHANNEL]:
if ChatType.is_private(self.chat):
raise TypeError('Invalid chat type!')
elif not self.chat.username:
raise TypeError('This chat does not have @username')
return f"https://t.me/{self.chat.username}/{self.message_id}"
url = 'https://t.me/'
if self.chat.username:
# Generates public link
url += f'{self.chat.username}/'
else:
# Generates private link available for chat members
url += f'c/{self.chat.shifted_id}/'
url += f'{self.message_id}'
return url
def link(self, text, as_html=True) -> str:
"""
@ -827,6 +811,39 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def answer_dice(self, emoji: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = False) -> Message:
"""
Use this method to send a dice, which will have a random value from 1 to 6.
On success, the sent Message is returned.
(Yes, we're aware of the “proper” singular of die.
But it's awkward, and we decided to help it change. One dice at a time!)
Source: https://core.telegram.org/bots/api#senddice
:param emoji: Emoji on which the dice throw animation is based. Currently, must be one of 🎲 or 🎯. Defauts to 🎲
:type emoji: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
return await self.bot.send_dice(chat_id=self.chat.id,
disable_notification=disable_notification,
emoji=emoji,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply(self, text: base.String,
parse_mode: typing.Union[base.String, None] = None,
disable_web_page_preview: typing.Union[base.Boolean, None] = None,
@ -959,71 +976,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def send_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
height: typing.Union[base.Integer, None] = None,
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
caption: typing.Union[base.String, None] = None,
parse_mode: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
On success, the sent Message is returned.
Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
Source https://core.telegram.org/bots/api#sendanimation
:param animation: Animation to send. Pass a file_id as String to send an animation that exists
on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation
from the Internet, or upload a new animation using multipart/form-data
:type animation: :obj:`typing.Union[base.InputFile, base.String]`
:param duration: Duration of sent animation in seconds
:type duration: :obj:`typing.Union[base.Integer, None]`
:param width: Animation width
:type width: :obj:`typing.Union[base.Integer, None]`
:param height: Animation height
:type height: :obj:`typing.Union[base.Integer, None]`
:param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnails width and height should not exceed 90.
:type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
:param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters
:type caption: :obj:`typing.Union[base.String, None]`
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
fixed-width text or inline URLs in the media caption
:type parse_mode: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove, types.ForceReply], None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_animation" method will be removed in 2.2 version.\n'
'Use "Message.reply_animation" instead.',
stacklevel=8)
return await self.bot.send_animation(self.chat.id,
animation=animation,
duration=duration,
width=width,
height=height,
thumb=thumb,
caption=caption,
parse_mode=parse_mode,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
@ -1323,55 +1275,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def send_venue(self,
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
foursquare_id: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send information about a venue.
Source: https://core.telegram.org/bots/api#sendvenue
:param latitude: Latitude of the venue
:type latitude: :obj:`base.Float`
:param longitude: Longitude of the venue
:type longitude: :obj:`base.Float`
:param title: Name of the venue
:type title: :obj:`base.String`
:param address: Address of the venue
:type address: :obj:`base.String`
:param foursquare_id: Foursquare identifier of the venue
:type foursquare_id: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_venue" method will be removed in 2.2 version.\n'
'Use "Message.reply_venue" instead.',
stacklevel=8)
return await self.bot.send_venue(chat_id=self.chat.id,
latitude=latitude,
longitude=longitude,
title=title,
address=address,
foursquare_id=foursquare_id,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_venue(self,
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
@ -1417,46 +1320,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def send_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send phone contacts.
Source: https://core.telegram.org/bots/api#sendcontact
:param phone_number: Contact's phone number
:type phone_number: :obj:`base.String`
:param first_name: Contact's first name
:type first_name: :obj:`base.String`
:param last_name: Contact's last name
:type last_name: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_contact" method will be removed in 2.2 version.\n'
'Use "Message.reply_contact" instead.',
stacklevel=8)
return await self.bot.send_contact(chat_id=self.chat.id,
phone_number=phone_number,
first_name=first_name, last_name=last_name,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
@ -1523,6 +1386,38 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_dice(self, emoji: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send a dice, which will have a random value from 1 to 6.
On success, the sent Message is returned.
(Yes, we're aware of the “proper” singular of die.
But it's awkward, and we decided to help it change. One dice at a time!)
Source: https://core.telegram.org/bots/api#senddice
:param emoji: Emoji on which the dice throw animation is based. Currently, must be one of 🎲 or 🎯. Defauts to 🎲
:type emoji: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
return await self.bot.send_dice(chat_id=self.chat.id,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def forward(self, chat_id: typing.Union[base.Integer, base.String],
disable_notification: typing.Union[base.Boolean, None] = None) -> Message:
"""
@ -1719,6 +1614,101 @@ class Message(base.TelegramObject):
"""
return await self.chat.pin_message(self.message_id, disable_notification)
async def send_copy(
self: Message,
chat_id: typing.Union[str, int],
disable_notification: typing.Optional[bool] = None,
reply_to_message_id: typing.Optional[int] = None,
reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None,
) -> Message:
"""
Send copy of current message
:param chat_id:
:param disable_notification:
:param reply_to_message_id:
:param reply_markup:
:return:
"""
kwargs = {
"chat_id": chat_id,
"reply_markup": reply_markup or self.reply_markup,
"parse_mode": ParseMode.HTML,
"disable_notification": disable_notification,
"reply_to_message_id": reply_to_message_id,
}
text = self.html_text if (self.text or self.caption) else None
if self.text:
return await self.bot.send_message(text=text, **kwargs)
elif self.audio:
return await self.bot.send_audio(
audio=self.audio.file_id,
caption=text,
title=self.audio.title,
performer=self.audio.performer,
duration=self.audio.duration,
**kwargs
)
elif self.animation:
return await self.bot.send_animation(
animation=self.animation.file_id, caption=text, **kwargs
)
elif self.document:
return await self.bot.send_document(
document=self.document.file_id, caption=text, **kwargs
)
elif self.photo:
return await self.bot.send_photo(
photo=self.photo[-1].file_id, caption=text, **kwargs
)
elif self.sticker:
kwargs.pop("parse_mode")
return await self.bot.send_sticker(sticker=self.sticker.file_id, **kwargs)
elif self.video:
return await self.bot.send_video(
video=self.video.file_id, caption=text, **kwargs
)
elif self.video_note:
kwargs.pop("parse_mode")
return await self.bot.send_video_note(
video_note=self.video_note.file_id, **kwargs
)
elif self.voice:
return await self.bot.send_voice(voice=self.voice.file_id, **kwargs)
elif self.contact:
kwargs.pop("parse_mode")
return await self.bot.send_contact(
phone_number=self.contact.phone_number,
first_name=self.contact.first_name,
last_name=self.contact.last_name,
vcard=self.contact.vcard,
**kwargs
)
elif self.venue:
kwargs.pop("parse_mode")
return await self.bot.send_venue(
latitude=self.venue.location.latitude,
longitude=self.venue.location.longitude,
title=self.venue.title,
address=self.venue.address,
foursquare_id=self.venue.foursquare_id,
foursquare_type=self.venue.foursquare_type,
**kwargs
)
elif self.location:
kwargs.pop("parse_mode")
return await self.bot.send_location(
latitude=self.location.latitude, longitude=self.location.longitude, **kwargs
)
elif self.poll:
kwargs.pop("parse_mode")
return await self.bot.send_poll(
question=self.poll.question, options=[option.text for option in self.poll.options], **kwargs
)
else:
raise TypeError("This type of message can't be copied.")
def __int__(self):
return self.message_id
@ -1766,6 +1756,8 @@ class ContentType(helper.Helper):
CONTACT = helper.Item() # contact
LOCATION = helper.Item() # location
VENUE = helper.Item() # venue
POLL = helper.Item() # poll
DICE = helper.Item() # dice
NEW_CHAT_MEMBERS = helper.Item() # new_chat_member
LEFT_CHAT_MEMBER = helper.Item() # left_chat_member
INVOICE = helper.Item() # invoice
@ -1779,7 +1771,6 @@ class ContentType(helper.Helper):
DELETE_CHAT_PHOTO = helper.Item() # delete_chat_photo
GROUP_CHAT_CREATED = helper.Item() # group_chat_created
PASSPORT_DATA = helper.Item() # passport_data
POLL = helper.Item()
UNKNOWN = helper.Item() # unknown
ANY = helper.Item() # any
@ -1858,4 +1849,5 @@ class ParseMode(helper.Helper):
mode = helper.HelperMode.lowercase
MARKDOWN = helper.Item()
MARKDOWN_V2 = helper.Item()
HTML = helper.Item()

View file

@ -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
@ -49,31 +52,26 @@ class MessageEntity(base.TelegramObject):
entity_text = self.get_text(text)
if self.type == MessageEntityType.BOLD:
if as_html:
return markdown.hbold(entity_text)
return markdown.bold(entity_text)
elif self.type == MessageEntityType.ITALIC:
if as_html:
return markdown.hitalic(entity_text)
return markdown.italic(entity_text)
elif self.type == MessageEntityType.PRE:
if as_html:
return markdown.hpre(entity_text)
return markdown.pre(entity_text)
elif self.type == MessageEntityType.CODE:
if as_html:
return markdown.hcode(entity_text)
return markdown.code(entity_text)
elif self.type == MessageEntityType.URL:
if as_html:
return markdown.hlink(entity_text, entity_text)
return markdown.link(entity_text, entity_text)
elif self.type == MessageEntityType.TEXT_LINK:
if as_html:
return markdown.hlink(entity_text, self.url)
return markdown.link(entity_text, self.url)
elif self.type == MessageEntityType.TEXT_MENTION and self.user:
return self.user.get_mention(entity_text)
method = markdown.hbold if as_html else markdown.bold
return method(entity_text)
if self.type == MessageEntityType.ITALIC:
method = markdown.hitalic if as_html else markdown.italic
return method(entity_text)
if self.type == MessageEntityType.PRE:
method = markdown.hpre if as_html else markdown.pre
return method(entity_text)
if self.type == MessageEntityType.CODE:
method = markdown.hcode if as_html else markdown.code
return method(entity_text)
if self.type == MessageEntityType.URL:
method = markdown.hlink if as_html else markdown.link
return method(entity_text, entity_text)
if self.type == MessageEntityType.TEXT_LINK:
method = markdown.hlink if as_html else markdown.link
return method(entity_text, self.url)
if self.type == MessageEntityType.TEXT_MENTION and self.user:
return self.user.get_mention(entity_text, as_html=as_html)
return entity_text
@ -92,6 +90,8 @@ class MessageEntityType(helper.Helper):
:key: ITALIC
:key: CODE
:key: PRE
:key: UNDERLINE
:key: STRIKETHROUGH
:key: TEXT_LINK
:key: TEXT_MENTION
"""
@ -106,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

View file

@ -24,7 +24,7 @@ class Downloadable:
if destination is None:
destination = file.file_path
elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination):
os.path.join(destination, file.file_path)
destination = os.path.join(destination, file.file_path)
else:
is_path = False

View file

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

View file

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

View file

@ -1,16 +1,83 @@
import datetime
import typing
from . import base
from . import fields
from . import base, fields
from .message_entity import MessageEntity
from .user import User
from ..utils import helper
from ..utils.text_decorations import html_decoration, markdown_decoration
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()
explanation: base.String = fields.Field()
explanation_entities: base.String = fields.ListField(base=MessageEntity)
open_period: base.Integer = fields.Field()
close_date: datetime.datetime = fields.DateTimeField()
def parse_entities(self, as_html=True):
text_decorator = html_decoration if as_html else markdown_decoration
return text_decorator.unparse(self.explanation or '', self.explanation_entities or [])
@property
def md_explanation(self) -> str:
"""
Explanation formatted as markdown.
:return: str
"""
return self.parse_entities(False)
@property
def html_explanation(self) -> str:
"""
Explanation formatted as HTML
:return: str
"""
return self.parse_entities()
class PollType(helper.Helper):
mode = helper.HelperMode.snake_case
REGULAR = helper.Item()
QUIZ = helper.Item()

View file

@ -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,31 @@ 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,
**kwargs):
super(KeyboardButton, self).__init__(text=text,
request_contact=request_contact,
request_location=request_location)
request_location=request_location,
request_poll=request_poll,
**kwargs)
class ReplyKeyboardRemove(base.TelegramObject):

View file

@ -12,10 +12,38 @@ 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()
thumb: PhotoSize = fields.Field(base=PhotoSize)
emoji: base.String = fields.Field()
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)

View file

@ -2,6 +2,7 @@ import typing
from . import base
from . import fields
from .photo_size import PhotoSize
from .sticker import Sticker
@ -13,5 +14,7 @@ class StickerSet(base.TelegramObject):
"""
name: base.String = fields.Field()
title: base.String = fields.Field()
is_animated: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field()
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
thumb: PhotoSize = fields.Field(base=PhotoSize)

View file

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

View file

@ -1,10 +1,13 @@
from __future__ import annotations
from typing import Optional
import babel
from . import base
from . import fields
from ..utils import markdown
from ..utils.deprecated import deprecated
class User(base.TelegramObject):
@ -19,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):
@ -45,7 +51,7 @@ class User(base.TelegramObject):
return self.full_name
@property
def locale(self) -> babel.core.Locale or None:
def locale(self) -> Optional[babel.core.Locale]:
"""
Get user's locale
@ -71,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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,10 @@ import collections
import hashlib
import hmac
from aiogram.utils.deprecated import deprecated
@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`', stacklevel=3)
def generate_hash(data: dict, token: str) -> str:
"""
Generate secret hash
@ -24,6 +27,7 @@ def generate_hash(data: dict, token: str) -> str:
return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
@deprecated('`check_token` helper was renamed to `check_integrity`', stacklevel=3)
def check_token(data: dict, token: str) -> bool:
"""
Validate auth token
@ -34,3 +38,32 @@ def check_token(data: dict, token: str) -> bool:
"""
param_hash = data.get('hash', '') or ''
return param_hash == generate_hash(data, token)
def check_signature(token: str, hash: str, **kwargs) -> bool:
"""
Generate hexadecimal representation
of the HMAC-SHA-256 signature of the data-check-string
with the SHA256 hash of the bot's token used as a secret key
:param token:
:param hash:
:param kwargs: all params received on auth
:return:
"""
secret = hashlib.sha256(token.encode('utf-8'))
check_string = '\n'.join(map(lambda k: f'{k}={kwargs[k]}', sorted(kwargs)))
hmac_string = hmac.new(secret.digest(), check_string.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
return hmac_string == hash
def check_integrity(token: str, data: dict) -> bool:
"""
Verify the authentication and the integrity
of the data received on user's auth
:param token: Bot's token
:param data: all data that came on auth
:return:
"""
return check_signature(token, **data)

View file

@ -28,13 +28,13 @@ class CallbackData:
def __init__(self, prefix, *parts, sep=':'):
if not isinstance(prefix, str):
raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}")
elif not prefix:
raise ValueError('Prefix can\'t be empty')
elif sep in prefix:
raise ValueError(f"Separator '{sep}' can't be used in prefix")
elif not parts:
raise TypeError('Parts is not passed!')
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
if not prefix:
raise ValueError("Prefix can't be empty")
if sep in prefix:
raise ValueError(f"Separator {sep!r} can't be used in prefix")
if not parts:
raise TypeError('Parts were not passed!')
self.prefix = prefix
self.sep = sep
@ -59,23 +59,23 @@ class CallbackData:
if args:
value = args.pop(0)
else:
raise ValueError(f"Value for '{part}' is not passed!")
raise ValueError(f'Value for {part!r} was not passed!')
if value is not None and not isinstance(value, str):
value = str(value)
if not value:
raise ValueError(f"Value for part {part} can't be empty!'")
elif self.sep in value:
raise ValueError(f"Symbol defined as separator can't be used in values of parts")
raise ValueError(f"Value for part {part!r} can't be empty!'")
if self.sep in value:
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
data.append(value)
if args or kwargs:
raise TypeError('Too many arguments is passed!')
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
@ -106,30 +106,31 @@ class CallbackData:
"""
for key in config.keys():
if key not in self._part_names:
raise ValueError(f"Invalid field name '{key}'")
raise ValueError(f'Invalid field name {key!r}')
return CallbackDataFilter(self, config)
class CallbackDataFilter(Filter):
def __init__(self, factory: CallbackData, config: typing.Dict[str, str]):
self.config = config
self.factory = factory
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]):
raise ValueError('That filter can\'t be used in filters factory!')
raise ValueError("That filter can't be used in filters factory!")
async def check(self, query: types.CallbackQuery):
try:
data = self.factory.parse(query.data)
except ValueError:
return False
else:
for key, value in self.config.items():
if isinstance(value, (list, tuple, set)):
if data.get(key) not in value:
return False
else:
if value != data.get(key):
return False
return {'callback_data': data}
for key, value in self.config.items():
if isinstance(value, (list, tuple, set, frozenset)):
if data.get(key) not in value:
return False
else:
if data.get(key) != value:
return False
return {'callback_data': data}

View 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

View file

@ -1,17 +1,17 @@
"""
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
"""
import functools
import asyncio
import inspect
import warnings
import functools
from typing import Callable
def deprecated(reason):
def deprecated(reason, stacklevel=2) -> Callable:
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
"""
if isinstance(reason, str):
@ -33,7 +33,7 @@ def deprecated(reason):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warn_deprecated(msg.format(name=func.__name__, reason=reason))
warn_deprecated(msg.format(name=func.__name__, reason=reason), stacklevel=stacklevel)
warnings.simplefilter('default', DeprecationWarning)
return func(*args, **kwargs)
@ -41,7 +41,7 @@ def deprecated(reason):
return decorator
elif inspect.isclass(reason) or inspect.isfunction(reason):
if inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'.
#
@ -60,16 +60,72 @@ def deprecated(reason):
@functools.wraps(func1)
def wrapper1(*args, **kwargs):
warn_deprecated(msg1.format(name=func1.__name__))
warn_deprecated(msg1.format(name=func1.__name__), stacklevel=stacklevel)
return func1(*args, **kwargs)
return wrapper1
else:
raise TypeError(repr(type(reason)))
raise TypeError(repr(type(reason)))
def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2):
warnings.simplefilter('always', warning)
warnings.warn(message, category=warning, stacklevel=stacklevel)
warnings.simplefilter('default', warning)
def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3):
"""
A meta-decorator to mark an argument as deprecated.
.. code-block:: python3
@renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default
@renamed_argument("user", "user_id", "3.0", stacklevel=4)
def some_function(user_id, chat_id=None):
print(f"user_id={user_id}, chat_id={chat_id}")
some_function(user=123) # prints 'user_id=123, chat_id=None' with warning
some_function(123) # prints 'user_id=123, chat_id=None' without warning
some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning
:param old_name:
:param new_name:
:param until_version: the version in which the argument is scheduled to be removed
:param stacklevel: leave it to default if it's the first decorator used.
Increment with any new decorator used.
:return: decorator
"""
def decorator(func):
is_coroutine = asyncio.iscoroutinefunction(func)
def _handling(kwargs):
"""
Returns updated version of kwargs.
"""
routine_type = 'coroutine' if is_coroutine else 'function'
if 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
if is_coroutine:
@functools.wraps(func)
async def wrapped(*args, **kwargs):
kwargs = _handling(kwargs)
return await func(*args, **kwargs)
else:
@functools.wraps(func)
def wrapped(*args, **kwargs):
kwargs = _handling(kwargs)
return func(*args, **kwargs)
return wrapped
return decorator

View file

@ -1,93 +1,93 @@
"""
TelegramAPIError
ValidationError
Throttled
BadRequest
MessageError
MessageNotModified
MessageToForwardNotFound
MessageToDeleteNotFound
MessageIdentifierNotSpecified
MessageTextIsEmpty
MessageCantBeEdited
MessageCantBeDeleted
MessageToEditNotFound
ToMuchMessages
PollError
PollCantBeStopped
PollHasAlreadyClosed
PollsCantBeSentToPrivateChats
PollSizeError
PollMustHaveMoreOptions
PollCantHaveMoreOptions
PollsOptionsLengthTooLong
PollOptionsMustBeNonEmpty
PollQuestionMustBeNonEmpty
MessageWithPollNotFound (with MessageError)
MessageIsNotAPoll (with MessageError)
ObjectExpectedAsReplyMarkup
InlineKeyboardExpected
ChatNotFound
ChatDescriptionIsNotModified
InvalidQueryID
InvalidPeerID
InvalidHTTPUrlContent
ButtonURLInvalid
URLHostIsEmpty
StartParamInvalid
ButtonDataInvalid
WrongFileIdentifier
GroupDeactivated
BadWebhook
WebhookRequireHTTPS
BadWebhookPort
BadWebhookAddrInfo
BadWebhookNoAddressAssociatedWithHostname
NotFound
MethodNotKnown
PhotoAsInputFileRequired
InvalidStickersSet
NoStickerInRequest
ChatAdminRequired
NeedAdministratorRightsInTheChannel
MethodNotAvailableInPrivateChats
CantDemoteChatCreator
CantRestrictSelf
NotEnoughRightsToRestrict
PhotoDimensions
UnavailableMembers
TypeOfFileMismatch
WrongRemoteFileIdSpecified
PaymentProviderInvalid
CurrencyTotalAmountInvalid
CantParseUrl
UnsupportedUrlProtocol
CantParseEntities
ResultIdDuplicate
ConflictError
TerminatedByOtherGetUpdates
CantGetUpdates
Unauthorized
BotKicked
BotBlocked
UserDeactivated
CantInitiateConversation
CantTalkWithBots
NetworkError
RetryAfter
MigrateToChat
RestartingTelegram
- TelegramAPIError
- ValidationError
- Throttled
- BadRequest
- MessageError
- MessageNotModified
- MessageToForwardNotFound
- MessageToDeleteNotFound
- MessageIdentifierNotSpecified
- MessageTextIsEmpty
- MessageCantBeEdited
- MessageCantBeDeleted
- MessageToEditNotFound
- MessageToReplyNotFound
- ToMuchMessages
- PollError
- PollCantBeStopped
- PollHasAlreadyClosed
- PollsCantBeSentToPrivateChats
- PollSizeError
- PollMustHaveMoreOptions
- PollCantHaveMoreOptions
- PollsOptionsLengthTooLong
- PollOptionsMustBeNonEmpty
- PollQuestionMustBeNonEmpty
- MessageWithPollNotFound (with MessageError)
- MessageIsNotAPoll (with MessageError)
- ObjectExpectedAsReplyMarkup
- InlineKeyboardExpected
- ChatNotFound
- ChatDescriptionIsNotModified
- InvalidQueryID
- InvalidPeerID
- InvalidHTTPUrlContent
- ButtonURLInvalid
- URLHostIsEmpty
- StartParamInvalid
- ButtonDataInvalid
- WrongFileIdentifier
- GroupDeactivated
- BadWebhook
- WebhookRequireHTTPS
- BadWebhookPort
- BadWebhookAddrInfo
- BadWebhookNoAddressAssociatedWithHostname
- NotFound
- MethodNotKnown
- PhotoAsInputFileRequired
- InvalidStickersSet
- NoStickerInRequest
- ChatAdminRequired
- NeedAdministratorRightsInTheChannel
- MethodNotAvailableInPrivateChats
- CantDemoteChatCreator
- CantRestrictSelf
- NotEnoughRightsToRestrict
- PhotoDimensions
- UnavailableMembers
- TypeOfFileMismatch
- WrongRemoteFileIdSpecified
- PaymentProviderInvalid
- CurrencyTotalAmountInvalid
- CantParseUrl
- UnsupportedUrlProtocol
- CantParseEntities
- ResultIdDuplicate
- MethodIsNotAvailable
- ConflictError
- TerminatedByOtherGetUpdates
- CantGetUpdates
- Unauthorized
- BotKicked
- BotBlocked
- UserDeactivated
- CantInitiateConversation
- CantTalkWithBots
- NetworkError
- RetryAfter
- MigrateToChat
- RestartingTelegram
TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
AIOGramWarning
TimeoutWarning
- AIOGramWarning
- TimeoutWarning
"""
import time
# TODO: Use exceptions detector from `aiograph`.
# TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
# TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: ']
@ -182,6 +182,13 @@ class MessageToDeleteNotFound(MessageError):
match = 'message to delete not found'
class MessageToReplyNotFound(MessageError):
"""
Will be raised when you try to reply to very old or deleted or unknown message.
"""
match = 'message to reply not found'
class MessageIdentifierNotSpecified(MessageError):
match = 'message identifier is not specified'
@ -265,6 +272,10 @@ class PollQuestionLengthTooLong(PollSizeError):
match = "poll question length must not exceed 255"
class PollCanBeRequestedInPrivateChatsOnly(PollError):
match = "Poll can be requested in private chats only"
class MessageWithPollNotFound(PollError, MessageError):
"""
Will be raised when you try to stop poll with message without poll
@ -297,8 +308,7 @@ class ChatDescriptionIsNotModified(BadRequest):
class InvalidQueryID(BadRequest):
match = 'QUERY_ID_INVALID'
text = 'Invalid query ID'
match = 'query is too old and response timeout expired or query id is invalid'
class InvalidPeerID(BadRequest):
@ -456,6 +466,10 @@ class BotDomainInvalid(BadRequest):
text = 'Invalid bot domain'
class MethodIsNotAvailable(BadRequest):
match = "Method is available only for supergroups"
class NotFound(TelegramAPIError, _MatchErrorMixin):
__group = True
@ -483,7 +497,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
class BotKicked(Unauthorized):
match = 'Bot was kicked from a chat'
match = 'bot was kicked from a chat'
class BotBlocked(Unauthorized):

View file

@ -15,7 +15,7 @@ from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, Webhook
APP_EXECUTOR_KEY = 'APP_EXECUTOR'
def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
if on_startup is not None:
executor.on_startup(on_startup)
if on_shutdown is not None:
@ -23,7 +23,7 @@ def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
on_startup=None, on_shutdown=None, timeout=20, fast=True):
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@ -38,7 +38,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
@ -291,7 +291,7 @@ class Executor:
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
self.run_app(**kwargs)
def start_polling(self, reset_webhook=None, timeout=20, fast=True):
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@ -303,7 +303,8 @@ class Executor:
try:
loop.run_until_complete(self._startup_polling())
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast))
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
relax=relax, fast=fast))
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
# loop.stop()
@ -339,7 +340,7 @@ class Executor:
async def _skip_updates(self):
await self.dispatcher.reset_webhook(True)
await self.dispatcher.skip_updates()
log.warning(f"Updates are skipped successfully.")
log.warning(f'Updates were skipped successfully.')
async def _welcome(self):
user = await self.dispatcher.bot.me
@ -360,11 +361,11 @@ class Executor:
await callback(self.dispatcher)
async def _shutdown_polling(self, wait_closed=False):
await self._shutdown()
for callback in self._on_shutdown_polling:
await callback(self.dispatcher)
await self._shutdown()
if wait_closed:
await self.dispatcher.wait_closed()

View file

@ -13,6 +13,9 @@ Example:
>>> print(MyHelper.all())
<<< ['barItem', 'bazItem', 'fooItem', 'lorem']
"""
from typing import List
PROPS_KEYS_ATTR_NAME = '_props_keys'
class Helper:
@ -120,15 +123,15 @@ class HelperMode(Helper):
"""
if mode == cls.SCREAMING_SNAKE_CASE:
return cls._screaming_snake_case(text)
elif mode == cls.snake_case:
if mode == cls.snake_case:
return cls._snake_case(text)
elif mode == cls.lowercase:
if mode == cls.lowercase:
return cls._snake_case(text).replace('_', '')
elif mode == cls.lowerCamelCase:
if mode == cls.lowerCamelCase:
return cls._camel_case(text)
elif mode == cls.CamelCase:
if mode == cls.CamelCase:
return cls._camel_case(text, True)
elif callable(mode):
if callable(mode):
return mode(text)
return text
@ -191,3 +194,36 @@ class ItemsList(list):
return self
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
class OrderedHelperMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super().__new__(mcs, name, bases, namespace)
props_keys = []
for prop_name in (name for name, prop in namespace.items() if isinstance(prop, (Item, ListItem))):
props_keys.append(prop_name)
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
return cls
class OrderedHelper(metaclass=OrderedHelperMeta):
mode = ''
@classmethod
def all(cls) -> List[str]:
"""
Get all Items values
"""
result = []
for name in getattr(cls, PROPS_KEYS_ATTR_NAME, []):
value = getattr(cls, name)
if isinstance(value, ItemsList):
result.append(value[0])
else:
result.append(value)
return result

View file

@ -21,13 +21,11 @@ for json_lib in (RAPIDJSON, UJSON):
if mode == RAPIDJSON:
def dumps(data):
return json.dumps(data, ensure_ascii=False, number_mode=json.NM_NATIVE,
datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC)
return json.dumps(data, ensure_ascii=False)
def loads(data):
return json.loads(data, number_mode=json.NM_NATIVE,
datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC)
return json.loads(data, number_mode=json.NM_NATIVE)
elif mode == UJSON:
def loads(data):

View file

@ -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 = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
}
HTML_QUOTES_MAP = {"<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;"}
_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=" ") -> str:
"""
Quote HTML symbols
@ -44,16 +26,31 @@ def quote_html(content):
an HTML entity must be replaced with the corresponding HTML entities
(< with &lt; > with &gt; & with &amp and " with &quot).
: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=" ") -> str:
"""
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=" ") -> str:
"""
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(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hbold(*content, sep=' '):
def hbold(*content, sep=" ") -> str:
"""
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(
value=html_decoration.quote(_join(*content, sep=sep))
)
def italic(*content, sep=' '):
def italic(*content, sep=" ") -> str:
"""
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(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hitalic(*content, sep=' '):
def hitalic(*content, sep=" ") -> str:
"""
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(
value=html_decoration.quote(_join(*content, sep=sep))
)
def code(*content, sep=' '):
def code(*content, sep=" ") -> str:
"""
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(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hcode(*content, sep=' '):
def hcode(*content, sep=" ") -> str:
"""
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(
value=html_decoration.quote(_join(*content, sep=sep))
)
def pre(*content, sep='\n'):
def pre(*content, sep="\n") -> str:
"""
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(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hpre(*content, sep='\n'):
def hpre(*content, sep="\n") -> str:
"""
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(
value=html_decoration.quote(_join(*content, sep=sep))
)
def link(title, url):
def underline(*content, sep=" ") -> str:
"""
Make underlined text (Markdown)
:param content:
:param sep:
:return:
"""
return markdown_decoration.underline(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hunderline(*content, sep=" ") -> str:
"""
Make underlined text (HTML)
:param content:
:param sep:
:return:
"""
return html_decoration.underline(
value=html_decoration.quote(_join(*content, sep=sep))
)
def strikethrough(*content, sep=" ") -> str:
"""
Make strikethrough text (Markdown)
:param content:
:param sep:
:return:
"""
return markdown_decoration.strikethrough(
value=markdown_decoration.quote(_join(*content, sep=sep))
)
def hstrikethrough(*content, sep=" ") -> str:
"""
Make strikethrough text (HTML)
:param content:
:param sep:
:return:
"""
return html_decoration.strikethrough(
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(value=markdown_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(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

View file

@ -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)
@ -31,7 +34,7 @@ T = TypeVar('T')
class ContextInstanceMixin:
def __init_subclass__(cls, **kwargs):
cls.__context_instance = contextvars.ContextVar('instance_' + cls.__name__)
cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}')
return cls
@classmethod
@ -43,5 +46,5 @@ class ContextInstanceMixin:
@classmethod
def set_current(cls: Type[T], value: T):
if not isinstance(value, cls):
raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'")
raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}')
cls.__context_instance.set(value)

View file

@ -52,14 +52,14 @@ def prepare_arg(value):
"""
if value is None:
return value
elif isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
if isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
return json.dumps(_normalize(value))
elif isinstance(value, datetime.timedelta):
if isinstance(value, datetime.timedelta):
now = datetime.datetime.now()
return int((now + value).timestamp())
elif isinstance(value, datetime.datetime):
if isinstance(value, datetime.datetime):
return round(value.timestamp())
elif isinstance(value, LazyProxy):
if isinstance(value, LazyProxy):
return str(value)
return value

View file

@ -0,0 +1,197 @@
from __future__ import annotations
import html
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast
if TYPE_CHECKING: # pragma: no cover
from aiogram.types import MessageEntity
__all__ = (
"TextDecoration",
"HtmlDecoration",
"MarkdownDecoration",
"html_decoration",
"markdown_decoration",
)
class TextDecoration(ABC):
def apply_entity(self, entity: MessageEntity, text: str) -> str:
"""
Apply single entity to text
:param entity:
:param text:
:return:
"""
if entity.type in {"bot_command", "url", "mention", "phone_number"}:
# This entities should not be changed
return text
if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}:
return cast(str, getattr(self, entity.type)(value=text))
if entity.type == "pre":
return (
self.pre_language(value=text, language=entity.language)
if entity.language
else self.pre(value=text)
)
if entity.type == "text_mention":
from aiogram.types import User
user = cast(User, entity.user)
return self.link(value=text, link=f"tg://user?id={user.id}")
if entity.type == "text_link":
return self.link(value=text, link=cast(str, entity.url))
return self.quote(text)
def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:
"""
Unparse message entities
:param text: raw text
:param entities: Array of MessageEntities
:return:
"""
result = "".join(
self._unparse_entities(
text, sorted(entities, key=lambda item: item.offset) if entities else []
)
)
return result
def _unparse_entities(
self,
text: str,
entities: List[MessageEntity],
offset: Optional[int] = None,
length: Optional[int] = None,
) -> Generator[str, None, None]:
if offset is None:
offset = 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 or 0), 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])
@abstractmethod
def link(self, value: str, link: str) -> str: # pragma: no cover
pass
@abstractmethod
def bold(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def italic(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def code(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def pre(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def pre_language(self, value: str, language: str) -> str: # pragma: no cover
pass
@abstractmethod
def underline(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def strikethrough(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def quote(self, value: str) -> str: # pragma: no cover
pass
class HtmlDecoration(TextDecoration):
def link(self, value: str, link: str) -> str:
return f'<a href="{link}">{value}</a>'
def bold(self, value: str) -> str:
return f"<b>{value}</b>"
def italic(self, value: str) -> str:
return f"<i>{value}</i>"
def code(self, value: str) -> str:
return f"<code>{value}</code>"
def pre(self, value: str) -> str:
return f"<pre>{value}</pre>"
def pre_language(self, value: str, language: str) -> str:
return f'<pre><code class="language-{language}">{value}</code></pre>'
def underline(self, value: str) -> str:
return f"<u>{value}</u>"
def strikethrough(self, value: str) -> str:
return f"<s>{value}</s>"
def quote(self, value: str) -> str:
return html.escape(value)
class MarkdownDecoration(TextDecoration):
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])")
def link(self, value: str, link: str) -> str:
return f"[{value}]({link})"
def bold(self, value: str) -> str:
return f"*{value}*"
def italic(self, value: str) -> str:
return f"_{value}_\r"
def code(self, value: str) -> str:
return f"`{value}`"
def pre(self, value: str) -> str:
return f"```{value}```"
def pre_language(self, value: str, language: str) -> str:
return f"```{language}\n{value}\n```"
def underline(self, value: str) -> str:
return f"__{value}__"
def strikethrough(self, value: str) -> str:
return f"~{value}~"
def quote(self, value: str) -> str:
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
html_decoration = HtmlDecoration()
markdown_decoration = MarkdownDecoration()

View file

@ -3,7 +3,7 @@
ujson>=1.35
python-rapidjson>=0.7.0
emoji>=0.5.2
pytest>=4.4.1
pytest>=5.4
pytest-asyncio>=0.10.0
tox>=3.9.0
aresponses>=1.1.1
@ -13,5 +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

View file

@ -94,6 +94,12 @@ ContentTypeFilter
:members:
:show-inheritance:
IsSenderContact
---------------
.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact
:members:
:show-inheritance:
StateFilter
-----------
@ -111,6 +117,38 @@ ExceptionsFilter
:show-inheritance:
IDFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter
:members:
:show-inheritance:
AdminFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter
:members:
:show-inheritance:
IsReplyFilter
-------------
.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter
:members:
:show-inheritance:
ForwardedMessageFilter
-------------
.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter
:members:
:show-inheritance:
Making own filters (Custom filters)
===================================
@ -156,3 +194,4 @@ BoundFilter
dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers])

View file

@ -1,28 +1,28 @@
.. Autogenerated file at 2018-10-28 19:31:48.335963
=========================
Adwanced executor example
Advanced executor example
=========================
!/usr/bin/env python3
**This example is outdated**
In this example used ArgumentParser for configuring Your bot.
Provided to start bot with webhook:
python adwanced_executor_example.py \
python advanced_executor_example.py \
--token TOKEN_HERE \
--host 0.0.0.0 \
--port 8084 \
--host-name example.com \
--webhook-port 443
Or long polling:
python adwanced_executor_example.py --token TOKEN_HERE
python advanced_executor_example.py --token TOKEN_HERE
So... In this example found small trouble:
can't get bot instance in handlers.
If you want to automatic change getting updates method use executor utils (from aiogram.utils.executor)
TODO: Move token to environment variables.
.. literalinclude:: ../../../examples/adwanced_executor_example.py
:caption: adwanced_executor_example.py
.. literalinclude:: ../../../examples/advanced_executor_example.py
:caption: advanced_executor_example.py
:language: python
:linenos:
:lines: 25-

View file

@ -6,7 +6,7 @@ Examples
echo_bot
inline_bot
adwanced_executor_example
advanced_executor_example
proxy_and_emojize
finite_state_machine_example
throtling_example

View file

@ -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.3-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
.. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
: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
@ -39,7 +39,7 @@ Welcome to aiogram's documentation!
:alt: MIT License
**aiogram** is a pretty simple and fully asynchronous library 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.
**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.
Official aiogram resources
@ -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>`_

View file

@ -7,21 +7,34 @@ Using PIP
$ pip install -U aiogram
Using Pipenv
------------
.. code-block:: bash
$ pipenv install aiogram
Using AUR
---------
*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 the `python-aiogram <https://aur.archlinux.org/packages/python-aiogram/>`_ package.
From sources
------------
Development versions:
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
$ python setup.py install
or if you want to install development version (maybe unstable):
Or if you want to install stable version (The same with version form PyPi):
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
$ git checkout dev-2.x
$ git checkout master
$ python setup.py install
@ -39,7 +52,7 @@ You can speedup your bots by following next instructions:
$ pip install uvloop
- Use `ujson <https://github.com/esnme/ultrajson>`_ instead of default json module.
- Use `ujson <https://github.com/esnme/ultrajson>`_ instead of the default json module.
*UltraJSON* is an ultra fast JSON encoder and decoder written in pure C with bindings for Python 2.5+ and 3.
@ -49,4 +62,36 @@ You can speedup your bots by following next instructions:
$ pip install ujson
In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment.
- Use aiohttp speedups
- Use `cchardet <https://github.com/PyYoshi/cChardet>`_ instead of the chardet module.
*cChardet* is a high speed universal character encoding detector.
**Installation:**
.. code-block:: bash
$ pip install cchardet
- Use `aiodns <https://github.com/saghul/aiodns>`_ for speeding up DNS resolving.
*aiodns* provides a simple way for doing asynchronous DNS resolutions.
**Installation:**
.. code-block:: bash
$ pip install aiodns
- Installing speedups altogether.
The following will get you ``aiohttp`` along with ``cchardet``, ``aiodns`` and ``brotlipy`` in one bundle.
**Installation:**
.. code-block:: bash
$ pip install aiohttp[speedups]
In addition, you don't need do anything, *aiogram* automatically starts using that if it is found in your environment.

View file

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

View file

@ -1,4 +1,6 @@
===========
Auth Widget
===========
Coming soon...
.. automodule:: aiogram.utils.auth_widget
:members:

View file

@ -1,4 +0,0 @@
=======
Context
=======
Coming soon...

View file

@ -0,0 +1,6 @@
============
Deep linking
============
.. automodule:: aiogram.utils.deep_linking
:members:

View file

@ -1,4 +1,6 @@
==========
Deprecated
==========
Coming soon...
.. automodule:: aiogram.utils.deprecated
:members:

View file

@ -1,4 +1,6 @@
=====
Emoji
=====
Coming soon...
.. automodule:: aiogram.utils.emoji
:members:

View file

@ -1,4 +1,6 @@
==========
Exceptions
==========
Coming soon...
.. automodule:: aiogram.utils.exceptions
:members:

View file

@ -1,4 +1,7 @@
========
Executor
========
Coming soon...
.. automodule:: aiogram.utils.executor
:members:

View file

@ -1,4 +1,6 @@
======
Helper
======
Coming soon...
.. automodule:: aiogram.utils.helper
:members:

View file

@ -3,14 +3,13 @@ Utils
.. toctree::
auth_widget
executor
exceptions
context
markdown
helper
auth_widget
deprecated
payload
parts
json
emoji
deprecated

View file

@ -1,4 +1,6 @@
====
JSON
====
Coming soon...
.. automodule:: aiogram.utils.json
:members:

View file

@ -1,4 +1,6 @@
========
Markdown
========
Coming soon...
.. automodule:: aiogram.utils.markdown
:members:

View file

@ -1,4 +1,6 @@
=====
Parts
=====
Coming soon...
.. automodule:: aiogram.utils.parts
:members:

View file

@ -1,4 +1,6 @@
=======
Payload
=======
Coming soon...
.. automodule:: aiogram.utils.payload
:members:

View file

@ -0,0 +1,33 @@
import logging
from aiogram import Bot, Dispatcher, types, executor
API_TOKEN = 'API_TOKEN_HERE'
logging.basicConfig(level=logging.DEBUG)
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot=bot)
# checks specified chat
@dp.message_handler(is_chat_admin=-1001241113577)
async def handle_specified(msg: types.Message):
await msg.answer("You are an admin of the specified chat!")
# checks multiple chats
@dp.message_handler(is_chat_admin=[-1001241113577, -320463906])
async def handle_multiple(msg: types.Message):
await msg.answer("You are an admin of multiple chats!")
# checks current chat
@dp.message_handler(is_chat_admin=True)
async def handler3(msg: types.Message):
await msg.answer("You are an admin of the current chat!")
if __name__ == '__main__':
executor.start_polling(dp)

View file

@ -4,7 +4,7 @@
In this example used ArgumentParser for configuring Your bot.
Provided to start bot with webhook:
python adwanced_executor_example.py \
python advanced_executor_example.py \
--token TOKEN_HERE \
--host 0.0.0.0 \
--port 8084 \
@ -12,7 +12,7 @@ Provided to start bot with webhook:
--webhook-port 443
Or long polling:
python adwanced_executor_example.py --token TOKEN_HERE
python advanced_executor_example.py --token TOKEN_HERE
So... In this example found small trouble:
can't get bot instance in handlers.

View file

@ -9,9 +9,8 @@ API_TOKEN = 'BOT TOKEN HERE'
logging.basicConfig(level=logging.INFO)
log = logging.getLogger('broadcast')
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot, loop=loop)
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot)
def get_users():

View file

@ -1,4 +1,3 @@
import asyncio
import logging
import random
import uuid
@ -13,20 +12,20 @@ logging.basicConfig(level=logging.INFO)
API_TOKEN = 'BOT TOKEN HERE'
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
dp.middleware.setup(LoggingMiddleware())
POSTS = {
str(uuid.uuid4()): {
'title': f"Post {index}",
'title': f'Post {index}',
'body': 'Lorem ipsum dolor sit amet, '
'consectetur adipiscing elit, '
'sed do eiusmod tempor incididunt ut '
'labore et dolore magna aliqua',
'votes': random.randint(-2, 5)
'votes': random.randint(-2, 5),
} for index in range(1, 6)
}
@ -42,21 +41,24 @@ def get_keyboard() -> types.InlineKeyboardMarkup:
markup.add(
types.InlineKeyboardButton(
post['title'],
callback_data=posts_cb.new(id=post_id, action='view'))
callback_data=posts_cb.new(id=post_id, action='view')),
)
return markup
def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup):
text = f"{md.hbold(post['title'])}\n" \
f"{md.quote_html(post['body'])}\n" \
f"\n" \
f"Votes: {post['votes']}"
text = md.text(
md.hbold(post['title']),
md.quote_html(post['body']),
'', # just new empty line
f"Votes: {post['votes']}",
sep = '\n',
)
markup = types.InlineKeyboardMarkup()
markup.row(
types.InlineKeyboardButton('👍', callback_data=posts_cb.new(id=post_id, action='like')),
types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='unlike')),
types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='dislike')),
)
markup.add(types.InlineKeyboardButton('<< Back', callback_data=posts_cb.new(id='-', action='list')))
return text, markup
@ -84,7 +86,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict):
await query.message.edit_text(text, reply_markup=markup)
@dp.callback_query_handler(posts_cb.filter(action=['like', 'unlike']))
@dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike']))
async def query_post_vote(query: types.CallbackQuery, callback_data: dict):
try:
await dp.throttle('vote', rate=1)
@ -100,10 +102,10 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict):
if action == 'like':
post['votes'] += 1
elif action == 'unlike':
elif action == 'dislike':
post['votes'] -= 1
await query.answer('Voted.')
await query.answer('Vote accepted')
text, markup = format_post(post_id, post)
await query.message.edit_text(text, reply_markup=markup)
@ -114,4 +116,4 @@ async def message_not_modified_handler(update, error):
if __name__ == '__main__':
executor.start_polling(dp, loop=loop, skip_updates=True)
executor.start_polling(dp, skip_updates=True)

View file

@ -0,0 +1,68 @@
"""
This is a simple example of usage of CallbackData factory
For more comprehensive example see callback_data_factory.py
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.utils.callback_data import CallbackData
from aiogram.utils.exceptions import MessageNotModified
logging.basicConfig(level=logging.INFO)
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
dp.middleware.setup(LoggingMiddleware())
vote_cb = CallbackData('vote', 'action') # vote:<action>
likes = {} # user_id: amount_of_likes
def get_keyboard():
return types.InlineKeyboardMarkup().row(
types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')),
types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')),
)
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0
await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard())
@dp.callback_query_handler(vote_cb.filter(action=['up', 'down']))
async def callback_vote_action(query: types.CallbackQuery, callback_data: dict):
logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data
await query.answer() # don't forget to answer callback query as soon as possible
callback_data_action = callback_data['action']
likes_count = likes.get(query.from_user.id, 0)
if callback_data_action == 'up':
likes_count += 1
else:
likes_count -= 1
likes[query.from_user.id] = likes_count # update amount of likes in storage
await bot.edit_message_text(
f'You voted {callback_data_action}! Now you have {likes_count} vote[s].',
query.from_user.id,
query.message.message_id,
reply_markup=get_keyboard(),
)
@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
async def message_not_modified_handler(update, error):
return True
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -2,7 +2,6 @@
Babel is required.
"""
import asyncio
import logging
from aiogram import Bot, Dispatcher, executor, md, types
@ -11,8 +10,8 @@ API_TOKEN = 'BOT TOKEN HERE'
logging.basicConfig(level=logging.INFO)
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.MARKDOWN)
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN)
dp = Dispatcher(bot)
@ -22,12 +21,13 @@ async def check_language(message: types.Message):
await message.reply(md.text(
md.bold('Info about your language:'),
md.text(' 🔸', md.bold('Code:'), md.italic(locale.locale)),
md.text(' 🔸', md.bold('Territory:'), md.italic(locale.territory or 'Unknown')),
md.text(' 🔸', md.bold('Language name:'), md.italic(locale.language_name)),
md.text(' 🔸', md.bold('English language name:'), md.italic(locale.english_name)),
sep='\n'))
md.text('🔸', md.bold('Code:'), md.code(locale.language)),
md.text('🔸', md.bold('Territory:'), md.code(locale.territory or 'Unknown')),
md.text('🔸', md.bold('Language name:'), md.code(locale.language_name)),
md.text('🔸', md.bold('English language name:'), md.code(locale.english_name)),
sep='\n',
))
if __name__ == '__main__':
executor.start_polling(dp, loop=loop, skip_updates=True)
executor.start_polling(dp, skip_updates=True)

View file

@ -20,7 +20,7 @@ dp = Dispatcher(bot)
@dp.message_handler(commands=['start', 'help'])
async def send_welcome(message: types.Message):
"""
This handler will be called when client send `/start` or `/help` commands.
This handler will be called when user sends `/start` or `/help` command
"""
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
@ -28,13 +28,25 @@ async def send_welcome(message: types.Message):
@dp.message_handler(regexp='(^cat[s]?$|puss)')
async def cats(message: types.Message):
with open('data/cats.jpg', 'rb') as photo:
await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺',
reply_to_message_id=message.message_id)
'''
# Old fashioned way:
await bot.send_photo(
message.chat.id,
photo,
caption='Cats are here 😺',
reply_to_message_id=message.message_id,
)
'''
await message.reply_photo(photo, caption='Cats are here 😺')
@dp.message_handler()
async def echo(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# old style:
# await bot.send_message(message.chat.id, message.text)
await message.answer(message.text)
if __name__ == '__main__':

View file

@ -1,19 +1,20 @@
import asyncio
from typing import Optional
import logging
import aiogram.utils.markdown as md
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters import Text
from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram.types import ParseMode
from aiogram.utils import executor
logging.basicConfig(level=logging.INFO)
API_TOKEN = 'BOT TOKEN HERE'
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop)
bot = Bot(token=API_TOKEN)
# For example use simple MemoryStorage for Dispatcher.
storage = MemoryStorage()
@ -27,7 +28,7 @@ class Form(StatesGroup):
gender = State() # Will be represented in storage as 'Form:gender'
@dp.message_handler(commands=['start'])
@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
"""
Conversation's entry point
@ -39,19 +40,21 @@ async def cmd_start(message: types.Message):
# You can use state '*' if you need to handle all states
@dp.message_handler(state='*', commands=['cancel'])
@dp.message_handler(lambda message: message.text.lower() == 'cancel', state='*')
async def cancel_handler(message: types.Message, state: FSMContext, raw_state: Optional[str] = None):
@dp.message_handler(state='*', commands='cancel')
@dp.message_handler(Text(equals='cancel', ignore_case=True), state='*')
async def cancel_handler(message: types.Message, state: FSMContext):
"""
Allow user to cancel any action
"""
if raw_state is None:
current_state = await state.get_state()
if current_state is None:
return
logging.info('Cancelling state %r', current_state)
# Cancel state and inform user about it
await state.finish()
# And remove keyboard (just in case)
await message.reply('Canceled.', reply_markup=types.ReplyKeyboardRemove())
await message.reply('Cancelled.', reply_markup=types.ReplyKeyboardRemove())
@dp.message_handler(state=Form.name)
@ -68,7 +71,7 @@ async def process_name(message: types.Message, state: FSMContext):
# Check age. Age gotta be digit
@dp.message_handler(lambda message: not message.text.isdigit(), state=Form.age)
async def failed_process_age(message: types.Message):
async def process_age_invalid(message: types.Message):
"""
If age is invalid
"""
@ -90,11 +93,11 @@ async def process_age(message: types.Message, state: FSMContext):
@dp.message_handler(lambda message: message.text not in ["Male", "Female", "Other"], state=Form.gender)
async def failed_process_gender(message: types.Message):
async def process_gender_invalid(message: types.Message):
"""
In this example gender has to be one of: Male, Female, Other.
"""
return await message.reply("Bad gender name. Choose you gender from keyboard.")
return await message.reply("Bad gender name. Choose your gender from the keyboard.")
@dp.message_handler(state=Form.gender)
@ -106,15 +109,21 @@ async def process_gender(message: types.Message, state: FSMContext):
markup = types.ReplyKeyboardRemove()
# And send message
await bot.send_message(message.chat.id, md.text(
md.text('Hi! Nice to meet you,', md.bold(data['name'])),
md.text('Age:', data['age']),
md.text('Gender:', data['gender']),
sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN)
await bot.send_message(
message.chat.id,
md.text(
md.text('Hi! Nice to meet you,', md.bold(data['name'])),
md.text('Age:', md.code(data['age'])),
md.text('Gender:', data['gender']),
sep='\n',
),
reply_markup=markup,
parse_mode=ParseMode.MARKDOWN,
)
# Finish conversation
data.state = None
# Finish conversation
await state.finish()
if __name__ == '__main__':
executor.start_polling(dp, loop=loop, skip_updates=True)
executor.start_polling(dp, skip_updates=True)

View file

@ -3,6 +3,19 @@ Internalize your bot
Step 1: extract texts
# pybabel extract i18n_example.py -o locales/mybot.pot
Some useful options:
- Extract texts with pluralization support
# -k __:1,2
- Add comments for translators, you can use another tag if you want (TR)
# --add-comments=NOTE
- Disable comments with string location in code
# --no-location
- Set project name
# --project=MySuperBot
- Set version
# --version=2.2
Step 2: create *.po files. For e.g. create en, ru, uk locales.
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
Step 3: translate texts
@ -24,7 +37,7 @@ from pathlib import Path
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.i18n import I18nMiddleware
TOKEN = 'BOT TOKEN HERE'
TOKEN = 'BOT_TOKEN_HERE'
I18N_DOMAIN = 'mybot'
BASE_DIR = Path(__file__).parent
@ -41,16 +54,43 @@ dp.middleware.setup(i18n)
_ = i18n.gettext
@dp.message_handler(commands=['start'])
@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
# Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts.
await message.reply(_('Hello, <b>{user}</b>!').format(user=message.from_user.full_name))
@dp.message_handler(commands=['lang'])
@dp.message_handler(commands='lang')
async def cmd_lang(message: types.Message, locale):
# 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
# And also, there's and example of comments for translators. Most translation tools support them.
# Alias for gettext method, parser will understand double underscore as plural (aka ngettext)
__ = i18n.gettext
# some likes manager
LIKES_STORAGE = {'count': 0}
def get_likes() -> int:
return LIKES_STORAGE['count']
def increase_likes() -> int:
LIKES_STORAGE['count'] += 1
return get_likes()
@dp.message_handler(commands='like')
async def cmd_like(message: types.Message, locale):
likes = increase_likes()
# NOTE: This is comment for a translator
await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes))
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -0,0 +1,36 @@
from aiogram import Bot, Dispatcher, executor, types
from aiogram.dispatcher.handler import SkipHandler
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
user_id_required = None # TODO: Set id here
chat_id_required = user_id_required # Change for use in groups (user_id == chat_id in pm)
@dp.message_handler(user_id=user_id_required)
async def handler1(msg: types.Message):
await bot.send_message(msg.chat.id, "Hello, checking with user_id=")
raise SkipHandler # just for demo
@dp.message_handler(chat_id=chat_id_required)
async def handler2(msg: types.Message):
await bot.send_message(msg.chat.id, "Hello, checking with chat_id=")
raise SkipHandler # just for demo
@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required)
async def handler3(msg: types.Message):
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.answer("Checked user_id with list!")
if __name__ == '__main__':
executor.start_polling(dp)

View file

@ -1,24 +1,37 @@
import asyncio
import hashlib
import logging
from aiogram import Bot, types, Dispatcher, executor
from aiogram import Bot, Dispatcher, executor
from aiogram.types import InlineQuery, \
InputTextMessageContent, InlineQueryResultArticle
API_TOKEN = 'BOT TOKEN HERE'
API_TOKEN = 'BOT_TOKEN_HERE'
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop)
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.inline_handler()
async def inline_echo(inline_query: types.InlineQuery):
input_content = types.InputTextMessageContent(inline_query.query or 'echo')
item = types.InlineQueryResultArticle(id='1', title='echo',
input_message_content=input_content)
async def inline_echo(inline_query: InlineQuery):
# id affects both preview and content,
# so it has to be unique for each result
# (Unique identifier for this result, 1-64 Bytes)
# you can set your unique id's
# but for example i'll generate it based on text because I know, that
# only text will be passed in this example
text = inline_query.query or 'echo'
input_content = InputTextMessageContent(text)
result_id: str = hashlib.md5(text.encode()).hexdigest()
item = InlineQueryResultArticle(
id=result_id,
title=f'Result {text!r}',
input_message_content=input_content,
)
# don't forget to set cache_time=1 for testing (default is 300s or 5m)
await bot.answer_inline_query(inline_query.id, results=[item], cache_time=1)
if __name__ == '__main__':
executor.start_polling(dp, loop=loop, skip_updates=True)
executor.start_polling(dp, skip_updates=True)

View file

@ -0,0 +1,62 @@
"""
This bot is created for the demonstration of a usage of inline keyboards.
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(commands='start')
async def start_cmd_handler(message: types.Message):
keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually
# kept for clearness
text_and_data = (
('Yes!', 'yes'),
('No!', 'no'),
)
# in real life for the callback_data the callback data factory should be used
# here the raw string is used for the simplicity
row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data)
keyboard_markup.row(*row_btns)
keyboard_markup.add(
# url buttons have no callback data
types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'),
)
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
# Use multiple registrators. Handler will execute when one of the filters is OK
@dp.callback_query_handler(text='no') # if cb.data == 'no'
@dp.callback_query_handler(text='yes') # if cb.data == 'yes'
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
answer_data = query.data
# always answer callback queries, even if you have nothing to say
await query.answer(f'You answered with {answer_data!r}')
if answer_data == 'yes':
text = 'Great, me too!'
elif answer_data == 'no':
text = 'Oh no...Why so?'
else:
text = f'Unexpected callback data {answer_data!r}!'
await bot.send_message(query.from_user.id, text)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -1,27 +1,26 @@
# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
"POT-Creation-Date: 2019-08-10 17:51+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
"Generated-By: Babel 2.7.0\n"
#: i18n_example.py:48
#: i18n_example.py:60
msgid "Hello, <b>{user}</b>!"
msgstr ""
#: i18n_example.py:53
#: i18n_example.py:67
msgid "Your current language: <i>{language}</i>"
msgstr ""

View file

@ -1,14 +1,14 @@
# Russian translations for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
"PO-Revision-Date: 2018-06-30 03:43+0300\n"
"POT-Creation-Date: 2019-08-10 17:51+0300\n"
"PO-Revision-Date: 2019-08-10 17:52+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
@ -17,13 +17,19 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
"Generated-By: Babel 2.7.0\n"
#: i18n_example.py:48
#: i18n_example.py:60
msgid "Hello, <b>{user}</b>!"
msgstr "Привет, <b>{user}</b>!"
#: i18n_example.py:53
#: i18n_example.py:67
msgid "Your current language: <i>{language}</i>"
msgstr "Твой язык: <i>{language}</i>"
#: i18n_example.py:95
msgid "Aiogram has {number} like!"
msgid_plural "Aiogram has {number} likes!"
msgstr[0] "Aiogram имеет {number} лайк!"
msgstr[1] "Aiogram имеет {number} лайка!"
msgstr[2] "Aiogram имеет {number} лайков!"

View file

@ -2,10 +2,10 @@ import asyncio
from aiogram import Bot, Dispatcher, executor, filters, types
API_TOKEN = 'BOT TOKEN HERE'
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop)
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@ -14,10 +14,10 @@ async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Do you want to see many pussies? Are you ready?")
# And wait few seconds...
# Wait a little...
await asyncio.sleep(1)
# Good bots should send chat actions. Or not.
# Good bots should send chat actions...
await types.ChatActions.upload_photo()
# Create media group
@ -40,4 +40,4 @@ async def send_welcome(message: types.Message):
if __name__ == '__main__':
executor.start_polling(dp, loop=loop, skip_updates=True)
executor.start_polling(dp, skip_updates=True)

View file

@ -7,14 +7,12 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
TOKEN = 'BOT TOKEN HERE'
loop = asyncio.get_event_loop()
TOKEN = 'BOT_TOKEN_HERE'
# In this example Redis storage is used
storage = RedisStorage2(db=5)
bot = Bot(token=TOKEN, loop=loop)
bot = Bot(token=TOKEN)
dp = Dispatcher(bot, storage=storage)
@ -119,4 +117,4 @@ if __name__ == '__main__':
dp.middleware.setup(ThrottlingMiddleware())
# Start long-polling
executor.start_polling(dp, loop=loop)
executor.start_polling(dp)

View file

@ -1,28 +1,26 @@
import asyncio
from aiogram import Bot
from aiogram import types
from aiogram.dispatcher import Dispatcher
from aiogram.types.message import ContentTypes
from aiogram.utils import executor
BOT_TOKEN = 'BOT TOKEN HERE'
PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef'
loop = asyncio.get_event_loop()
BOT_TOKEN = 'BOT_TOKEN_HERE'
PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1422'
bot = Bot(BOT_TOKEN)
dp = Dispatcher(bot, loop=loop)
dp = Dispatcher(bot)
# Setup prices
prices = [
types.LabeledPrice(label='Working Time Machine', amount=5750),
types.LabeledPrice(label='Gift wrapping', amount=500)
types.LabeledPrice(label='Gift wrapping', amount=500),
]
# Setup shipping options
shipping_options = [
types.ShippingOption(id='instant', title='WorldWide Teleporter').add(types.LabeledPrice('Teleporter', 1000)),
types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300))
types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)),
]
@ -60,7 +58,7 @@ async def cmd_buy(message: types.Message):
' Order our Working Time Machine today!',
provider_token=PAYMENTS_PROVIDER_TOKEN,
currency='usd',
photo_url='https://images.fineartamerica.com/images-medium-large/2-the-time-machine-dmitriy-khristenko.jpg',
photo_url='https://telegra.ph/file/d08ff863531f10bf2ea4b.jpg',
photo_height=512, # !=0/None or picture won't be shown
photo_width=512,
photo_size=512,
@ -70,14 +68,14 @@ async def cmd_buy(message: types.Message):
payload='HAPPY FRIDAYS COUPON')
@dp.shipping_query_handler(func=lambda query: True)
@dp.shipping_query_handler(lambda query: True)
async def shipping(shipping_query: types.ShippingQuery):
await bot.answer_shipping_query(shipping_query.id, ok=True, shipping_options=shipping_options,
error_message='Oh, seems like our Dog couriers are having a lunch right now.'
' Try again later!')
@dp.pre_checkout_query_handler(func=lambda query: True)
@dp.pre_checkout_query_handler(lambda query: True)
async def checkout(pre_checkout_query: types.PreCheckoutQuery):
await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True,
error_message="Aliens tried to steal your card's CVV,"
@ -96,4 +94,4 @@ async def got_payment(message: types.Message):
if __name__ == '__main__':
executor.start_polling(dp, loop=loop)
executor.start_polling(dp, skip_updates=True)

View file

@ -1,4 +1,3 @@
import asyncio
import logging
import aiohttp
@ -11,13 +10,13 @@ from aiogram.utils.executor import start_polling
from aiogram.utils.markdown import bold, code, italic, text
# Configure bot here
API_TOKEN = 'BOT TOKEN HERE'
PROXY_URL = 'http://PROXY_URL' # Or 'socks5://...'
API_TOKEN = 'BOT_TOKEN_HERE'
PROXY_URL = 'http://PROXY_URL' # Or 'socks5://host:port'
# If authentication is required in your proxy then uncomment next line and change login/password for it
# NOTE: If authentication is required in your proxy then uncomment next line and change login/password for it
# PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password')
# And add `proxy_auth=PROXY_AUTH` argument in line 25, like this:
# >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
# And add `proxy_auth=PROXY_AUTH` argument in line 30, like this:
# >>> bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
# Also you can use Socks5 proxy but you need manually install aiohttp_socks package.
# Get my ip URL
@ -25,28 +24,33 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/'
logging.basicConfig(level=logging.INFO)
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL)
bot = Bot(token=API_TOKEN, proxy=PROXY_URL)
# If auth is required:
# bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
dp = Dispatcher(bot)
async def fetch(url, proxy=None, proxy_auth=None):
async with aiohttp.ClientSession() as session:
async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response:
return await response.text()
async def fetch(url, session):
async with session.get(url) as response:
return await response.text()
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
# fetching urls will take some time, so notify user that everything is OK
await types.ChatActions.typing()
content = []
# Make request (without proxy)
ip = await fetch(GET_IP_URL)
async with aiohttp.ClientSession() as session:
ip = await fetch(GET_IP_URL, session)
content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip)))
# This line is formatted to '🌎 *IP:* `YOUR IP`'
# Make request through proxy
ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth)
# Make request through bot's proxy
ip = await fetch(GET_IP_URL, bot.session)
content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy')))
# This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_'
@ -62,4 +66,4 @@ async def cmd_start(message: types.Message):
if __name__ == '__main__':
start_polling(dp, loop=loop, skip_updates=True)
start_polling(dp, skip_updates=True)

Some files were not shown because too many files have changed in this diff Show more