mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
commit
5410efca24
56 changed files with 2648 additions and 410 deletions
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github: [JRootJunior]
|
||||
open_collective: aiogram
|
||||
19
.readthedocs.yml
Normal file
19
.readthedocs.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# .readthedocs.yml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
formats: all
|
||||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: dev_requirements.txt
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ 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
|
||||
: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
|
||||
:alt: Documentation Status
|
||||
|
|
|
|||
|
|
@ -38,5 +38,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.0.2.dev1'
|
||||
__api_version__ = '4.1'
|
||||
__version__ = '2.2.1.dev1'
|
||||
__api_version__ = '4.3'
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ def check_token(token: str) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def check_result(method_name: str, content_type: str, status_code: int, body: str):
|
||||
def check_result(method_name: str, content_type: str, status_code: int, body: str):
|
||||
"""
|
||||
Checks whether `result` is a valid API response.
|
||||
A result is considered invalid if:
|
||||
|
|
@ -95,7 +95,7 @@ async def make_request(session, token, method, data=None, files=None, **kwargs):
|
|||
req = compose_data(data, files)
|
||||
try:
|
||||
async with session.post(url, data=req, **kwargs) as response:
|
||||
return await check_result(method, response.content_type, response.status, await response.text())
|
||||
return check_result(method, response.content_type, response.status, await response.text())
|
||||
except aiohttp.ClientError as e:
|
||||
raise exceptions.NetworkError(f"aiohttp client throws an error: {e.__class__.__name__}: {e}")
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 4.1
|
||||
List is updated to Bot API 4.3
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -174,6 +174,7 @@ class Methods(Helper):
|
|||
STOP_MESSAGE_LIVE_LOCATION = Item() # stopMessageLiveLocation
|
||||
SEND_VENUE = Item() # sendVenue
|
||||
SEND_CONTACT = Item() # sendContact
|
||||
SEND_POLL = Item() # sendPoll
|
||||
SEND_CHAT_ACTION = Item() # sendChatAction
|
||||
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
|
||||
GET_FILE = Item() # getFile
|
||||
|
|
@ -202,6 +203,7 @@ class Methods(Helper):
|
|||
EDIT_MESSAGE_CAPTION = Item() # editMessageCaption
|
||||
EDIT_MESSAGE_MEDIA = Item() # editMessageMedia
|
||||
EDIT_MESSAGE_REPLY_MARKUP = Item() # editMessageReplyMarkup
|
||||
STOP_POLL = Item() # stopPoll
|
||||
DELETE_MESSAGE = Item() # deleteMessage
|
||||
|
||||
# Stickers
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import io
|
||||
import ssl
|
||||
import typing
|
||||
from contextvars import ContextVar
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
import certifi
|
||||
from aiohttp.helpers import sentinel
|
||||
|
||||
from . import api
|
||||
from ..types import ParseMode, base
|
||||
|
|
@ -16,13 +20,20 @@ class BaseBot:
|
|||
"""
|
||||
Base class for bot. It's raw bot.
|
||||
"""
|
||||
_ctx_timeout = ContextVar('TelegramRequestTimeout')
|
||||
_ctx_token = ContextVar('BotDifferentToken')
|
||||
|
||||
def __init__(self, token: base.String,
|
||||
loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None,
|
||||
connections_limit: Optional[base.Integer] = None,
|
||||
proxy: Optional[base.String] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None,
|
||||
validate_token: Optional[base.Boolean] = True,
|
||||
parse_mode=None):
|
||||
def __init__(
|
||||
self,
|
||||
token: base.String,
|
||||
loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None,
|
||||
connections_limit: Optional[base.Integer] = None,
|
||||
proxy: Optional[base.String] = None,
|
||||
proxy_auth: Optional[aiohttp.BasicAuth] = None,
|
||||
validate_token: Optional[base.Boolean] = True,
|
||||
parse_mode: typing.Optional[base.String] = None,
|
||||
timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None
|
||||
):
|
||||
"""
|
||||
Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot
|
||||
|
||||
|
|
@ -40,11 +51,14 @@ class BaseBot:
|
|||
:type validate_token: :obj:`bool`
|
||||
:param parse_mode: You can set default parse mode
|
||||
:type parse_mode: :obj:`str`
|
||||
:param timeout: Request timeout
|
||||
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
|
||||
:raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError`
|
||||
"""
|
||||
# Authentication
|
||||
if validate_token:
|
||||
api.check_token(token)
|
||||
self._token = None
|
||||
self.__token = token
|
||||
|
||||
self.proxy = proxy
|
||||
|
|
@ -77,13 +91,71 @@ class BaseBot:
|
|||
self.proxy = None
|
||||
self.proxy_auth = None
|
||||
else:
|
||||
connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context,
|
||||
loop=self.loop)
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _prepare_timeout(
|
||||
value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]
|
||||
) -> typing.Optional[aiohttp.ClientTimeout]:
|
||||
if value is None or isinstance(value, aiohttp.ClientTimeout):
|
||||
return value
|
||||
return aiohttp.ClientTimeout(total=value)
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
timeout = self._ctx_timeout.get(self._timeout)
|
||||
if timeout is None:
|
||||
return sentinel
|
||||
return timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value):
|
||||
self._timeout = self._prepare_timeout(value)
|
||||
|
||||
@timeout.deleter
|
||||
def timeout(self):
|
||||
self.timeout = None
|
||||
|
||||
@contextlib.contextmanager
|
||||
def request_timeout(self, timeout: typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]):
|
||||
"""
|
||||
Context manager implements opportunity to change request timeout in current context
|
||||
|
||||
:param timeout: Request timeout
|
||||
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
|
||||
:return:
|
||||
"""
|
||||
timeout = self._prepare_timeout(timeout)
|
||||
token = self._ctx_timeout.set(timeout)
|
||||
try:
|
||||
yield
|
||||
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
|
||||
|
|
@ -109,11 +181,11 @@ class BaseBot:
|
|||
:raise: :obj:`aiogram.exceptions.TelegramApiError`
|
||||
"""
|
||||
return await api.make_request(self.session, self.__token, method, data, files,
|
||||
proxy=self.proxy, proxy_auth=self.proxy_auth, **kwargs)
|
||||
proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs)
|
||||
|
||||
async def download_file(self, file_path: base.String,
|
||||
destination: Optional[base.InputFile] = None,
|
||||
timeout: Optional[base.Integer] = 30,
|
||||
timeout: Optional[base.Integer] = sentinel,
|
||||
chunk_size: Optional[base.Integer] = 65536,
|
||||
seek: Optional[base.Boolean] = True) -> Union[io.BytesIO, io.FileIO]:
|
||||
"""
|
||||
|
|
@ -133,7 +205,7 @@ class BaseBot:
|
|||
if destination is None:
|
||||
destination = io.BytesIO()
|
||||
|
||||
url = api.Methods.file_url(token=self.__token, path=file_path)
|
||||
url = self.get_file_url(file_path)
|
||||
|
||||
dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb')
|
||||
async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response:
|
||||
|
|
@ -147,6 +219,9 @@ class BaseBot:
|
|||
dest.seek(0)
|
||||
return dest
|
||||
|
||||
def get_file_url(self, file_path):
|
||||
return api.Methods.file_url(token=self.__token, path=file_path)
|
||||
|
||||
async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]:
|
||||
"""
|
||||
Send file
|
||||
|
|
|
|||
|
|
@ -201,7 +201,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -267,7 +268,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -327,7 +329,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -377,7 +380,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -438,7 +442,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -557,7 +562,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -605,7 +611,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -679,7 +686,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -794,7 +802,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -835,7 +844,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
@ -847,6 +857,44 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
result = await self.request(api.Methods.SEND_CONTACT, payload)
|
||||
return types.Message(**result)
|
||||
|
||||
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],
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove,
|
||||
types.ForceReply, None] = None) -> types.Message:
|
||||
"""
|
||||
Use this method to send a native poll. A native poll can't be sent to a private chat.
|
||||
On success, the sent Message is returned.
|
||||
|
||||
:param chat_id: Unique identifier for the target chat
|
||||
or username of the target channel (in the format @channelusername).
|
||||
A native poll can't be sent to a private chat.
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
: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]`
|
||||
: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
|
||||
:type reply_to_message_id: :obj:`typing.Optional[Integer]`
|
||||
: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`
|
||||
"""
|
||||
options = prepare_arg(options)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SEND_POLL, payload)
|
||||
return types.Message(**result)
|
||||
|
||||
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
action: base.String) -> base.Boolean:
|
||||
"""
|
||||
|
|
@ -1524,18 +1572,38 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
return result
|
||||
return types.Message(**result)
|
||||
|
||||
async def stop_poll(self, chat_id: typing.Union[base.String, base.Integer],
|
||||
message_id: base.Integer,
|
||||
reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Poll:
|
||||
"""
|
||||
Use this method to stop a poll which was sent by the bot.
|
||||
On success, the stopped Poll with the final results is returned.
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
:type chat_id: :obj:`typing.Union[base.String, base.Integer]`
|
||||
:param message_id: Identifier of the original message with the poll
|
||||
:type message_id: :obj:`base.Integer`
|
||||
:param reply_markup: A JSON-serialized object for a new message inline keyboard.
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]`
|
||||
:return: On success, the stopped Poll with the final results is returned.
|
||||
:rtype: :obj:`types.Poll`
|
||||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.STOP_POLL, payload)
|
||||
return types.Poll(**result)
|
||||
|
||||
async def delete_message(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
message_id: base.Integer) -> base.Boolean:
|
||||
"""
|
||||
Use this method to delete a message, including service messages, with the following limitations
|
||||
Use this method to delete a message, including service messages, with the following limitations:
|
||||
- A message can only be deleted if it was sent less than 48 hours ago.
|
||||
- Bots can delete outgoing messages in groups and supergroups.
|
||||
- Bots can delete outgoing messages in private chats, groups, and supergroups.
|
||||
- Bots can delete incoming messages in private chats.
|
||||
- Bots granted can_post_messages permissions can delete outgoing messages in channels.
|
||||
- If the bot is an administrator of a group, it can delete any message there.
|
||||
- If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there.
|
||||
|
||||
The following methods and objects allow your bot to handle stickers and sticker sets.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#deletemessage
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
|
|
@ -1574,7 +1642,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
: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
|
||||
: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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class _FileStorage(MemoryStorage):
|
|||
path = self.path = pathlib.Path(path)
|
||||
|
||||
try:
|
||||
self._data = self.read(path)
|
||||
self.data = self.read(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -62,8 +61,6 @@ class RedisStorage(BaseStorage):
|
|||
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:
|
||||
|
|
@ -173,10 +170,10 @@ class RedisStorage(BaseStorage):
|
|||
conn = await self.redis()
|
||||
|
||||
if full:
|
||||
conn.execute('FLUSHDB')
|
||||
await conn.execute('FLUSHDB')
|
||||
else:
|
||||
keys = await conn.execute('KEYS', 'fsm:*')
|
||||
conn.execute('DEL', *keys)
|
||||
await conn.execute('DEL', *keys)
|
||||
|
||||
def has_bucket(self):
|
||||
return True
|
||||
|
|
@ -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,14 +235,16 @@ class RedisStorage2(BaseStorage):
|
|||
self._kwargs = kwargs
|
||||
self._prefix = (prefix,)
|
||||
|
||||
self._state_ttl = state_ttl
|
||||
self._data_ttl = data_ttl
|
||||
self._bucket_ttl = bucket_ttl
|
||||
|
||||
self._redis: 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:
|
||||
|
|
@ -294,14 +296,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,16 +331,16 @@ 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,
|
||||
bucket: typing.Dict = None, **kwargs):
|
||||
if bucket is None:
|
||||
bucket = {}
|
||||
temp_bucket = await self.get_data(chat=chat, user=user)
|
||||
temp_bucket = await self.get_bucket(chat=chat, user=user)
|
||||
temp_bucket.update(bucket, **kwargs)
|
||||
await self.set_data(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):
|
||||
"""
|
||||
|
|
@ -350,10 +352,10 @@ class RedisStorage2(BaseStorage):
|
|||
conn = await self.redis()
|
||||
|
||||
if full:
|
||||
conn.flushdb()
|
||||
await conn.flushdb()
|
||||
else:
|
||||
keys = await conn.keys(self.generate_key('*'))
|
||||
conn.delete(*keys)
|
||||
await conn.delete(*keys)
|
||||
|
||||
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from contextvars import ContextVar
|
|||
from typing import Any, Dict, Tuple
|
||||
|
||||
from babel import Locale
|
||||
from babel.support import LazyProxy
|
||||
|
||||
from ... import types
|
||||
from ...dispatcher.middlewares import BaseMiddleware
|
||||
|
|
@ -106,6 +107,18 @@ class I18nMiddleware(BaseMiddleware):
|
|||
else:
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
|
||||
"""
|
||||
Lazy get text
|
||||
|
||||
:param singular:
|
||||
:param plural:
|
||||
:param n:
|
||||
:param locale:
|
||||
:return:
|
||||
"""
|
||||
return LazyProxy(self.gettext, singular, plural, n, locale)
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.dispatcher.middlewares import BaseMiddleware
|
||||
|
||||
|
|
@ -88,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}] "
|
||||
|
|
@ -139,3 +145,283 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
timeout = self.check_timeout(update)
|
||||
if timeout > 0:
|
||||
self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)")
|
||||
|
||||
|
||||
class LoggingFilter(logging.Filter):
|
||||
"""
|
||||
Extend LogRecord by data from Telegram Update object.
|
||||
|
||||
Can be used in logging config:
|
||||
.. code-block: python3
|
||||
|
||||
'filters': {
|
||||
'telegram': {
|
||||
'()': LoggingFilter,
|
||||
'include_content': True,
|
||||
}
|
||||
},
|
||||
...
|
||||
'handlers': {
|
||||
'graypy': {
|
||||
'()': GELFRabbitHandler,
|
||||
'url': 'amqp://localhost:5672/',
|
||||
'routing_key': '#',
|
||||
'localname': 'testapp',
|
||||
'filters': ['telegram']
|
||||
},
|
||||
},
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name='', prefix='tg', include_content=False):
|
||||
"""
|
||||
:param name:
|
||||
:param prefix: prefix for all records
|
||||
:param include_content: pass into record all data from Update object
|
||||
"""
|
||||
super(LoggingFilter, self).__init__(name=name)
|
||||
|
||||
self.prefix = prefix
|
||||
self.include_content = include_content
|
||||
|
||||
def filter(self, record: logging.LogRecord):
|
||||
"""
|
||||
Extend LogRecord by data from Telegram Update object.
|
||||
|
||||
:param record:
|
||||
:return:
|
||||
"""
|
||||
update = types.Update.get_current(True)
|
||||
if update:
|
||||
for key, value in self.make_prefix(self.prefix, self.process_update(update)):
|
||||
setattr(record, key, value)
|
||||
|
||||
return True
|
||||
|
||||
def process_update(self, update: types.Update):
|
||||
"""
|
||||
Parse Update object
|
||||
|
||||
:param update:
|
||||
:return:
|
||||
"""
|
||||
yield 'update_id', update.update_id
|
||||
|
||||
if update.message:
|
||||
yield 'update_type', 'message'
|
||||
yield from self.process_message(update.message)
|
||||
if update.edited_message:
|
||||
yield 'update_type', 'edited_message'
|
||||
yield from self.process_message(update.edited_message)
|
||||
if update.channel_post:
|
||||
yield 'update_type', 'channel_post'
|
||||
yield from self.process_message(update.channel_post)
|
||||
if update.edited_channel_post:
|
||||
yield 'update_type', 'edited_channel_post'
|
||||
yield from self.process_message(update.edited_channel_post)
|
||||
if update.inline_query:
|
||||
yield 'update_type', 'inline_query'
|
||||
yield from self.process_inline_query(update.inline_query)
|
||||
if update.chosen_inline_result:
|
||||
yield 'update_type', 'chosen_inline_result'
|
||||
yield from self.process_chosen_inline_result(update.chosen_inline_result)
|
||||
if update.callback_query:
|
||||
yield 'update_type', 'callback_query'
|
||||
yield from self.process_callback_query(update.callback_query)
|
||||
if update.shipping_query:
|
||||
yield 'update_type', 'shipping_query'
|
||||
yield from self.process_shipping_query(update.shipping_query)
|
||||
if update.pre_checkout_query:
|
||||
yield 'update_type', 'pre_checkout_query'
|
||||
yield from self.process_pre_checkout_query(update.pre_checkout_query)
|
||||
|
||||
def make_prefix(self, prefix, iterable):
|
||||
"""
|
||||
Add prefix to the label
|
||||
|
||||
:param prefix:
|
||||
:param iterable:
|
||||
:return:
|
||||
"""
|
||||
if not prefix:
|
||||
yield from iterable
|
||||
|
||||
for key, value in iterable:
|
||||
yield f"{prefix}_{key}", value
|
||||
|
||||
def process_user(self, user: types.User):
|
||||
"""
|
||||
Generate user data
|
||||
|
||||
:param user:
|
||||
:return:
|
||||
"""
|
||||
if not user:
|
||||
return
|
||||
|
||||
yield 'user_id', user.id
|
||||
if self.include_content:
|
||||
yield 'user_full_name', user.full_name
|
||||
if user.username:
|
||||
yield 'user_name', f"@{user.username}"
|
||||
|
||||
def process_chat(self, chat: types.Chat):
|
||||
"""
|
||||
Generate chat data
|
||||
|
||||
:param chat:
|
||||
:return:
|
||||
"""
|
||||
if not chat:
|
||||
return
|
||||
|
||||
yield 'chat_id', chat.id
|
||||
yield 'chat_type', chat.type
|
||||
if self.include_content:
|
||||
yield 'chat_title', chat.full_name
|
||||
if chat.username:
|
||||
yield 'chat_name', f"@{chat.username}"
|
||||
|
||||
def process_message(self, message: types.Message):
|
||||
yield 'message_content_type', message.content_type
|
||||
yield from self.process_user(message.from_user)
|
||||
yield from self.process_chat(message.chat)
|
||||
|
||||
if not self.include_content:
|
||||
return
|
||||
|
||||
if message.reply_to_message:
|
||||
yield from self.make_prefix('reply_to', self.process_message(message.reply_to_message))
|
||||
if message.forward_from:
|
||||
yield from self.make_prefix('forward_from', self.process_user(message.forward_from))
|
||||
if message.forward_from_chat:
|
||||
yield from self.make_prefix('forward_from_chat', self.process_chat(message.forward_from_chat))
|
||||
if message.forward_from_message_id:
|
||||
yield 'message_forward_from_message_id', message.forward_from_message_id
|
||||
if message.forward_date:
|
||||
yield 'message_forward_date', message.forward_date
|
||||
if message.edit_date:
|
||||
yield 'message_edit_date', message.edit_date
|
||||
if message.media_group_id:
|
||||
yield 'message_media_group_id', message.media_group_id
|
||||
if message.author_signature:
|
||||
yield 'message_author_signature', message.author_signature
|
||||
|
||||
if message.text:
|
||||
yield 'text', message.text or message.caption
|
||||
yield 'html_text', message.html_text
|
||||
elif message.audio:
|
||||
yield 'audio', message.audio.file_id
|
||||
elif message.animation:
|
||||
yield 'animation', message.animation.file_id
|
||||
elif message.document:
|
||||
yield 'document', message.document.file_id
|
||||
elif message.game:
|
||||
yield 'game', message.game.title
|
||||
elif message.photo:
|
||||
yield 'photo', message.photo[-1].file_id
|
||||
elif message.sticker:
|
||||
yield 'sticker', message.sticker.file_id
|
||||
elif message.video:
|
||||
yield 'video', message.video.file_id
|
||||
elif message.video_note:
|
||||
yield 'video_note', message.video_note.file_id
|
||||
elif message.voice:
|
||||
yield 'voice', message.voice.file_id
|
||||
elif message.contact:
|
||||
yield 'contact_full_name', message.contact.full_name
|
||||
yield 'contact_phone_number', message.contact.phone_number
|
||||
elif message.venue:
|
||||
yield 'venue_address', message.venue.address
|
||||
yield 'location_latitude', message.venue.location.latitude
|
||||
yield 'location_longitude', message.venue.location.longitude
|
||||
elif message.location:
|
||||
yield 'location_latitude', message.location.latitude
|
||||
yield 'location_longitude', message.location.longitude
|
||||
elif message.new_chat_members:
|
||||
yield 'new_chat_members', [user.id for user in message.new_chat_members]
|
||||
elif message.left_chat_member:
|
||||
yield 'left_chat_member', [user.id for user in message.new_chat_members]
|
||||
elif message.invoice:
|
||||
yield 'invoice_title', message.invoice.title
|
||||
yield 'invoice_description', message.invoice.description
|
||||
yield 'invoice_start_parameter', message.invoice.start_parameter
|
||||
yield 'invoice_currency', message.invoice.currency
|
||||
yield 'invoice_total_amount', message.invoice.total_amount
|
||||
elif message.successful_payment:
|
||||
yield 'successful_payment_currency', message.successful_payment.currency
|
||||
yield 'successful_payment_total_amount', message.successful_payment.total_amount
|
||||
yield 'successful_payment_invoice_payload', message.successful_payment.invoice_payload
|
||||
yield 'successful_payment_shipping_option_id', message.successful_payment.shipping_option_id
|
||||
yield 'successful_payment_telegram_payment_charge_id', message.successful_payment.telegram_payment_charge_id
|
||||
yield 'successful_payment_provider_payment_charge_id', message.successful_payment.provider_payment_charge_id
|
||||
elif message.connected_website:
|
||||
yield 'connected_website', message.connected_website
|
||||
elif message.migrate_from_chat_id:
|
||||
yield 'migrate_from_chat_id', message.migrate_from_chat_id
|
||||
elif message.migrate_to_chat_id:
|
||||
yield 'migrate_to_chat_id', message.migrate_to_chat_id
|
||||
elif message.pinned_message:
|
||||
yield from self.make_prefix('pinned_message', message.pinned_message)
|
||||
elif message.new_chat_title:
|
||||
yield 'new_chat_title', message.new_chat_title
|
||||
elif message.new_chat_photo:
|
||||
yield 'new_chat_photo', message.new_chat_photo[-1].file_id
|
||||
# elif message.delete_chat_photo:
|
||||
# yield 'delete_chat_photo', message.delete_chat_photo
|
||||
# elif message.group_chat_created:
|
||||
# yield 'group_chat_created', message.group_chat_created
|
||||
# elif message.passport_data:
|
||||
# yield 'passport_data', message.passport_data
|
||||
|
||||
def process_inline_query(self, inline_query: types.InlineQuery):
|
||||
yield 'inline_query_id', inline_query.id
|
||||
yield from self.process_user(inline_query.from_user)
|
||||
|
||||
if self.include_content:
|
||||
yield 'inline_query_text', inline_query.query
|
||||
if inline_query.location:
|
||||
yield 'location_latitude', inline_query.location.latitude
|
||||
yield 'location_longitude', inline_query.location.longitude
|
||||
if inline_query.offset:
|
||||
yield 'inline_query_offset', inline_query.offset
|
||||
|
||||
def process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult):
|
||||
yield 'chosen_inline_result_id', chosen_inline_result.result_id
|
||||
yield from self.process_user(chosen_inline_result.from_user)
|
||||
|
||||
if self.include_content:
|
||||
yield 'inline_query_text', chosen_inline_result.query
|
||||
if chosen_inline_result.location:
|
||||
yield 'location_latitude', chosen_inline_result.location.latitude
|
||||
yield 'location_longitude', chosen_inline_result.location.longitude
|
||||
|
||||
def process_callback_query(self, callback_query: types.CallbackQuery):
|
||||
yield from self.process_user(callback_query.from_user)
|
||||
yield 'callback_query_data', callback_query.data
|
||||
|
||||
if callback_query.message:
|
||||
yield from self.make_prefix('callback_query_message', self.process_message(callback_query.message))
|
||||
if callback_query.inline_message_id:
|
||||
yield 'callback_query_inline_message_id', callback_query.inline_message_id
|
||||
if callback_query.chat_instance:
|
||||
yield 'callback_query_chat_instance', callback_query.chat_instance
|
||||
if callback_query.game_short_name:
|
||||
yield 'callback_query_game_short_name', callback_query.game_short_name
|
||||
|
||||
def process_shipping_query(self, shipping_query: types.ShippingQuery):
|
||||
yield 'shipping_query_id', shipping_query.id
|
||||
yield from self.process_user(shipping_query.from_user)
|
||||
|
||||
if self.include_content:
|
||||
yield 'shipping_query_invoice_payload', shipping_query.invoice_payload
|
||||
|
||||
def process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery):
|
||||
yield 'pre_checkout_query_id', pre_checkout_query.id
|
||||
yield from self.process_user(pre_checkout_query.from_user)
|
||||
|
||||
if self.include_content:
|
||||
yield 'pre_checkout_query_currency', pre_checkout_query.currency
|
||||
yield 'pre_checkout_query_total_amount', pre_checkout_query.total_amount
|
||||
yield 'pre_checkout_query_invoice_payload', pre_checkout_query.invoice_payload
|
||||
yield 'pre_checkout_query_shipping_option_id', pre_checkout_query.shipping_option_id
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import logging
|
|||
import time
|
||||
import typing
|
||||
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, FuncFilter, HashTag, Regexp, \
|
||||
import aiohttp
|
||||
from aiohttp.helpers import sentinel
|
||||
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
|
||||
RegexpCommandsFilter, StateFilter, Text
|
||||
from .handler import Handler
|
||||
from .middlewares import MiddlewareManager
|
||||
|
|
@ -35,6 +38,9 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
throttling_rate_limit=DEFAULT_RATE_LIMIT, no_throttle_error=False,
|
||||
filters_factory=None):
|
||||
|
||||
if not isinstance(bot, Bot):
|
||||
raise TypeError(f"Argument 'bot' must be an instance of Bot, not '{type(bot).__name__}'")
|
||||
|
||||
if loop is None:
|
||||
loop = bot.loop
|
||||
if storage is None:
|
||||
|
|
@ -61,6 +67,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self.callback_query_handlers = Handler(self, middleware_key='callback_query')
|
||||
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.errors_handlers = Handler(self, once=False, middleware_key='error')
|
||||
|
||||
self.middleware = MiddlewareManager(self)
|
||||
|
|
@ -77,7 +84,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory = self.filters_factory
|
||||
|
||||
filters_factory.bind(StateFilter, exclude_event_handlers=[
|
||||
self.errors_handlers
|
||||
self.errors_handlers,
|
||||
self.poll_handlers
|
||||
])
|
||||
filters_factory.bind(ContentTypeFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
|
|
@ -89,7 +97,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
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.callback_query_handlers, self.poll_handlers
|
||||
])
|
||||
filters_factory.bind(HashTag, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
|
|
@ -98,7 +106,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
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.callback_query_handlers, self.poll_handlers
|
||||
])
|
||||
filters_factory.bind(RegexpCommandsFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers
|
||||
|
|
@ -106,9 +114,6 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(ExceptionsFilter, event_handlers=[
|
||||
self.errors_handlers
|
||||
])
|
||||
filters_factory.bind(FuncFilter, exclude_event_handlers=[
|
||||
self.errors_handlers
|
||||
])
|
||||
|
||||
def __del__(self):
|
||||
self.stop_polling()
|
||||
|
|
@ -182,6 +187,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
if update.pre_checkout_query:
|
||||
types.User.set_current(update.pre_checkout_query.from_user)
|
||||
return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query)
|
||||
if update.poll:
|
||||
return await self.poll_handlers.notify(update.poll)
|
||||
except Exception as e:
|
||||
err = await self.errors_handlers.notify(update, e)
|
||||
if err:
|
||||
|
|
@ -202,8 +209,13 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
return await self.bot.delete_webhook()
|
||||
|
||||
async def start_polling(self, timeout=20, relax=0.1, limit=None, reset_webhook=None,
|
||||
fast: typing.Optional[bool] = True):
|
||||
async def start_polling(self,
|
||||
timeout=20,
|
||||
relax=0.1,
|
||||
limit=None,
|
||||
reset_webhook=None,
|
||||
fast: typing.Optional[bool] = True,
|
||||
error_sleep: int = 5):
|
||||
"""
|
||||
Start long-polling
|
||||
|
||||
|
|
@ -231,12 +243,19 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
self._polling = True
|
||||
offset = None
|
||||
try:
|
||||
current_request_timeout = self.bot.timeout
|
||||
if current_request_timeout is not sentinel and timeout is not None:
|
||||
request_timeout = aiohttp.ClientTimeout(total=current_request_timeout.total + timeout or 1)
|
||||
else:
|
||||
request_timeout = None
|
||||
|
||||
while self._polling:
|
||||
try:
|
||||
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
|
||||
with self.bot.request_timeout(request_timeout):
|
||||
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
|
||||
except:
|
||||
log.exception('Cause exception while getting updates.')
|
||||
await asyncio.sleep(15)
|
||||
await asyncio.sleep(error_sleep)
|
||||
continue
|
||||
|
||||
if updates:
|
||||
|
|
@ -247,6 +266,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
if relax:
|
||||
await asyncio.sleep(relax)
|
||||
|
||||
finally:
|
||||
self._close_waiter._set_result(None)
|
||||
log.warning('Polling is stopped.')
|
||||
|
|
@ -276,7 +296,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
:return:
|
||||
"""
|
||||
if self._polling:
|
||||
if hasattr(self, '_polling') and self._polling:
|
||||
log.info('Stop polling...')
|
||||
self._polling = False
|
||||
|
||||
|
|
@ -793,6 +813,20 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
|
||||
return decorator
|
||||
|
||||
def register_poll_handler(self, callback, *custom_filters, run_task=None, **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):
|
||||
def decorator(callback):
|
||||
self.register_poll_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):
|
||||
"""
|
||||
Register handler for errors
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
|
||||
ExceptionsFilter, FuncFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
|
||||
from .factory import FiltersFactory
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, check_filter, check_filters
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
|
||||
check_filters, get_filter_spec, get_filters_spec
|
||||
|
||||
__all__ = [
|
||||
'AbstractFilter',
|
||||
|
|
@ -14,7 +15,6 @@ __all__ = [
|
|||
'ContentTypeFilter',
|
||||
'ExceptionsFilter',
|
||||
'HashTag',
|
||||
'FuncFilter',
|
||||
'Filter',
|
||||
'FilterNotPassed',
|
||||
'FilterRecord',
|
||||
|
|
@ -23,6 +23,8 @@ __all__ = [
|
|||
'Regexp',
|
||||
'StateFilter',
|
||||
'Text',
|
||||
'check_filter',
|
||||
'get_filter_spec',
|
||||
'get_filters_spec',
|
||||
'execute_filter',
|
||||
'check_filters'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import inspect
|
||||
import re
|
||||
import typing
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterable, Optional, Union
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
|
||||
from aiogram.types import CallbackQuery, Message, InlineQuery
|
||||
from aiogram.utils.deprecated import warn_deprecated
|
||||
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll
|
||||
|
||||
|
||||
class Command(Filter):
|
||||
"""
|
||||
You can handle commands by using this filter
|
||||
You can handle commands by using this filter.
|
||||
|
||||
If filter is successful processed the :obj:`Command.CommandObj` will be passed to the handler arguments.
|
||||
|
||||
By default this filter is registered for messages and edited messages handlers.
|
||||
"""
|
||||
|
||||
def __init__(self, commands: Union[Iterable, str],
|
||||
|
|
@ -20,12 +26,22 @@ class Command(Filter):
|
|||
ignore_case: bool = True,
|
||||
ignore_mention: bool = False):
|
||||
"""
|
||||
Filter can be initialized from filters factory or by simply creating instance of this class
|
||||
Filter can be initialized from filters factory or by simply creating instance of this class.
|
||||
|
||||
:param commands: command or list of commands
|
||||
:param prefixes:
|
||||
:param ignore_case:
|
||||
:param ignore_mention:
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@dp.message_handler(commands=['myCommand'])
|
||||
@dp.message_handler(Command(['myCommand']))
|
||||
@dp.message_handler(commands=['myCommand'], commands_prefix='!/')
|
||||
|
||||
:param commands: Command or list of commands always without leading slashes (prefix)
|
||||
:param prefixes: Allowed commands prefix. By default is slash.
|
||||
If you change the default behavior pass the list of prefixes to this argument.
|
||||
:param ignore_case: Ignore case of the command
|
||||
:param ignore_mention: Ignore mention in command
|
||||
(By default this filter pass only the commands addressed to current bot)
|
||||
"""
|
||||
if isinstance(commands, str):
|
||||
commands = (commands,)
|
||||
|
|
@ -40,15 +56,21 @@ class Command(Filter):
|
|||
"""
|
||||
Validator for filters factory
|
||||
|
||||
From filters factory this filter can be registered with arguments:
|
||||
|
||||
- ``command``
|
||||
- ``commands_prefix`` (will be passed as ``prefixes``)
|
||||
- ``commands_ignore_mention`` (will be passed as ``ignore_mention``
|
||||
|
||||
:param full_config:
|
||||
:return: config or empty dict
|
||||
"""
|
||||
config = {}
|
||||
if 'commands' in full_config:
|
||||
config['commands'] = full_config.pop('commands')
|
||||
if 'commands_prefix' in full_config:
|
||||
if config and 'commands_prefix' in full_config:
|
||||
config['prefixes'] = full_config.pop('commands_prefix')
|
||||
if 'commands_ignore_mention' in full_config:
|
||||
if config and 'commands_ignore_mention' in full_config:
|
||||
config['ignore_mention'] = full_config.pop('commands_ignore_mention')
|
||||
return config
|
||||
|
||||
|
|
@ -71,17 +93,37 @@ class Command(Filter):
|
|||
|
||||
@dataclass
|
||||
class CommandObj:
|
||||
"""
|
||||
Instance of this object is always has command and it prefix.
|
||||
|
||||
Can be passed as keyword argument ``command`` to the handler
|
||||
"""
|
||||
|
||||
"""Command prefix"""
|
||||
prefix: str = '/'
|
||||
"""Command without prefix and mention"""
|
||||
command: str = ''
|
||||
"""Mention (if available)"""
|
||||
mention: str = None
|
||||
"""Command argument"""
|
||||
args: str = field(repr=False, default=None)
|
||||
|
||||
@property
|
||||
def mentioned(self) -> bool:
|
||||
"""
|
||||
This command has mention?
|
||||
|
||||
:return:
|
||||
"""
|
||||
return bool(self.mention)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""
|
||||
Generate original text from object
|
||||
|
||||
:return:
|
||||
"""
|
||||
line = self.prefix + self.command
|
||||
if self.mentioned:
|
||||
line += '@' + self.mention
|
||||
|
|
@ -91,21 +133,69 @@ class Command(Filter):
|
|||
|
||||
|
||||
class CommandStart(Command):
|
||||
def __init__(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Also this filter can handle `deep-linking <https://core.telegram.org/bots#deep-linking>`_ arguments.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)')))
|
||||
|
||||
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
|
||||
"""
|
||||
super(CommandStart, self).__init__(['start'])
|
||||
self.deep_link = deep_link
|
||||
|
||||
async def check(self, message: types.Message):
|
||||
"""
|
||||
If deep-linking is passed to the filter result of the matching will be passed as ``deep_link`` to the handler
|
||||
|
||||
:param message:
|
||||
:return:
|
||||
"""
|
||||
check = await super(CommandStart, self).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
|
||||
|
||||
match = self.deep_link.match(message.get_args())
|
||||
if match:
|
||||
return {'deep_link': match}
|
||||
return False
|
||||
|
||||
return check
|
||||
|
||||
|
||||
class CommandHelp(Command):
|
||||
"""
|
||||
This filter based on :obj:`Command` filter but can handle only ``/help`` command.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CommandHelp, self).__init__(['help'])
|
||||
|
||||
|
||||
class CommandSettings(Command):
|
||||
"""
|
||||
This filter based on :obj:`Command` filter but can handle only ``/settings`` command.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CommandSettings, self).__init__(['settings'])
|
||||
|
||||
|
||||
class CommandPrivacy(Command):
|
||||
"""
|
||||
This filter based on :obj:`Command` filter but can handle only ``/privacy`` command.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CommandPrivacy, self).__init__(['privacy'])
|
||||
|
||||
|
|
@ -116,10 +206,10 @@ class Text(Filter):
|
|||
"""
|
||||
|
||||
def __init__(self,
|
||||
equals: Optional[str] = None,
|
||||
contains: Optional[str] = None,
|
||||
startswith: Optional[str] = None,
|
||||
endswith: Optional[str] = None,
|
||||
equals: Optional[Union[str, LazyProxy]] = None,
|
||||
contains: Optional[Union[str, LazyProxy]] = None,
|
||||
startswith: Optional[Union[str, LazyProxy]] = None,
|
||||
endswith: Optional[Union[str, LazyProxy]] = None,
|
||||
ignore_case=False):
|
||||
"""
|
||||
Check text for one of pattern. Only one mode can be used in one filter.
|
||||
|
|
@ -162,10 +252,14 @@ class Text(Filter):
|
|||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
|
||||
if isinstance(obj, Message):
|
||||
text = obj.text or obj.caption or ''
|
||||
if not text and obj.poll:
|
||||
text = obj.poll.question
|
||||
elif isinstance(obj, CallbackQuery):
|
||||
text = obj.data
|
||||
elif isinstance(obj, InlineQuery):
|
||||
text = obj.query
|
||||
elif isinstance(obj, Poll):
|
||||
text = obj.question
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -173,13 +267,13 @@ class Text(Filter):
|
|||
text = text.lower()
|
||||
|
||||
if self.equals:
|
||||
return text == self.equals
|
||||
return text == str(self.equals)
|
||||
elif self.contains:
|
||||
return self.contains in text
|
||||
return str(self.contains) in text
|
||||
elif self.startswith:
|
||||
return text.startswith(self.startswith)
|
||||
return text.startswith(str(self.startswith))
|
||||
elif self.endswith:
|
||||
return text.endswith(self.endswith)
|
||||
return text.endswith(str(self.endswith))
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -267,12 +361,16 @@ class Regexp(Filter):
|
|||
|
||||
async def check(self, obj: Union[Message, CallbackQuery]):
|
||||
if isinstance(obj, Message):
|
||||
match = self.regexp.search(obj.text or obj.caption or '')
|
||||
content = obj.text or obj.caption or ''
|
||||
if not content and obj.poll:
|
||||
content = obj.poll.question
|
||||
elif isinstance(obj, CallbackQuery) and obj.data:
|
||||
match = self.regexp.search(obj.data)
|
||||
content = obj.data
|
||||
else:
|
||||
return False
|
||||
|
||||
match = self.regexp.search(content)
|
||||
|
||||
if match:
|
||||
return {'regexp': match}
|
||||
return False
|
||||
|
|
@ -372,22 +470,6 @@ class StateFilter(BoundFilter):
|
|||
return False
|
||||
|
||||
|
||||
class FuncFilter(BoundFilter):
|
||||
key = 'func'
|
||||
|
||||
def __init__(self, dispatcher, func):
|
||||
self.dispatcher = dispatcher
|
||||
self.func = func
|
||||
|
||||
warn_deprecated('"func" filter will be removed in 2.1 version.\n'
|
||||
'Read mode: https://aiogram.readthedocs.io/en/dev-2.x/migration_1_to_2.html#custom-filters',
|
||||
stacklevel=8)
|
||||
|
||||
async def check(self, obj) -> bool:
|
||||
from .filters import check_filter
|
||||
return await check_filter(self.dispatcher, self.func, (obj,))
|
||||
|
||||
|
||||
class ExceptionsFilter(BoundFilter):
|
||||
"""
|
||||
Filter for exceptions
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from ..handler import Handler
|
|||
|
||||
class FiltersFactory:
|
||||
"""
|
||||
Default filters factory
|
||||
Filters factory
|
||||
"""
|
||||
|
||||
def __init__(self, dispatcher):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import abc
|
|||
import inspect
|
||||
import typing
|
||||
|
||||
from ..handler import Handler
|
||||
from ..handler import Handler, FilterObj
|
||||
|
||||
|
||||
class FilterNotPassed(Exception):
|
||||
|
|
@ -20,15 +20,7 @@ def wrap_async(func):
|
|||
return async_wrapper
|
||||
|
||||
|
||||
async def check_filter(dispatcher, filter_, args):
|
||||
"""
|
||||
Helper for executing filter
|
||||
|
||||
:param dispatcher:
|
||||
:param filter_:
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
def get_filter_spec(dispatcher, filter_: callable):
|
||||
kwargs = {}
|
||||
if not callable(filter_):
|
||||
raise TypeError('Filter must be callable and/or awaitable!')
|
||||
|
|
@ -39,16 +31,37 @@ async def check_filter(dispatcher, filter_, args):
|
|||
if inspect.isawaitable(filter_) \
|
||||
or inspect.iscoroutinefunction(filter_) \
|
||||
or isinstance(filter_, AbstractFilter):
|
||||
return await filter_(*args, **kwargs)
|
||||
return FilterObj(filter=filter_, kwargs=kwargs, is_async=True)
|
||||
else:
|
||||
return filter_(*args, **kwargs)
|
||||
return FilterObj(filter=filter_, kwargs=kwargs, is_async=False)
|
||||
|
||||
|
||||
async def check_filters(dispatcher, filters, args):
|
||||
def get_filters_spec(dispatcher, filters: typing.Iterable[callable]):
|
||||
data = []
|
||||
if filters is not None:
|
||||
for i in filters:
|
||||
data.append(get_filter_spec(dispatcher, i))
|
||||
return data
|
||||
|
||||
|
||||
async def execute_filter(filter_: FilterObj, args):
|
||||
"""
|
||||
Helper for executing filter
|
||||
|
||||
:param filter_:
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
if filter_.is_async:
|
||||
return await filter_.filter(*args, **filter_.kwargs)
|
||||
else:
|
||||
return filter_.filter(*args, **filter_.kwargs)
|
||||
|
||||
|
||||
async def check_filters(filters: typing.Iterable[FilterObj], args):
|
||||
"""
|
||||
Check list of filters
|
||||
|
||||
:param dispatcher:
|
||||
:param filters:
|
||||
:param args:
|
||||
:return:
|
||||
|
|
@ -56,7 +69,7 @@ async def check_filters(dispatcher, filters, args):
|
|||
data = {}
|
||||
if filters is not None:
|
||||
for filter_ in filters:
|
||||
f = await check_filter(dispatcher, filter_, args)
|
||||
f = await execute_filter(filter_, args)
|
||||
if not f:
|
||||
raise FilterNotPassed()
|
||||
elif isinstance(f, dict):
|
||||
|
|
@ -115,24 +128,29 @@ class FilterRecord:
|
|||
|
||||
class AbstractFilter(abc.ABC):
|
||||
"""
|
||||
Abstract class for custom filters
|
||||
Abstract class for custom filters.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
"""
|
||||
Validate and parse config
|
||||
Validate and parse config.
|
||||
|
||||
:param full_config:
|
||||
:return: config
|
||||
This method will be called by the filters factory when you bind this filter.
|
||||
Must be overridden.
|
||||
|
||||
:param full_config: dict with arguments passed to handler registrar
|
||||
:return: Current filter config
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def check(self, *args) -> bool:
|
||||
"""
|
||||
Check object
|
||||
Will be called when filters checks.
|
||||
|
||||
This method must be overridden.
|
||||
|
||||
:param args:
|
||||
:return:
|
||||
|
|
@ -160,24 +178,46 @@ class AbstractFilter(abc.ABC):
|
|||
|
||||
class Filter(AbstractFilter):
|
||||
"""
|
||||
You can make subclasses of that class for custom filters
|
||||
You can make subclasses of that class for custom filters.
|
||||
|
||||
Method ``check`` must be overridden
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
"""
|
||||
Here method ``validate`` is optional.
|
||||
If you need to use filter from filters factory you need to override this method.
|
||||
|
||||
:param full_config: dict with arguments passed to handler registrar
|
||||
:return: Current filter config
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BoundFilter(Filter):
|
||||
"""
|
||||
Base class for filters with default validator
|
||||
To easily create your own filters with one parameter, you can inherit from this 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"""
|
||||
required = False
|
||||
"""Default value for configure required filters"""
|
||||
default = None
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
If ``cls.key`` is not :obj:`None` and that is in config returns config with that argument.
|
||||
|
||||
:param full_config:
|
||||
:return:
|
||||
"""
|
||||
if cls.key is not None:
|
||||
if cls.key in full_config:
|
||||
return {cls.key: full_config[cls.key]}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import inspect
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Iterable
|
||||
|
||||
ctx_data = ContextVar('ctx_handler_data')
|
||||
current_handler = ContextVar('current_handler')
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilterObj:
|
||||
filter: callable
|
||||
kwargs: dict
|
||||
is_async: bool
|
||||
|
||||
|
||||
class SkipHandler(Exception):
|
||||
pass
|
||||
|
||||
|
|
@ -13,11 +22,15 @@ class CancelHandler(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def _check_spec(func: callable, kwargs: dict):
|
||||
def _get_spec(func: callable):
|
||||
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
|
||||
func = func.__wrapped__
|
||||
|
||||
spec = inspect.getfullargspec(func)
|
||||
return spec, func
|
||||
|
||||
|
||||
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
|
||||
if spec.varkw:
|
||||
return kwargs
|
||||
|
||||
|
|
@ -33,6 +46,7 @@ class Handler:
|
|||
self.middleware_key = middleware_key
|
||||
|
||||
def register(self, handler, filters=None, index=None):
|
||||
from .filters import get_filters_spec
|
||||
"""
|
||||
Register callback
|
||||
|
||||
|
|
@ -42,9 +56,13 @@ class Handler:
|
|||
:param filters: list of filters
|
||||
:param index: you can reorder handlers
|
||||
"""
|
||||
spec, handler = _get_spec(handler)
|
||||
|
||||
if filters and not isinstance(filters, (list, tuple, set)):
|
||||
filters = [filters]
|
||||
record = (filters, handler)
|
||||
filters = get_filters_spec(self.dispatcher, filters)
|
||||
|
||||
record = Handler.HandlerObj(handler=handler, spec=spec, filters=filters)
|
||||
if index is None:
|
||||
self.handlers.append(record)
|
||||
else:
|
||||
|
|
@ -57,10 +75,10 @@ class Handler:
|
|||
:param handler: callback
|
||||
:return:
|
||||
"""
|
||||
for handler_with_filters in self.handlers:
|
||||
_, registered = handler_with_filters
|
||||
for handler_obj in self.handlers:
|
||||
registered = handler_obj.handler
|
||||
if handler is registered:
|
||||
self.handlers.remove(handler_with_filters)
|
||||
self.handlers.remove(handler_obj)
|
||||
return True
|
||||
raise ValueError('This handler is not registered!')
|
||||
|
||||
|
|
@ -85,18 +103,18 @@ class Handler:
|
|||
return results
|
||||
|
||||
try:
|
||||
for filters, handler in self.handlers:
|
||||
for handler_obj in self.handlers:
|
||||
try:
|
||||
data.update(await check_filters(self.dispatcher, filters, args))
|
||||
data.update(await check_filters(handler_obj.filters, args))
|
||||
except FilterNotPassed:
|
||||
continue
|
||||
else:
|
||||
ctx_token = current_handler.set(handler)
|
||||
ctx_token = current_handler.set(handler_obj.handler)
|
||||
try:
|
||||
if self.middleware_key:
|
||||
await self.dispatcher.middleware.trigger(f"process_{self.middleware_key}", args + (data,))
|
||||
partial_data = _check_spec(handler, data)
|
||||
response = await handler(*args, **partial_data)
|
||||
partial_data = _check_spec(handler_obj.spec, data)
|
||||
response = await handler_obj.handler(*args, **partial_data)
|
||||
if response is not None:
|
||||
results.append(response)
|
||||
if self.once:
|
||||
|
|
@ -113,3 +131,9 @@ class Handler:
|
|||
args + (results, data,))
|
||||
|
||||
return results
|
||||
|
||||
@dataclass
|
||||
class HandlerObj:
|
||||
handler: callable
|
||||
spec: inspect.FullArgSpec
|
||||
filters: Optional[Iterable[FilterObj]] = None
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import asyncio.tasks
|
|||
import datetime
|
||||
import functools
|
||||
import ipaddress
|
||||
import itertools
|
||||
import typing
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGone
|
||||
|
||||
|
||||
from .. import types
|
||||
from ..bot import api
|
||||
from ..types import ParseMode
|
||||
|
|
@ -30,8 +30,8 @@ WEBHOOK = 'webhook'
|
|||
WEBHOOK_CONNECTION = 'WEBHOOK_CONNECTION'
|
||||
WEBHOOK_REQUEST = 'WEBHOOK_REQUEST'
|
||||
|
||||
TELEGRAM_IP_LOWER = ipaddress.IPv4Address('149.154.167.197')
|
||||
TELEGRAM_IP_UPPER = ipaddress.IPv4Address('149.154.167.233')
|
||||
TELEGRAM_SUBNET_1 = ipaddress.IPv4Network('149.154.160.0/20')
|
||||
TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22')
|
||||
|
||||
allowed_ips = set()
|
||||
|
||||
|
|
@ -47,18 +47,26 @@ def _check_ip(ip: str) -> bool:
|
|||
return address in allowed_ips
|
||||
|
||||
|
||||
def allow_ip(*ips: str):
|
||||
def allow_ip(*ips: typing.Union[str, ipaddress.IPv4Network, ipaddress.IPv4Address]):
|
||||
"""
|
||||
Allow ip address.
|
||||
|
||||
:param ips:
|
||||
:return:
|
||||
"""
|
||||
allowed_ips.update(ipaddress.IPv4Address(ip) for ip in ips)
|
||||
for ip in ips:
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
allowed_ips.add(ip)
|
||||
elif isinstance(ip, str):
|
||||
allowed_ips.add(ipaddress.IPv4Address(ip))
|
||||
elif isinstance(ip, ipaddress.IPv4Network):
|
||||
allowed_ips.update(ip.hosts())
|
||||
else:
|
||||
raise ValueError(f"Bad type of ipaddress: {type(ip)} ('{ip}')")
|
||||
|
||||
|
||||
# Allow access from Telegram servers
|
||||
allow_ip(*(ip for ip in range(int(TELEGRAM_IP_LOWER), int(TELEGRAM_IP_UPPER) + 1)))
|
||||
allow_ip(TELEGRAM_SUBNET_1, TELEGRAM_SUBNET_2)
|
||||
|
||||
|
||||
class WebhookRequestHandler(web.View):
|
||||
|
|
@ -69,7 +77,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'
|
||||
|
|
@ -165,7 +173,7 @@ class WebhookRequestHandler(web.View):
|
|||
timeout_handle = loop.call_later(RESPONSE_TIMEOUT, asyncio.tasks._release_waiter, waiter)
|
||||
cb = functools.partial(asyncio.tasks._release_waiter, waiter)
|
||||
|
||||
fut = asyncio.ensure_future(dispatcher.process_update(update), loop=loop)
|
||||
fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update), loop=loop)
|
||||
fut.add_done_callback(cb)
|
||||
|
||||
try:
|
||||
|
|
@ -219,7 +227,7 @@ class WebhookRequestHandler(web.View):
|
|||
"""
|
||||
if results is None:
|
||||
return None
|
||||
for result in results:
|
||||
for result in itertools.chain.from_iterable(results):
|
||||
if isinstance(result, BaseResponse):
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from .input_message_content import InputContactMessageContent, InputLocationMess
|
|||
from .invoice import Invoice
|
||||
from .labeled_price import LabeledPrice
|
||||
from .location import Location
|
||||
from .login_url import LoginUrl
|
||||
from .mask_position import MaskPosition
|
||||
from .message import ContentType, ContentTypes, Message, ParseMode
|
||||
from .message_entity import MessageEntity, MessageEntityType
|
||||
|
|
@ -43,6 +44,7 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa
|
|||
PassportElementErrorSelfie
|
||||
from .passport_file import PassportFile
|
||||
from .photo_size import PhotoSize
|
||||
from .poll import PollOption, Poll
|
||||
from .pre_checkout_query import PreCheckoutQuery
|
||||
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
|
||||
from .response_parameters import ResponseParameters
|
||||
|
|
@ -125,6 +127,7 @@ __all__ = (
|
|||
'KeyboardButton',
|
||||
'LabeledPrice',
|
||||
'Location',
|
||||
'LoginUrl',
|
||||
'MaskPosition',
|
||||
'MediaGroup',
|
||||
'Message',
|
||||
|
|
@ -142,6 +145,8 @@ __all__ = (
|
|||
'PassportElementErrorSelfie',
|
||||
'PassportFile',
|
||||
'PhotoSize',
|
||||
'Poll',
|
||||
'PollOption',
|
||||
'PreCheckoutQuery',
|
||||
'ReplyKeyboardMarkup',
|
||||
'ReplyKeyboardRemove',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import io
|
|||
import typing
|
||||
from typing import TypeVar
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
from .fields import BaseField
|
||||
from ..utils import json
|
||||
from ..utils.mixins import ContextInstanceMixin
|
||||
|
|
@ -142,7 +144,13 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
@property
|
||||
def bot(self):
|
||||
from ..bot.bot import Bot
|
||||
return Bot.get_current()
|
||||
|
||||
bot = Bot.get_current()
|
||||
if bot is None:
|
||||
raise RuntimeError("Can't get bot instance from context. "
|
||||
"You can fix it with setting current instance: "
|
||||
"'Bot.set_current(bot_instance)'")
|
||||
return bot
|
||||
|
||||
def to_python(self) -> typing.Dict:
|
||||
"""
|
||||
|
|
@ -157,6 +165,8 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
|
|||
value = self.props[name].export(self)
|
||||
if isinstance(value, TelegramObject):
|
||||
value = value.to_python()
|
||||
if isinstance(value, LazyProxy):
|
||||
value = str(value)
|
||||
result[self.props_aliases.get(name, name)] = value
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import warnings
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
|
|
@ -24,15 +25,22 @@ class ChatMember(base.TelegramObject):
|
|||
can_restrict_members: base.Boolean = fields.Field()
|
||||
can_pin_messages: base.Boolean = fields.Field()
|
||||
can_promote_members: base.Boolean = fields.Field()
|
||||
is_member: base.Boolean = fields.Field()
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: 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.3',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return self.is_chat_admin()
|
||||
|
||||
def is_chat_admin(self):
|
||||
return ChatMemberStatus.is_admin(self.status)
|
||||
|
||||
def is_member(self):
|
||||
def is_chat_member(self):
|
||||
return ChatMemberStatus.is_member(self.status)
|
||||
|
||||
def __int__(self):
|
||||
|
|
@ -54,8 +62,22 @@ class ChatMemberStatus(helper.Helper):
|
|||
|
||||
@classmethod
|
||||
def is_admin(cls, role):
|
||||
return role in [cls.ADMINISTRATOR, cls.CREATOR]
|
||||
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.3',
|
||||
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.3',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return cls.is_chat_member(role)
|
||||
|
||||
@classmethod
|
||||
def is_chat_admin(cls, role):
|
||||
return role in [cls.ADMINISTRATOR, cls.CREATOR]
|
||||
|
||||
@classmethod
|
||||
def is_chat_member(cls, role):
|
||||
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import typing
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
import typing
|
||||
from .passport_file import PassportFile
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import typing
|
|||
from . import base
|
||||
from . import fields
|
||||
from .callback_game import CallbackGame
|
||||
from .login_url import LoginUrl
|
||||
|
||||
|
||||
class InlineKeyboardMarkup(base.TelegramObject):
|
||||
|
|
@ -16,10 +17,16 @@ class InlineKeyboardMarkup(base.TelegramObject):
|
|||
"""
|
||||
inline_keyboard: 'typing.List[typing.List[InlineKeyboardButton]]' = fields.ListOfLists(base='InlineKeyboardButton')
|
||||
|
||||
def __init__(self, row_width=3, inline_keyboard=None):
|
||||
def __init__(self, row_width=3, inline_keyboard=None, **kwargs):
|
||||
if inline_keyboard is None:
|
||||
inline_keyboard = []
|
||||
super(InlineKeyboardMarkup, self).__init__(conf={'row_width': row_width}, inline_keyboard=inline_keyboard)
|
||||
|
||||
conf = kwargs.pop('conf', {}) or {}
|
||||
conf['row_width'] = row_width
|
||||
|
||||
super(InlineKeyboardMarkup, self).__init__(**kwargs,
|
||||
conf=conf,
|
||||
inline_keyboard=inline_keyboard)
|
||||
|
||||
@property
|
||||
def row_width(self):
|
||||
|
|
@ -84,16 +91,26 @@ class InlineKeyboardButton(base.TelegramObject):
|
|||
"""
|
||||
text: base.String = fields.Field()
|
||||
url: base.String = fields.Field()
|
||||
login_url: LoginUrl = fields.Field(base=LoginUrl)
|
||||
callback_data: base.String = fields.Field()
|
||||
switch_inline_query: base.String = fields.Field()
|
||||
switch_inline_query_current_chat: base.String = fields.Field()
|
||||
callback_game: CallbackGame = fields.Field(base=CallbackGame)
|
||||
pay: base.Boolean = fields.Field()
|
||||
|
||||
def __init__(self, text: base.String, url: base.String = None, callback_data: base.String = None,
|
||||
switch_inline_query: base.String = None, switch_inline_query_current_chat: base.String = None,
|
||||
callback_game: CallbackGame = None, pay: base.Boolean = None):
|
||||
super(InlineKeyboardButton, self).__init__(text=text, url=url, callback_data=callback_data,
|
||||
def __init__(self, text: base.String,
|
||||
url: base.String = None,
|
||||
login_url: LoginUrl = None,
|
||||
callback_data: base.String = None,
|
||||
switch_inline_query: base.String = None,
|
||||
switch_inline_query_current_chat: base.String = None,
|
||||
callback_game: CallbackGame = None,
|
||||
pay: base.Boolean = None, **kwargs):
|
||||
super(InlineKeyboardButton, self).__init__(text=text,
|
||||
url=url,
|
||||
login_url=login_url,
|
||||
callback_data=callback_data,
|
||||
switch_inline_query=switch_inline_query,
|
||||
switch_inline_query_current_chat=switch_inline_query_current_chat,
|
||||
callback_game=callback_game, pay=pay)
|
||||
callback_game=callback_game,
|
||||
pay=pay, **kwargs)
|
||||
|
|
|
|||
30
aiogram/types/login_url.py
Normal file
30
aiogram/types/login_url.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from . import base
|
||||
from . import fields
|
||||
|
||||
|
||||
class LoginUrl(base.TelegramObject):
|
||||
"""
|
||||
This object represents a parameter of the inline keyboard button used to automatically authorize a user.
|
||||
Serves as a great replacement for the Telegram Login Widget when the user is coming from Telegram.
|
||||
All the user needs to do is tap/click a button and confirm that they want to log in.
|
||||
|
||||
https://core.telegram.org/bots/api#loginurl
|
||||
"""
|
||||
url: base.String = fields.Field()
|
||||
forward_text: base.String = fields.Field()
|
||||
bot_username: base.String = fields.Field()
|
||||
request_write_access: base.Boolean = fields.Field()
|
||||
|
||||
def __init__(self,
|
||||
url: base.String,
|
||||
forward_text: base.String = None,
|
||||
bot_username: base.String = None,
|
||||
request_write_access: base.Boolean = None,
|
||||
**kwargs):
|
||||
super(LoginUrl, self).__init__(
|
||||
url=url,
|
||||
forward_text=forward_text,
|
||||
bot_username=bot_username,
|
||||
request_write_access=request_write_access,
|
||||
**kwargs
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
@ -45,5 +45,18 @@ class Downloadable:
|
|||
else:
|
||||
return await self.bot.get_file(self.file_id)
|
||||
|
||||
async def get_url(self):
|
||||
"""
|
||||
Get file url.
|
||||
|
||||
Attention!!
|
||||
This method has security vulnerabilities for the reason that result
|
||||
contains bot's *access token* in open form. Use at your own risk!
|
||||
|
||||
:return: url
|
||||
"""
|
||||
file = await self.get_file()
|
||||
return self.bot.get_file_url(file.file_path)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.file_id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import typing
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
import typing
|
||||
from .encrypted_passport_element import EncryptedPassportElement
|
||||
from .encrypted_credentials import EncryptedCredentials
|
||||
from .encrypted_passport_element import EncryptedPassportElement
|
||||
|
||||
|
||||
class PassportData(base.TelegramObject):
|
||||
|
|
|
|||
16
aiogram/types/poll.py
Normal file
16
aiogram/types/poll.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import typing
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
|
||||
|
||||
class PollOption(base.TelegramObject):
|
||||
text: base.String = fields.Field()
|
||||
voter_count: base.Integer = fields.Field()
|
||||
|
||||
|
||||
class Poll(base.TelegramObject):
|
||||
id: base.String = fields.Field()
|
||||
question: base.String = fields.Field()
|
||||
options: typing.List[PollOption] = fields.ListField(base=PollOption)
|
||||
is_closed: base.Boolean = fields.Field()
|
||||
|
|
@ -6,6 +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 .pre_checkout_query import PreCheckoutQuery
|
||||
from .shipping_query import ShippingQuery
|
||||
from ..utils import helper
|
||||
|
|
@ -28,6 +29,7 @@ class Update(base.TelegramObject):
|
|||
callback_query: CallbackQuery = fields.Field(base=CallbackQuery)
|
||||
shipping_query: ShippingQuery = fields.Field(base=ShippingQuery)
|
||||
pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery)
|
||||
poll: Poll = fields.Field(base=Poll)
|
||||
|
||||
def __hash__(self):
|
||||
return self.update_id
|
||||
|
|
|
|||
|
|
@ -14,4 +14,3 @@ class Venue(base.TelegramObject):
|
|||
address: base.String = fields.Field()
|
||||
foursquare_id: base.String = fields.Field()
|
||||
foursquare_type: base.String = fields.Field()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ for more information https://core.telegram.org/widgets/login#checking-authorizat
|
|||
|
||||
Source: https://gist.github.com/JrooTJunior/887791de7273c9df5277d2b1ecadc839
|
||||
"""
|
||||
import collections
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
import collections
|
||||
|
||||
|
||||
def generate_hash(data: dict, token: str) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -31,10 +31,8 @@ class CallbackData:
|
|||
raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}")
|
||||
elif not prefix:
|
||||
raise ValueError('Prefix can\'t be empty')
|
||||
elif len(sep) != 1:
|
||||
raise ValueError(f"Length of sep should be equals to 1")
|
||||
elif sep in prefix:
|
||||
raise ValueError(f"Symbol '{sep}' can't be used in prefix")
|
||||
raise ValueError(f"Separator '{sep}' can't be used in prefix")
|
||||
elif not parts:
|
||||
raise TypeError('Parts is not passed!')
|
||||
|
||||
|
|
@ -57,15 +55,16 @@ class CallbackData:
|
|||
|
||||
for part in self._part_names:
|
||||
value = kwargs.pop(part, None)
|
||||
if not value:
|
||||
if value is None:
|
||||
if args:
|
||||
value = args.pop(0)
|
||||
else:
|
||||
raise ValueError(f"Value for '{part}' is not passed!")
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Value must be instance of str not {type(value).__name__}")
|
||||
elif not value:
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -12,11 +12,23 @@ TelegramAPIError
|
|||
MessageCantBeEdited
|
||||
MessageCantBeDeleted
|
||||
MessageToEditNotFound
|
||||
MessageToReplyNotFound
|
||||
ToMuchMessages
|
||||
PollError
|
||||
PollCantBeStopped
|
||||
PollHasAlreadyClosed
|
||||
PollsCantBeSentToPrivateChats
|
||||
PollSizeError
|
||||
PollMustHaveMoreOptions
|
||||
PollCantHaveMoreOptions
|
||||
PollsOptionsLengthTooLong
|
||||
PollOptionsMustBeNonEmpty
|
||||
PollQuestionMustBeNonEmpty
|
||||
MessageWithPollNotFound (with MessageError)
|
||||
MessageIsNotAPoll (with MessageError)
|
||||
ObjectExpectedAsReplyMarkup
|
||||
InlineKeyboardExpected
|
||||
ChatNotFound
|
||||
ChatIdIsEmpty
|
||||
ChatDescriptionIsNotModified
|
||||
InvalidQueryID
|
||||
InvalidPeerID
|
||||
|
|
@ -31,14 +43,15 @@ TelegramAPIError
|
|||
WebhookRequireHTTPS
|
||||
BadWebhookPort
|
||||
BadWebhookAddrInfo
|
||||
CantParseUrl
|
||||
BadWebhookNoAddressAssociatedWithHostname
|
||||
NotFound
|
||||
MethodNotKnown
|
||||
PhotoAsInputFileRequired
|
||||
InvalidStickersSet
|
||||
NoStickerInRequest
|
||||
ChatAdminRequired
|
||||
NotEnoughRightsToPinMessage
|
||||
NeedAdministratorRightsInTheChannel
|
||||
MethodNotAvailableInPrivateChats
|
||||
CantDemoteChatCreator
|
||||
CantRestrictSelf
|
||||
NotEnoughRightsToRestrict
|
||||
|
|
@ -49,7 +62,9 @@ TelegramAPIError
|
|||
PaymentProviderInvalid
|
||||
CurrencyTotalAmountInvalid
|
||||
CantParseUrl
|
||||
UnsupportedUrlProtocol
|
||||
CantParseEntities
|
||||
ResultIdDuplicate
|
||||
ConflictError
|
||||
TerminatedByOtherGetUpdates
|
||||
CantGetUpdates
|
||||
|
|
@ -64,6 +79,10 @@ TelegramAPIError
|
|||
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
|
||||
"""
|
||||
|
|
@ -71,7 +90,7 @@ import time
|
|||
|
||||
# TODO: Use exceptions detector from `aiograph`.
|
||||
|
||||
_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: ', 'Not Found: ']
|
||||
_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: ']
|
||||
|
||||
|
||||
def _clean_message(text):
|
||||
|
|
@ -164,6 +183,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'
|
||||
|
||||
|
|
@ -174,7 +200,7 @@ class MessageTextIsEmpty(MessageError):
|
|||
|
||||
class MessageCantBeEdited(MessageError):
|
||||
match = 'message can\'t be edited'
|
||||
|
||||
|
||||
|
||||
class MessageCantBeDeleted(MessageError):
|
||||
match = 'message can\'t be deleted'
|
||||
|
|
@ -203,6 +229,64 @@ class InlineKeyboardExpected(BadRequest):
|
|||
match = 'inline keyboard expected'
|
||||
|
||||
|
||||
class PollError(BadRequest):
|
||||
__group = True
|
||||
|
||||
|
||||
class PollCantBeStopped(PollError):
|
||||
match = "poll can't be stopped"
|
||||
|
||||
|
||||
class PollHasAlreadyBeenClosed(PollError):
|
||||
match = 'poll has already been closed'
|
||||
|
||||
|
||||
class PollsCantBeSentToPrivateChats(PollError):
|
||||
match = "polls can't be sent to private chats"
|
||||
|
||||
|
||||
class PollSizeError(PollError):
|
||||
__group = True
|
||||
|
||||
|
||||
class PollMustHaveMoreOptions(PollSizeError):
|
||||
match = "poll must have at least 2 option"
|
||||
|
||||
|
||||
class PollCantHaveMoreOptions(PollSizeError):
|
||||
match = "poll can't have more than 10 options"
|
||||
|
||||
|
||||
class PollOptionsMustBeNonEmpty(PollSizeError):
|
||||
match = "poll options must be non-empty"
|
||||
|
||||
|
||||
class PollQuestionMustBeNonEmpty(PollSizeError):
|
||||
match = "poll question must be non-empty"
|
||||
|
||||
|
||||
class PollOptionsLengthTooLong(PollSizeError):
|
||||
match = "poll options length must not exceed 100"
|
||||
|
||||
|
||||
class PollQuestionLengthTooLong(PollSizeError):
|
||||
match = "poll question length must not exceed 255"
|
||||
|
||||
|
||||
class MessageWithPollNotFound(PollError, MessageError):
|
||||
"""
|
||||
Will be raised when you try to stop poll with message without poll
|
||||
"""
|
||||
match = 'message with poll to stop not found'
|
||||
|
||||
|
||||
class MessageIsNotAPoll(PollError, MessageError):
|
||||
"""
|
||||
Will be raised when you try to stop poll with message without poll
|
||||
"""
|
||||
match = 'message is not a poll'
|
||||
|
||||
|
||||
class ChatNotFound(BadRequest):
|
||||
match = 'chat not found'
|
||||
|
||||
|
|
@ -211,13 +295,17 @@ class ChatIdIsEmpty(BadRequest):
|
|||
match = 'chat_id is empty'
|
||||
|
||||
|
||||
class InvalidUserId(BadRequest):
|
||||
match = 'user_id_invalid'
|
||||
text = 'Invalid user id'
|
||||
|
||||
|
||||
class ChatDescriptionIsNotModified(BadRequest):
|
||||
match = 'chat description is not modified'
|
||||
|
||||
|
||||
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):
|
||||
|
|
@ -277,10 +365,19 @@ class ChatAdminRequired(BadRequest):
|
|||
text = 'Admin permissions is required!'
|
||||
|
||||
|
||||
class NeedAdministratorRightsInTheChannel(BadRequest):
|
||||
match = 'need administrator rights in the channel chat'
|
||||
text = 'Admin permissions is required!'
|
||||
|
||||
|
||||
class NotEnoughRightsToPinMessage(BadRequest):
|
||||
match = 'not enough rights to pin a message'
|
||||
|
||||
|
||||
class MethodNotAvailableInPrivateChats(BadRequest):
|
||||
match = 'method is available only for supergroups and channel'
|
||||
|
||||
|
||||
class CantDemoteChatCreator(BadRequest):
|
||||
match = 'can\'t demote chat creator'
|
||||
|
||||
|
|
@ -340,14 +437,32 @@ class BadWebhookAddrInfo(BadWebhook):
|
|||
text = 'bad webhook: ' + match
|
||||
|
||||
|
||||
class BadWebhookNoAddressAssociatedWithHostname(BadWebhook):
|
||||
match = 'failed to resolve host: no address associated with hostname'
|
||||
|
||||
|
||||
class CantParseUrl(BadRequest):
|
||||
match = 'can\'t parse URL'
|
||||
|
||||
|
||||
class UnsupportedUrlProtocol(BadRequest):
|
||||
match = 'unsupported URL protocol'
|
||||
|
||||
|
||||
class CantParseEntities(BadRequest):
|
||||
match = 'can\'t parse entities'
|
||||
|
||||
|
||||
class ResultIdDuplicate(BadRequest):
|
||||
match = 'result_id_duplicate'
|
||||
text = 'Result ID duplicate'
|
||||
|
||||
|
||||
class BotDomainInvalid(BadRequest):
|
||||
match = 'bot_domain_invalid'
|
||||
text = 'Invalid bot domain'
|
||||
|
||||
|
||||
class NotFound(TelegramAPIError, _MatchErrorMixin):
|
||||
__group = True
|
||||
|
||||
|
|
@ -375,7 +490,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):
|
||||
|
|
@ -429,5 +544,5 @@ class Throttled(TelegramAPIError):
|
|||
|
||||
def __str__(self):
|
||||
return f"Rate limit exceeded! (Limit: {self.rate} s, " \
|
||||
f"exceeded: {self.exceeded_count}, " \
|
||||
f"time delta: {round(self.delta, 3)} s)"
|
||||
f"exceeded: {self.exceeded_count}, " \
|
||||
f"time delta: {round(self.delta, 3)} s)"
|
||||
|
|
|
|||
|
|
@ -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=None, fast=True):
|
||||
on_startup=None, on_shutdown=None, timeout=20, fast=True):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -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=None, fast=True):
|
||||
def start_polling(self, reset_webhook=None, timeout=20, fast=True):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
|
|||
|
|
@ -45,4 +45,3 @@ class ContextInstanceMixin:
|
|||
if not isinstance(value, cls):
|
||||
raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'")
|
||||
cls.__context_instance.set(value)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import datetime
|
||||
import secrets
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
from aiogram import types
|
||||
from . import json
|
||||
|
||||
|
|
@ -57,6 +59,8 @@ def prepare_arg(value):
|
|||
return int((now + value).timestamp())
|
||||
elif isinstance(value, datetime.datetime):
|
||||
return round(value.timestamp())
|
||||
elif isinstance(value, LazyProxy):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
-r requirements.txt
|
||||
|
||||
ujson>=1.35
|
||||
python-rapidjson>=0.6.3
|
||||
emoji>=0.5.0
|
||||
pytest>=3.5.0
|
||||
pytest-asyncio>=0.8.0
|
||||
tox>=3.0.0
|
||||
aresponses>=1.0.0
|
||||
uvloop>=0.9.1
|
||||
aioredis>=1.1.0
|
||||
wheel>=0.31.0
|
||||
rethinkdb>=2.3.0
|
||||
sphinx>=1.7.3
|
||||
sphinx-rtd-theme>=0.3.0
|
||||
sphinxcontrib-programoutput>=0.11
|
||||
aresponses>=1.0.0
|
||||
aiohttp-socks>=0.1.5
|
||||
rethinkdb>=2.4.1
|
||||
python-rapidjson>=0.7.0
|
||||
emoji>=0.5.2
|
||||
pytest>=4.4.1,<4.6
|
||||
pytest-asyncio>=0.10.0
|
||||
tox>=3.9.0
|
||||
aresponses>=1.1.1
|
||||
uvloop>=0.12.2
|
||||
aioredis>=1.2.0
|
||||
wheel>=0.31.1
|
||||
sphinx>=2.0.1
|
||||
sphinx-rtd-theme>=0.4.3
|
||||
sphinxcontrib-programoutput>=0.14
|
||||
aiohttp-socks>=0.2.2
|
||||
rethinkdb>=2.4.1
|
||||
|
|
|
|||
|
|
@ -4,16 +4,155 @@ Filters
|
|||
|
||||
Basics
|
||||
======
|
||||
Coming soon...
|
||||
|
||||
Filter factory greatly simplifies the reuse of filters when registering handlers.
|
||||
|
||||
Filters factory
|
||||
===============
|
||||
Coming soon...
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.factory.FiltersFactory
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Builtin filters
|
||||
===============
|
||||
Coming soon...
|
||||
``aiogram`` has some builtin filters. Here you can see all of them:
|
||||
|
||||
Command
|
||||
-------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.Command
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
CommandStart
|
||||
------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandStart
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
CommandHelp
|
||||
-----------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandHelp
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
CommandSettings
|
||||
---------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandSettings
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
CommandPrivacy
|
||||
--------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandPrivacy
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Text
|
||||
----
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.Text
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
HashTag
|
||||
-------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.HashTag
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Regexp
|
||||
------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.Regexp
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
RegexpCommandsFilter
|
||||
--------------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.RegexpCommandsFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
ContentTypeFilter
|
||||
-----------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.ContentTypeFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
StateFilter
|
||||
-----------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.StateFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
ExceptionsFilter
|
||||
----------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.ExceptionsFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Making own filters (Custom filters)
|
||||
===================================
|
||||
Coming soon...
|
||||
|
||||
Own filter can be:
|
||||
|
||||
- any callable object
|
||||
- any async function
|
||||
- any anonymous function (Example: ``lambda msg: msg.text == 'spam'``)
|
||||
- Subclass of :obj:`AbstractFilter`, :obj:`Filter` or :obj:`BoundFilter`
|
||||
|
||||
|
||||
AbstractFilter
|
||||
--------------
|
||||
.. autoclass:: aiogram.dispatcher.filters.filters.AbstractFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Filter
|
||||
------
|
||||
.. autoclass:: aiogram.dispatcher.filters.filters.Filter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
BoundFilter
|
||||
-----------
|
||||
.. autoclass:: aiogram.dispatcher.filters.filters.BoundFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ChatIdFilter(BoundFilter):
|
||||
key = 'chat_id'
|
||||
|
||||
def __init__(self, chat_id: typing.Union[typing.Iterable, int]):
|
||||
if isinstance(chat_id, int):
|
||||
chat_id = [chat_id]
|
||||
self.chat_id = chat_id
|
||||
|
||||
def check(self, message: types.Message) -> bool:
|
||||
return message.chat.id in self.chat_id
|
||||
|
||||
|
||||
dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers])
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ Welcome to aiogram's documentation!
|
|||
===================================
|
||||
|
||||
|
||||
.. image:: https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square
|
||||
:target: https://t.me/aiogram_live
|
||||
:alt: [Telegram] aiogram live
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/aiogram.svg?style=flat-square
|
||||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: PyPi Package Version
|
||||
|
|
@ -10,13 +14,17 @@ Welcome to aiogram's documentation!
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: PyPi status
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square
|
||||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: PyPi downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square
|
||||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square
|
||||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: PyPi downloads
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-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
|
||||
|
|
|
|||
|
|
@ -22,19 +22,19 @@ Next step: interaction with bots starts with one command. Register your first co
|
|||
|
||||
.. literalinclude:: ../../examples/echo_bot.py
|
||||
:language: python
|
||||
:lines: 21-25
|
||||
:lines: 20-25
|
||||
|
||||
If you want to handle all messages in the chat simply add handler without filters:
|
||||
|
||||
.. literalinclude:: ../../examples/echo_bot.py
|
||||
:language: python
|
||||
:lines: 28-30
|
||||
:lines: 35-37
|
||||
|
||||
Last step: run long polling.
|
||||
|
||||
.. literalinclude:: ../../examples/echo_bot.py
|
||||
:language: python
|
||||
:lines: 33-34
|
||||
:lines: 40-41
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ def get_keyboard() -> types.InlineKeyboardMarkup:
|
|||
|
||||
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']}"
|
||||
f"{md.quote_html(post['body'])}\n" \
|
||||
f"\n" \
|
||||
f"Votes: {post['votes']}"
|
||||
|
||||
markup = types.InlineKeyboardMarkup()
|
||||
markup.row(
|
||||
|
|
|
|||
|
|
@ -112,8 +112,8 @@ async def process_gender(message: types.Message, state: FSMContext):
|
|||
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__':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -51,6 +64,21 @@ async def cmd_start(message: types.Message):
|
|||
async def cmd_lang(message: types.Message, locale):
|
||||
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 pseudo numeric value
|
||||
TOTAL_LIKES = 0
|
||||
|
||||
@dp.message_handler(commands=['like'])
|
||||
async def cmd_like(message: types.Message, locale):
|
||||
TOTAL_LIKES += 1
|
||||
|
||||
# NOTE: This is comment for a translator
|
||||
await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES))
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
56
examples/inline_keyboard_example.py
Normal file
56
examples/inline_keyboard_example.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
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)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 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
|
||||
|
||||
keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'),
|
||||
# in real life for the callback_data the callback data factory should be used
|
||||
# here the raw string is used for the simplicity
|
||||
types.InlineKeyboardButton("No!", callback_data='no'))
|
||||
|
||||
keyboard_markup.add(types.InlineKeyboardButton("aiogram link",
|
||||
url='https://github.com/aiogram/aiogram'))
|
||||
# url buttons has no callback data
|
||||
|
||||
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
|
||||
|
||||
|
||||
@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no'
|
||||
# @dp.callback_query_handler(text='yes') # if cb.data == 'yes'
|
||||
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
|
||||
await query.answer() # send answer to close the rounding circle
|
||||
|
||||
answer_data = query.data
|
||||
logger.debug(f"answer_data={answer_data}")
|
||||
# here we can work with query.data
|
||||
if answer_data == 'yes':
|
||||
await bot.send_message(query.from_user.id, "That's great!")
|
||||
elif answer_data == 'no':
|
||||
await bot.send_message(query.from_user.id, "Oh no...Why so?")
|
||||
else:
|
||||
await bot.send_message(query.from_user.id, "Invalid callback data!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
@ -25,3 +25,7 @@ msgstr ""
|
|||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr ""
|
||||
|
||||
msgid "Aiogram has {number} like!"
|
||||
msgid_plural "Aiogram has {number} likes!"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
|
|
|||
|
|
@ -27,3 +27,8 @@ msgstr "Привет, <b>{user}</b>!"
|
|||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr "Твой язык: <i>{language}</i>"
|
||||
|
||||
msgid "Aiogram has {number} like!"
|
||||
msgid_plural "Aiogram has {number} likes!"
|
||||
msgstr[0] "Aiogram имеет {number} лайк!"
|
||||
msgstr[1] "Aiogram имеет {number} лайка!"
|
||||
msgstr[2] "Aiogram имеет {number} лайков!"
|
||||
|
|
@ -2,10 +2,9 @@ import asyncio
|
|||
|
||||
from aiogram import Bot
|
||||
from aiogram import types
|
||||
from aiogram.utils import executor
|
||||
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'
|
||||
|
|
|
|||
61
examples/regular_keyboard_example.py
Normal file
61
examples/regular_keyboard_example.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
This bot is created for the demonstration of a usage of regular keyboards.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
|
||||
API_TOKEN = 'BOT_TOKEN_HERE'
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 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.ReplyKeyboardMarkup(row_width=3)
|
||||
# default row_width is 3, so here we can omit it actually
|
||||
# kept for clearness
|
||||
|
||||
keyboard_markup.row(types.KeyboardButton("Yes!"),
|
||||
types.KeyboardButton("No!"))
|
||||
# adds buttons as a new row to the existing keyboard
|
||||
# the behaviour doesn't depend on row_width attribute
|
||||
|
||||
keyboard_markup.add(types.KeyboardButton("I don't know"),
|
||||
types.KeyboardButton("Who am i?"),
|
||||
types.KeyboardButton("Where am i?"),
|
||||
types.KeyboardButton("Who is there?"))
|
||||
# adds buttons. New rows is formed according to row_width parameter
|
||||
|
||||
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
|
||||
|
||||
|
||||
@dp.message_handler()
|
||||
async def all_msg_handler(message: types.Message):
|
||||
# pressing of a KeyboardButton is the same as sending the regular message with the same text
|
||||
# so, to handle the responses from the keyboard, we need to use a message_handler
|
||||
# in real bot, it's better to define message_handler(text="...") for each button
|
||||
# but here for the simplicity only one handler is defined
|
||||
|
||||
text_of_button = message.text
|
||||
logger.debug(text_of_button) # print the text we got
|
||||
|
||||
if text_of_button == 'Yes!':
|
||||
await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove())
|
||||
elif text_of_button == 'No!':
|
||||
await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove())
|
||||
else:
|
||||
await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove())
|
||||
# with message, we send types.ReplyKeyboardRemove() to hide the keyboard
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
@ -76,7 +76,8 @@ async def unknown(message: types.Message):
|
|||
"""
|
||||
Handler for unknown messages.
|
||||
"""
|
||||
return SendMessage(message.chat.id, f"I don\'t know what to do with content type `{message.content_type()}`. Sorry :c")
|
||||
return SendMessage(message.chat.id,
|
||||
f"I don\'t know what to do with content type `{message.content_type()}`. Sorry :c")
|
||||
|
||||
|
||||
async def cmd_id(message: types.Message):
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
conda:
|
||||
file: environment.yml
|
||||
python:
|
||||
version: 3
|
||||
pip_install: true
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
aiohttp>=3.4.4
|
||||
aiohttp>=3.5.4
|
||||
Babel>=2.6.0
|
||||
certifi>=2018.8.24
|
||||
certifi>=2019.3.9
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import aresponses
|
||||
|
||||
from aiogram import Bot
|
||||
|
||||
TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
|
||||
|
||||
|
||||
class FakeTelegram(aresponses.ResponsesMockServer):
|
||||
def __init__(self, message_dict, bot=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._body, self._headers = self.parse_data(message_dict)
|
||||
|
||||
if isinstance(bot, Bot):
|
||||
Bot.set_current(bot)
|
||||
|
||||
async def __aenter__(self):
|
||||
await super().__aenter__()
|
||||
_response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK')
|
||||
self.add(self.ANY, response=_response)
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if hasattr(self, 'monkeypatch'):
|
||||
self.monkeypatch.undo()
|
||||
await super().__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@staticmethod
|
||||
def parse_data(message_dict):
|
||||
import json
|
||||
|
||||
_body = '{"ok":true,"result":' + json.dumps(message_dict) + '}'
|
||||
_headers = {'Server': 'nginx/1.12.2',
|
||||
'Date': 'Tue, 03 Apr 2018 16:59:54 GMT',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': str(len(_body)),
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection',
|
||||
'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'}
|
||||
return _body, _headers
|
||||
|
|
@ -185,7 +185,7 @@ async def test_send_location(bot: Bot, event_loop):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_live_location(bot: Bot, event_loop):
|
||||
async def test_edit_message_live_location_by_bot(bot: Bot, event_loop):
|
||||
""" editMessageLiveLocation method test """
|
||||
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
|
||||
msg = types.Message(**MESSAGE_WITH_LOCATION)
|
||||
|
|
@ -197,6 +197,14 @@ async def test_edit_message_live_location(bot: Bot, event_loop):
|
|||
latitude=location.latitude, longitude=location.longitude)
|
||||
assert result == msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_live_location_by_user(bot: Bot, event_loop):
|
||||
""" editMessageLiveLocation method test """
|
||||
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
|
||||
msg = types.Message(**MESSAGE_WITH_LOCATION)
|
||||
location = types.Location(**LOCATION)
|
||||
|
||||
# editing user's message
|
||||
async with FakeTelegram(message_dict=True, loop=event_loop):
|
||||
result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id,
|
||||
|
|
@ -205,7 +213,7 @@ async def test_edit_message_live_location(bot: Bot, event_loop):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_message_live_location(bot: Bot, event_loop):
|
||||
async def test_stop_message_live_location_by_bot(bot: Bot, event_loop):
|
||||
""" stopMessageLiveLocation method test """
|
||||
from .types.dataset import MESSAGE_WITH_LOCATION
|
||||
msg = types.Message(**MESSAGE_WITH_LOCATION)
|
||||
|
|
@ -215,6 +223,13 @@ async def test_stop_message_live_location(bot: Bot, event_loop):
|
|||
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
|
||||
assert result == msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_message_live_location_by_user(bot: Bot, event_loop):
|
||||
""" stopMessageLiveLocation method test """
|
||||
from .types.dataset import MESSAGE_WITH_LOCATION
|
||||
msg = types.Message(**MESSAGE_WITH_LOCATION)
|
||||
|
||||
# stopping user's message
|
||||
async with FakeTelegram(message_dict=True, loop=event_loop):
|
||||
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
|
||||
|
|
@ -509,7 +524,7 @@ async def test_answer_callback_query(bot: Bot, event_loop):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_text(bot: Bot, event_loop):
|
||||
async def test_edit_message_text_by_bot(bot: Bot, event_loop):
|
||||
""" editMessageText method test """
|
||||
from .types.dataset import EDITED_MESSAGE
|
||||
msg = types.Message(**EDITED_MESSAGE)
|
||||
|
|
@ -519,6 +534,13 @@ async def test_edit_message_text(bot: Bot, event_loop):
|
|||
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
|
||||
assert result == msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_text_by_user(bot: Bot, event_loop):
|
||||
""" editMessageText method test """
|
||||
from .types.dataset import EDITED_MESSAGE
|
||||
msg = types.Message(**EDITED_MESSAGE)
|
||||
|
||||
# message by user
|
||||
async with FakeTelegram(message_dict=True, loop=event_loop):
|
||||
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
|
||||
|
|
|
|||
35
tests/test_dispatcher.py
Normal file
35
tests/test_dispatcher.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import pytest
|
||||
|
||||
from aiogram import Dispatcher, Bot
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.yield_fixture()
|
||||
async def bot(event_loop):
|
||||
""" Bot fixture """
|
||||
_bot = Bot(token='123456789:AABBCCDDEEFFaabbccddeeff-1234567890',
|
||||
loop=event_loop)
|
||||
yield _bot
|
||||
await _bot.close()
|
||||
|
||||
|
||||
class TestDispatcherInit:
|
||||
async def test_successful_init(self, bot):
|
||||
"""
|
||||
Success __init__ case
|
||||
|
||||
:param bot: bot instance
|
||||
:type bot: Bot
|
||||
"""
|
||||
dp = Dispatcher(bot=bot)
|
||||
assert isinstance(dp, Dispatcher)
|
||||
|
||||
@pytest.mark.parametrize("bot_instance", [None, Bot, 123, 'abc'])
|
||||
async def test_wrong_bot_instance(self, bot_instance):
|
||||
"""
|
||||
User provides wrong data to 'bot' argument.
|
||||
:return: TypeError with reason
|
||||
"""
|
||||
with pytest.raises(TypeError):
|
||||
_ = Dispatcher(bot=bot_instance)
|
||||
47
tests/test_message.py
Normal file
47
tests/test_message.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from asyncio import BaseEventLoop
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Bot, types
|
||||
from . import FakeTelegram, TOKEN
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.yield_fixture()
|
||||
async def bot(event_loop):
|
||||
""" Bot fixture """
|
||||
_bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.HTML)
|
||||
yield _bot
|
||||
await _bot.close()
|
||||
|
||||
|
||||
@pytest.yield_fixture()
|
||||
async def message(bot, event_loop):
|
||||
"""
|
||||
Message fixture
|
||||
:param bot: Telegram bot fixture
|
||||
:type bot: Bot
|
||||
:param event_loop: asyncio event loop
|
||||
:type event_loop: BaseEventLoop
|
||||
"""
|
||||
from .types.dataset import MESSAGE
|
||||
msg = types.Message(**MESSAGE)
|
||||
|
||||
async with FakeTelegram(message_dict=MESSAGE, loop=event_loop):
|
||||
_message = await bot.send_message(chat_id=msg.chat.id, text=msg.text)
|
||||
|
||||
yield _message
|
||||
|
||||
|
||||
class TestMiscCases:
|
||||
async def test_calling_bot_not_from_context(self, message):
|
||||
"""
|
||||
Calling any helper method without bot instance in context.
|
||||
|
||||
:param message: message fixture
|
||||
:type message: types.Message
|
||||
:return: RuntimeError with reason and help
|
||||
"""
|
||||
with pytest.raises(RuntimeError):
|
||||
await message.edit_text('test_calling_bot_not_from_context')
|
||||
Loading…
Add table
Add a link
Reference in a new issue