Merge branch 'dev-2.x' into add-new-ways-throttle

# Conflicts:
#	examples/throtling_example.py
This commit is contained in:
birdi 2019-08-15 17:06:10 +03:00
commit ed0d4796e7
63 changed files with 1567 additions and 939 deletions

View file

@ -6,7 +6,7 @@
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT)

View file

@ -21,7 +21,7 @@ AIOGramBot
:target: https://pypi.python.org/pypi/aiogram :target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions :alt: Supported python versions
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api :target: https://core.telegram.org/bots/api
:alt: Telegram Bot API :alt: Telegram Bot API

View file

@ -38,5 +38,5 @@ __all__ = [
'utils' 'utils'
] ]
__version__ = '2.2.1.dev1' __version__ = '2.3.dev1'
__api_version__ = '4.3' __api_version__ = '4.4'

View file

@ -147,7 +147,7 @@ class Methods(Helper):
""" """
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
List is updated to Bot API 4.3 List is updated to Bot API 4.4
""" """
mode = HelperMode.lowerCamelCase mode = HelperMode.lowerCamelCase
@ -182,6 +182,7 @@ class Methods(Helper):
UNBAN_CHAT_MEMBER = Item() # unbanChatMember UNBAN_CHAT_MEMBER = Item() # unbanChatMember
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
SET_CHAT_PHOTO = Item() # setChatPhoto SET_CHAT_PHOTO = Item() # setChatPhoto
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto DELETE_CHAT_PHOTO = Item() # deleteChatPhoto

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
import warnings
from .base import BaseBot, api from .base import BaseBot, api
from .. import types from .. import types
@ -345,7 +346,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
prepare_file(payload, files, 'audio', audio) prepare_file(payload, files, 'audio', audio)
prepare_attachment(payload, files, 'thumb', thumb) prepare_attachment(payload, files, 'thumb', thumb)
result = await self.request(api.Methods.SEND_AUDIO, payload, files) result = await self.request(api.Methods.SEND_AUDIO, payload, files)
return types.Message(**result) return types.Message(**result)
@ -1016,6 +1016,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String], async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
user_id: base.Integer, user_id: base.Integer,
permissions: typing.Optional[types.ChatPermissions] = None,
# permissions argument need to be required after removing other `can_*` arguments
until_date: typing.Union[base.Integer, None] = None, until_date: typing.Union[base.Integer, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None,
@ -1032,6 +1034,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type chat_id: :obj:`typing.Union[base.Integer, base.String]` :type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param user_id: Unique identifier of the target user :param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer` :type user_id: :obj:`base.Integer`
:param permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time :param until_date: Date when restrictions will be lifted for the user, unix time
:type until_date: :obj:`typing.Union[base.Integer, None]` :type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@ -1049,8 +1053,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean` :rtype: :obj:`base.Boolean`
""" """
until_date = prepare_arg(until_date) until_date = prepare_arg(until_date)
permissions = prepare_arg(permissions)
payload = generate_payload(**locals()) payload = generate_payload(**locals())
for permission in ['can_send_messages',
'can_send_media_messages',
'can_send_other_messages',
'can_add_web_page_previews']:
if permission in payload:
warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions "
f"in a single argument of the type ChatPermissions instead of "
f"passing regular argument {payload[permission]}",
DeprecationWarning, stacklevel=2)
result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload)
return result return result
@ -1101,6 +1116,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
return result return result
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
permissions: types.ChatPermissions) -> base.Boolean:
"""
Use this method to set default chat permissions for all members.
The bot must be an administrator in the group or a supergroup for this to work and must have the
can_restrict_members admin rights.
Returns True on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup
:param permissions: New default chat permissions
:return: True on success.
"""
permissions = prepare_arg(permissions)
payload = generate_payload(**locals())
result = await self.request(api.Methods.SET_CHAT_PERMISSIONS)
return result
async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String:
""" """
Use this method to generate a new invite link for a chat; any previously generated link is revoked. Use this method to generate a new invite link for a chat; any previously generated link is revoked.

View file

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

View file

@ -97,15 +97,13 @@ class I18nMiddleware(BaseMiddleware):
if locale not in self.locales: if locale not in self.locales:
if n is 1: if n is 1:
return singular return singular
else: return plural
return plural
translator = self.locales[locale] translator = self.locales[locale]
if plural is None: if plural is None:
return translator.gettext(singular) return translator.gettext(singular)
else: return translator.ngettext(singular, plural, n)
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy: def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy:
""" """

View file

@ -9,7 +9,7 @@ import aiohttp
from aiohttp.helpers import sentinel from aiohttp.helpers import sentinel
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
RegexpCommandsFilter, StateFilter, Text, IdFilter RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter
from .handler import Handler from .handler import Handler
from .middlewares import MiddlewareManager from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@ -85,39 +85,64 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(StateFilter, exclude_event_handlers=[ filters_factory.bind(StateFilter, exclude_event_handlers=[
self.errors_handlers, self.errors_handlers,
self.poll_handlers self.poll_handlers,
]) ])
filters_factory.bind(ContentTypeFilter, event_handlers=[ filters_factory.bind(ContentTypeFilter, event_handlers=[
self.message_handlers, self.edited_message_handlers, self.message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers, self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
]), ]),
filters_factory.bind(Command, event_handlers=[ filters_factory.bind(Command, event_handlers=[
self.message_handlers, self.edited_message_handlers self.message_handlers,
self.edited_message_handlers
]) ])
filters_factory.bind(Text, event_handlers=[ filters_factory.bind(Text, event_handlers=[
self.message_handlers, self.edited_message_handlers, self.message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers, self.edited_message_handlers,
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.poll_handlers,
self.inline_query_handlers,
]) ])
filters_factory.bind(HashTag, event_handlers=[ filters_factory.bind(HashTag, event_handlers=[
self.message_handlers, self.edited_message_handlers, self.message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
]) ])
filters_factory.bind(Regexp, event_handlers=[ filters_factory.bind(Regexp, event_handlers=[
self.message_handlers, self.edited_message_handlers, self.message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers, self.edited_message_handlers,
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.poll_handlers,
self.inline_query_handlers,
]) ])
filters_factory.bind(RegexpCommandsFilter, event_handlers=[ filters_factory.bind(RegexpCommandsFilter, event_handlers=[
self.message_handlers, self.edited_message_handlers self.message_handlers,
self.edited_message_handlers,
]) ])
filters_factory.bind(ExceptionsFilter, event_handlers=[ filters_factory.bind(ExceptionsFilter, event_handlers=[
self.errors_handlers self.errors_handlers,
]) ])
filters_factory.bind(IdFilter, event_handlers=[ filters_factory.bind(AdminFilter, event_handlers=[
self.message_handlers, self.edited_message_handlers, self.message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers, self.edited_message_handlers,
self.callback_query_handlers, self.inline_query_handlers self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
])
filters_factory.bind(IDFilter, event_handlers=[
self.message_handlers,
self.edited_message_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
]) ])
def __del__(self): def __del__(self):

View file

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

View file

@ -9,7 +9,7 @@ from babel.support import LazyProxy
from aiogram import types from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
class Command(Filter): class Command(Filter):
@ -84,9 +84,9 @@ class Command(Filter):
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower(): if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
return False return False
elif prefix not in prefixes: if prefix not in prefixes:
return False return False
elif (command.lower() if ignore_case else command) not in commands: if (command.lower() if ignore_case else command) not in commands:
return False return False
return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)} return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)}
@ -149,7 +149,7 @@ class CommandStart(Command):
:param deep_link: string or compiled regular expression (by ``re.compile(...)``). :param deep_link: string or compiled regular expression (by ``re.compile(...)``).
""" """
super(CommandStart, self).__init__(['start']) super().__init__(['start'])
self.deep_link = deep_link self.deep_link = deep_link
async def check(self, message: types.Message): async def check(self, message: types.Message):
@ -159,7 +159,7 @@ class CommandStart(Command):
:param message: :param message:
:return: :return:
""" """
check = await super(CommandStart, self).check(message) check = await super().check(message)
if check and self.deep_link is not None: if check and self.deep_link is not None:
if not isinstance(self.deep_link, re.Pattern): if not isinstance(self.deep_link, re.Pattern):
@ -179,7 +179,7 @@ class CommandHelp(Command):
""" """
def __init__(self): def __init__(self):
super(CommandHelp, self).__init__(['help']) super().__init__(['help'])
class CommandSettings(Command): class CommandSettings(Command):
@ -188,7 +188,7 @@ class CommandSettings(Command):
""" """
def __init__(self): def __init__(self):
super(CommandSettings, self).__init__(['settings']) super().__init__(['settings'])
class CommandPrivacy(Command): class CommandPrivacy(Command):
@ -197,7 +197,7 @@ class CommandPrivacy(Command):
""" """
def __init__(self): def __init__(self):
super(CommandPrivacy, self).__init__(['privacy']) super().__init__(['privacy'])
class Text(Filter): class Text(Filter):
@ -205,19 +205,27 @@ class Text(Filter):
Simple text filter Simple text filter
""" """
_default_params = (
('text', 'equals'),
('text_contains', 'contains'),
('text_startswith', 'startswith'),
('text_endswith', 'endswith'),
)
def __init__(self, def __init__(self,
equals: Optional[Union[str, LazyProxy]] = None, equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
contains: Optional[Union[str, LazyProxy]] = None, contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
startswith: Optional[Union[str, LazyProxy]] = None, startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
endswith: Optional[Union[str, LazyProxy]] = None, endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
ignore_case=False): ignore_case=False):
""" """
Check text for one of pattern. Only one mode can be used in one filter. Check text for one of pattern. Only one mode can be used in one filter.
In every pattern, a single string is treated as a list with 1 element.
:param equals: :param equals: True if object's text in the list
:param contains: :param contains: True if object's text contains all strings from the list
:param startswith: :param startswith: True if object's text starts with any of strings from the list
:param endswith: :param endswith: True if object's text ends with any of strings from the list
:param ignore_case: case insensitive :param ignore_case: case insensitive
""" """
# Only one mode can be used. check it. # Only one mode can be used. check it.
@ -232,6 +240,9 @@ class Text(Filter):
elif check == 0: elif check == 0:
raise ValueError(f"No one mode is specified!") raise ValueError(f"No one mode is specified!")
equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
else e,
(equals, contains, endswith, startswith))
self.equals = equals self.equals = equals
self.contains = contains self.contains = contains
self.endswith = endswith self.endswith = endswith
@ -240,14 +251,9 @@ class Text(Filter):
@classmethod @classmethod
def validate(cls, full_config: Dict[str, Any]): def validate(cls, full_config: Dict[str, Any]):
if 'text' in full_config: for param, key in cls._default_params:
return {'equals': full_config.pop('text')} if param in full_config:
elif 'text_contains' in full_config: return {key: full_config.pop(param)}
return {'contains': full_config.pop('text_contains')}
elif 'text_startswith' in full_config:
return {'startswith': full_config.pop('text_startswith')}
elif 'text_endswith' in full_config:
return {'endswith': full_config.pop('text_endswith')}
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]): async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message): if isinstance(obj, Message):
@ -265,27 +271,26 @@ class Text(Filter):
if self.ignore_case: if self.ignore_case:
text = text.lower() text = text.lower()
_pre_process_func = lambda s: str(s).lower()
else:
_pre_process_func = str
# now check
if self.equals is not None: if self.equals is not None:
self.equals = str(self.equals) equals = list(map(_pre_process_func, self.equals))
if self.ignore_case: return text in equals
self.equals = self.equals.lower()
return text == self.equals if self.contains is not None:
elif self.contains is not None: contains = list(map(_pre_process_func, self.contains))
self.contains = str(self.contains) return all(map(text.__contains__, contains))
if self.ignore_case:
self.contains = self.contains.lower() if self.startswith is not None:
return self.contains in text startswith = list(map(_pre_process_func, self.startswith))
elif self.startswith is not None: return any(map(text.startswith, startswith))
self.startswith = str(self.startswith)
if self.ignore_case: if self.endswith is not None:
self.startswith = self.startswith.lower() endswith = list(map(_pre_process_func, self.endswith))
return text.startswith(self.startswith) return any(map(text.endswith, endswith))
elif self.endswith is not None:
self.endswith = str(self.endswith)
if self.ignore_case:
self.endswith = self.endswith.lower()
return text.endswith(self.endswith)
return False return False
@ -505,7 +510,7 @@ class ExceptionsFilter(BoundFilter):
return False return False
class IdFilter(Filter): class IDFilter(Filter):
def __init__(self, def __init__(self,
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
@ -560,9 +565,63 @@ class IdFilter(Filter):
if self.user_id and self.chat_id: if self.user_id and self.chat_id:
return user_id in self.user_id and chat_id in self.chat_id return user_id in self.user_id and chat_id in self.chat_id
elif self.user_id: if self.user_id:
return user_id in self.user_id return user_id in self.user_id
elif self.chat_id: if self.chat_id:
return chat_id in self.chat_id return chat_id in self.chat_id
return False return False
class AdminFilter(Filter):
"""
Checks if user is admin in a chat.
If is_chat_admin is not set, the filter will check in the current chat (correct only for messages).
is_chat_admin is required for InlineQuery.
"""
def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
if is_chat_admin:
if isinstance(is_chat_admin, bool):
self._check_current = is_chat_admin
if isinstance(is_chat_admin, Iterable):
self._chat_ids = list(is_chat_admin)
else:
self._chat_ids = [is_chat_admin]
else:
self._check_current = True
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if "is_chat_admin" in full_config:
result["is_chat_admin"] = full_config.pop("is_chat_admin")
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool:
user_id = obj.from_user.id
if self._check_current:
if isinstance(obj, Message):
message = obj
elif isinstance(obj, CallbackQuery) and obj.message:
message = obj.message
else:
return False
if ChatType.is_private(message): # there is no admin in private chats
return False
chat_ids = [message.chat.id]
else:
chat_ids = self._chat_ids
admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)]
return user_id in admins

View file

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

View file

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

View file

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

View file

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

View file

@ -523,7 +523,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio
'disable_web_page_preview': self.disable_web_page_preview, 'disable_web_page_preview': self.disable_web_page_preview,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
def write(self, *text, sep=' '): def write(self, *text, sep=' '):
@ -642,7 +642,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption, 'caption': self.caption,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -704,7 +704,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'title': self.title, 'title': self.title,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -817,7 +817,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption, 'caption': self.caption,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -871,7 +871,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'duration': self.duration, 'duration': self.duration,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -924,7 +924,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'length': self.length, 'length': self.length,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1050,7 +1050,7 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'longitude': self.longitude, 'longitude': self.longitude,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1109,7 +1109,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'foursquare_id': self.foursquare_id, 'foursquare_id': self.foursquare_id,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1160,7 +1160,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'last_name': self.last_name, 'last_name': self.last_name,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1220,7 +1220,7 @@ class KickChatMember(BaseResponse):
return { return {
'chat_id': self.chat_id, 'chat_id': self.chat_id,
'user_id': self.user_id, 'user_id': self.user_id,
'until_date': prepare_arg(self.until_date) 'until_date': prepare_arg(self.until_date),
} }
@ -1608,7 +1608,7 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin):
'text': self.text, 'text': self.text,
'parse_mode': self.parse_mode, 'parse_mode': self.parse_mode,
'disable_web_page_preview': self.disable_web_page_preview, 'disable_web_page_preview': self.disable_web_page_preview,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1649,7 +1649,7 @@ class EditMessageCaption(BaseResponse):
'message_id': self.message_id, 'message_id': self.message_id,
'inline_message_id': self.inline_message_id, 'inline_message_id': self.inline_message_id,
'caption': self.caption, 'caption': self.caption,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1685,7 +1685,7 @@ class EditMessageReplyMarkup(BaseResponse):
'chat_id': self.chat_id, 'chat_id': self.chat_id,
'message_id': self.message_id, 'message_id': self.message_id,
'inline_message_id': self.inline_message_id, 'inline_message_id': self.inline_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1756,7 +1756,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'sticker': self.sticker, 'sticker': self.sticker,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }
@ -1848,7 +1848,7 @@ class AddStickerToSet(BaseResponse):
'name': self.name, 'name': self.name,
'png_sticker': self.png_sticker, 'png_sticker': self.png_sticker,
'emojis': self.emojis, 'emojis': self.emojis,
'mask_position': prepare_arg(self.mask_position) 'mask_position': prepare_arg(self.mask_position),
} }
@ -2177,5 +2177,5 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'game_short_name': self.game_short_name, 'game_short_name': self.game_short_name,
'disable_notification': self.disable_notification, 'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id, 'reply_to_message_id': self.reply_to_message_id,
'reply_markup': prepare_arg(self.reply_markup) 'reply_markup': prepare_arg(self.reply_markup),
} }

View file

@ -7,6 +7,7 @@ from .callback_game import CallbackGame
from .callback_query import CallbackQuery from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType from .chat import Chat, ChatActions, ChatType
from .chat_member import ChatMember, ChatMemberStatus from .chat_member import ChatMember, ChatMemberStatus
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult from .chosen_inline_result import ChosenInlineResult
from .contact import Contact from .contact import Contact

View file

@ -5,6 +5,7 @@ import typing
from . import base from . import base
from . import fields from . import fields
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto from .chat_photo import ChatPhoto
from ..utils import helper from ..utils import helper
from ..utils import markdown from ..utils import markdown
@ -27,6 +28,7 @@ class Chat(base.TelegramObject):
description: base.String = fields.Field() description: base.String = fields.Field()
invite_link: base.String = fields.Field() invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message') pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
sticker_set_name: base.String = fields.Field() sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field() can_set_sticker_set: base.Boolean = fields.Field()
@ -202,6 +204,7 @@ class Chat(base.TelegramObject):
return await self.bot.unban_chat_member(self.id, user_id=user_id) return await self.bot.unban_chat_member(self.id, user_id=user_id)
async def restrict(self, user_id: base.Integer, async def restrict(self, user_id: base.Integer,
permissions: typing.Optional[ChatPermissions] = None,
until_date: typing.Union[base.Integer, None] = None, until_date: typing.Union[base.Integer, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None,
@ -216,6 +219,8 @@ class Chat(base.TelegramObject):
:param user_id: Unique identifier of the target user :param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer` :type user_id: :obj:`base.Integer`
:param permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time. :param until_date: Date when restrictions will be lifted for the user, unix time.
:type until_date: :obj:`typing.Union[base.Integer, None]` :type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@ -232,7 +237,9 @@ class Chat(base.TelegramObject):
:return: Returns True on success. :return: Returns True on success.
:rtype: :obj:`base.Boolean` :rtype: :obj:`base.Boolean`
""" """
return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date, return await self.bot.restrict_chat_member(self.id, user_id=user_id,
permissions=permissions,
until_date=until_date,
can_send_messages=can_send_messages, can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages, can_send_media_messages=can_send_media_messages,
can_send_other_messages=can_send_other_messages, can_send_other_messages=can_send_other_messages,

View file

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

View file

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

View file

@ -94,60 +94,60 @@ class Message(base.TelegramObject):
def content_type(self): def content_type(self):
if self.text: if self.text:
return ContentType.TEXT return ContentType.TEXT
elif self.audio: if self.audio:
return ContentType.AUDIO return ContentType.AUDIO
elif self.animation: if self.animation:
return ContentType.ANIMATION return ContentType.ANIMATION
elif self.document: if self.document:
return ContentType.DOCUMENT return ContentType.DOCUMENT
elif self.game: if self.game:
return ContentType.GAME return ContentType.GAME
elif self.photo: if self.photo:
return ContentType.PHOTO return ContentType.PHOTO
elif self.sticker: if self.sticker:
return ContentType.STICKER return ContentType.STICKER
elif self.video: if self.video:
return ContentType.VIDEO return ContentType.VIDEO
elif self.video_note: if self.video_note:
return ContentType.VIDEO_NOTE return ContentType.VIDEO_NOTE
elif self.voice: if self.voice:
return ContentType.VOICE return ContentType.VOICE
elif self.contact: if self.contact:
return ContentType.CONTACT return ContentType.CONTACT
elif self.venue: if self.venue:
return ContentType.VENUE return ContentType.VENUE
elif self.location: if self.location:
return ContentType.LOCATION return ContentType.LOCATION
elif self.new_chat_members: if self.new_chat_members:
return ContentType.NEW_CHAT_MEMBERS return ContentType.NEW_CHAT_MEMBERS
elif self.left_chat_member: if self.left_chat_member:
return ContentType.LEFT_CHAT_MEMBER return ContentType.LEFT_CHAT_MEMBER
elif self.invoice: if self.invoice:
return ContentType.INVOICE return ContentType.INVOICE
elif self.successful_payment: if self.successful_payment:
return ContentType.SUCCESSFUL_PAYMENT return ContentType.SUCCESSFUL_PAYMENT
elif self.connected_website: if self.connected_website:
return ContentType.CONNECTED_WEBSITE return ContentType.CONNECTED_WEBSITE
elif self.migrate_from_chat_id: if self.migrate_from_chat_id:
return ContentType.MIGRATE_FROM_CHAT_ID return ContentType.MIGRATE_FROM_CHAT_ID
elif self.migrate_to_chat_id: if self.migrate_to_chat_id:
return ContentType.MIGRATE_TO_CHAT_ID return ContentType.MIGRATE_TO_CHAT_ID
elif self.pinned_message: if self.pinned_message:
return ContentType.PINNED_MESSAGE return ContentType.PINNED_MESSAGE
elif self.new_chat_title: if self.new_chat_title:
return ContentType.NEW_CHAT_TITLE return ContentType.NEW_CHAT_TITLE
elif self.new_chat_photo: if self.new_chat_photo:
return ContentType.NEW_CHAT_PHOTO return ContentType.NEW_CHAT_PHOTO
elif self.delete_chat_photo: if self.delete_chat_photo:
return ContentType.DELETE_CHAT_PHOTO return ContentType.DELETE_CHAT_PHOTO
elif self.group_chat_created: if self.group_chat_created:
return ContentType.GROUP_CHAT_CREATED return ContentType.GROUP_CHAT_CREATED
elif self.passport_data: if self.passport_data:
return ContentType.PASSPORT_DATA return ContentType.PASSPORT_DATA
elif self.poll: if self.poll:
return ContentType.POLL return ContentType.POLL
else:
return ContentType.UNKNOWN return ContentType.UNKNOWN
def is_command(self): def is_command(self):
""" """
@ -959,70 +959,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None, reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup) reply_markup=reply_markup)
async def send_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
height: typing.Union[base.Integer, None] = None,
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
caption: typing.Union[base.String, None] = None,
parse_mode: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
On success, the sent Message is returned.
Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
Source https://core.telegram.org/bots/api#sendanimation
:param animation: Animation to send. Pass a file_id as String to send an animation that exists
on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation
from the Internet, or upload a new animation using multipart/form-data
:type animation: :obj:`typing.Union[base.InputFile, base.String]`
:param duration: Duration of sent animation in seconds
:type duration: :obj:`typing.Union[base.Integer, None]`
:param width: Animation width
:type width: :obj:`typing.Union[base.Integer, None]`
:param height: Animation height
:type height: :obj:`typing.Union[base.Integer, None]`
:param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnails width and height should not exceed 90.
:type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
:param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters
:type caption: :obj:`typing.Union[base.String, None]`
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
fixed-width text or inline URLs in the media caption
:type parse_mode: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove, types.ForceReply], None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n'
'Use "Message.reply_animation" instead.',
stacklevel=8)
return await self.bot.send_animation(self.chat.id,
animation=animation,
duration=duration,
width=width,
height=height,
thumb=thumb,
caption=caption,
parse_mode=parse_mode,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String], async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None, duration: typing.Union[base.Integer, None] = None,
@ -1323,55 +1259,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None, reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup) reply_markup=reply_markup)
async def send_venue(self,
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
foursquare_id: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send information about a venue.
Source: https://core.telegram.org/bots/api#sendvenue
:param latitude: Latitude of the venue
:type latitude: :obj:`base.Float`
:param longitude: Longitude of the venue
:type longitude: :obj:`base.Float`
:param title: Name of the venue
:type title: :obj:`base.String`
:param address: Address of the venue
:type address: :obj:`base.String`
:param foursquare_id: Foursquare identifier of the venue
:type foursquare_id: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n'
'Use "Message.reply_venue" instead.',
stacklevel=8)
return await self.bot.send_venue(chat_id=self.chat.id,
latitude=latitude,
longitude=longitude,
title=title,
address=address,
foursquare_id=foursquare_id,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_venue(self, async def reply_venue(self,
latitude: base.Float, longitude: base.Float, latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String, title: base.String, address: base.String,
@ -1417,46 +1304,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None, reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup) reply_markup=reply_markup)
async def send_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
reply: base.Boolean = True) -> Message:
"""
Use this method to send phone contacts.
Source: https://core.telegram.org/bots/api#sendcontact
:param phone_number: Contact's phone number
:type phone_number: :obj:`base.String`
:param first_name: Contact's first name
:type first_name: :obj:`base.String`
:param last_name: Contact's last name
:type last_name: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param reply: fill 'reply_to_message_id'
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n'
'Use "Message.reply_contact" instead.',
stacklevel=8)
return await self.bot.send_contact(chat_id=self.chat.id,
phone_number=phone_number,
first_name=first_name, last_name=last_name,
disable_notification=disable_notification,
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
async def reply_contact(self, phone_number: base.String, async def reply_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None, first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None, disable_notification: typing.Union[base.Boolean, None] = None,

View file

@ -49,30 +49,24 @@ class MessageEntity(base.TelegramObject):
entity_text = self.get_text(text) entity_text = self.get_text(text)
if self.type == MessageEntityType.BOLD: if self.type == MessageEntityType.BOLD:
if as_html: method = markdown.hbold if as_html else markdown.bold
return markdown.hbold(entity_text) return method(entity_text)
return markdown.bold(entity_text) if self.type == MessageEntityType.ITALIC:
elif self.type == MessageEntityType.ITALIC: method = markdown.hitalic if as_html else markdown.italic
if as_html: return method(entity_text)
return markdown.hitalic(entity_text) if self.type == MessageEntityType.PRE:
return markdown.italic(entity_text) method = markdown.hpre if as_html else markdown.pre
elif self.type == MessageEntityType.PRE: return method(entity_text)
if as_html: if self.type == MessageEntityType.CODE:
return markdown.hpre(entity_text) method = markdown.hcode if as_html else markdown.code
return markdown.pre(entity_text) return method(entity_text)
elif self.type == MessageEntityType.CODE: if self.type == MessageEntityType.URL:
if as_html: method = markdown.hlink if as_html else markdown.link
return markdown.hcode(entity_text) return method(entity_text, entity_text)
return markdown.code(entity_text) if self.type == MessageEntityType.TEXT_LINK:
elif self.type == MessageEntityType.URL: method = markdown.hlink if as_html else markdown.link
if as_html: return method(entity_text, self.url)
return markdown.hlink(entity_text, entity_text) if self.type == MessageEntityType.TEXT_MENTION and self.user:
return markdown.link(entity_text, entity_text)
elif self.type == MessageEntityType.TEXT_LINK:
if as_html:
return markdown.hlink(entity_text, self.url)
return markdown.link(entity_text, self.url)
elif self.type == MessageEntityType.TEXT_MENTION and self.user:
return self.user.get_mention(entity_text) return self.user.get_mention(entity_text)
return entity_text return entity_text

View file

@ -14,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
file_id: base.String = fields.Field() file_id: base.String = fields.Field()
width: base.Integer = fields.Field() width: base.Integer = fields.Field()
height: base.Integer = fields.Field() height: base.Integer = fields.Field()
is_animated: base.Boolean = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize) thumb: PhotoSize = fields.Field(base=PhotoSize)
emoji: base.String = fields.Field() emoji: base.String = fields.Field()
set_name: base.String = fields.Field() set_name: base.String = fields.Field()

View file

@ -13,5 +13,6 @@ class StickerSet(base.TelegramObject):
""" """
name: base.String = fields.Field() name: base.String = fields.Field()
title: base.String = fields.Field() title: base.String = fields.Field()
is_animated: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field() contains_masks: base.Boolean = fields.Field()
stickers: typing.List[Sticker] = fields.ListField(base=Sticker) stickers: typing.List[Sticker] = fields.ListField(base=Sticker)

View file

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
import babel import babel
from . import base from . import base
@ -45,7 +47,7 @@ class User(base.TelegramObject):
return self.full_name return self.full_name
@property @property
def locale(self) -> babel.core.Locale or None: def locale(self) -> Optional[babel.core.Locale]:
""" """
Get user's locale Get user's locale

View file

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

View file

@ -1,10 +1,7 @@
"""
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
"""
import functools import functools
import inspect import inspect
import warnings import warnings
import asyncio
def deprecated(reason): def deprecated(reason):
@ -12,6 +9,8 @@ def deprecated(reason):
This is a decorator which can be used to mark functions This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted as deprecated. It will result in a warning being emitted
when the function is used. when the function is used.
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
""" """
if isinstance(reason, str): if isinstance(reason, str):
@ -41,7 +40,7 @@ def deprecated(reason):
return decorator return decorator
elif inspect.isclass(reason) or inspect.isfunction(reason): if inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'. # The @deprecated is used without any 'reason'.
# #
@ -65,11 +64,71 @@ def deprecated(reason):
return wrapper1 return wrapper1
else: raise TypeError(repr(type(reason)))
raise TypeError(repr(type(reason)))
def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2):
warnings.simplefilter('always', warning) warnings.simplefilter('always', warning)
warnings.warn(message, category=warning, stacklevel=stacklevel) warnings.warn(message, category=warning, stacklevel=stacklevel)
warnings.simplefilter('default', warning) warnings.simplefilter('default', warning)
def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3):
"""
A meta-decorator to mark an argument as deprecated.
.. code-block:: python3
@renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default
@renamed_argument("user", "user_id", "3.0", stacklevel=4)
def some_function(user_id, chat_id=None):
print(f"user_id={user_id}, chat_id={chat_id}")
some_function(user=123) # prints 'user_id=123, chat_id=None' with warning
some_function(123) # prints 'user_id=123, chat_id=None' without warning
some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning
:param old_name:
:param new_name:
:param until_version: the version in which the argument is scheduled to be removed
:param stacklevel: leave it to default if it's the first decorator used.
Increment with any new decorator used.
:return: decorator
"""
def decorator(func):
if asyncio.iscoroutinefunction(func):
@functools.wraps(func)
async def wrapped(*args, **kwargs):
if old_name in kwargs:
warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' "
f"is renamed to '{new_name}' "
f"and will be removed in aiogram {until_version}",
stacklevel=stacklevel)
kwargs.update(
{
new_name: kwargs[old_name],
}
)
kwargs.pop(old_name)
await func(*args, **kwargs)
else:
@functools.wraps(func)
def wrapped(*args, **kwargs):
if old_name in kwargs:
warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` "
f"is renamed to `{new_name}` "
f"and will be removed in aiogram {until_version}",
stacklevel=stacklevel)
kwargs.update(
{
new_name: kwargs[old_name],
}
)
kwargs.pop(old_name)
func(*args, **kwargs)
return wrapped
return decorator

View file

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

View file

@ -120,15 +120,15 @@ class HelperMode(Helper):
""" """
if mode == cls.SCREAMING_SNAKE_CASE: if mode == cls.SCREAMING_SNAKE_CASE:
return cls._screaming_snake_case(text) return cls._screaming_snake_case(text)
elif mode == cls.snake_case: if mode == cls.snake_case:
return cls._snake_case(text) return cls._snake_case(text)
elif mode == cls.lowercase: if mode == cls.lowercase:
return cls._snake_case(text).replace('_', '') return cls._snake_case(text).replace('_', '')
elif mode == cls.lowerCamelCase: if mode == cls.lowerCamelCase:
return cls._camel_case(text) return cls._camel_case(text)
elif mode == cls.CamelCase: if mode == cls.CamelCase:
return cls._camel_case(text, True) return cls._camel_case(text, True)
elif callable(mode): if callable(mode):
return mode(text) return mode(text)
return text return text

View file

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

View file

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

View file

@ -15,3 +15,4 @@ sphinx-rtd-theme>=0.4.3
sphinxcontrib-programoutput>=0.14 sphinxcontrib-programoutput>=0.14
aiohttp-socks>=0.2.2 aiohttp-socks>=0.2.2
rethinkdb>=2.4.1 rethinkdb>=2.4.1
coverage==4.5.3

View file

@ -111,10 +111,18 @@ ExceptionsFilter
:show-inheritance: :show-inheritance:
IdFilter IDFilter
---------------- ----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter .. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter
:members:
:show-inheritance:
AdminFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter
:members: :members:
:show-inheritance: :show-inheritance:

View file

@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
:target: https://pypi.python.org/pypi/aiogram :target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions :alt: Supported python versions
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api :target: https://core.telegram.org/bots/api
:alt: Telegram Bot API :alt: Telegram Bot API

View file

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

View file

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

View file

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

View file

@ -27,42 +27,40 @@ likes = {} # user_id: amount_of_likes
def get_keyboard(): def get_keyboard():
return types.InlineKeyboardMarkup().row( return types.InlineKeyboardMarkup().row(
types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')), types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')),
types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down'))) types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')),
)
@dp.message_handler(commands=['start']) @dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message): async def cmd_start(message: types.Message):
amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0 amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0
await message.reply(f'Vote! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard()) await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard())
@dp.callback_query_handler(vote_cb.filter(action='up')) @dp.callback_query_handler(vote_cb.filter(action=['up', 'down']))
async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict): async def callback_vote_action(query: types.CallbackQuery, callback_data: dict):
logging.info(callback_data) # callback_data contains all info from callback data logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data
likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage await query.answer() # don't forget to answer callback query as soon as possible
amount_of_likes = likes[query.from_user.id] callback_data_action = callback_data['action']
likes_count = likes.get(query.from_user.id, 0)
await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.', if callback_data_action == 'up':
query.from_user.id, likes_count += 1
query.message.message_id, else:
reply_markup=get_keyboard()) likes_count -= 1
likes[query.from_user.id] = likes_count # update amount of likes in storage
@dp.callback_query_handler(vote_cb.filter(action='down')) await bot.edit_message_text(
async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict): f'You voted {callback_data_action}! Now you have {likes_count} vote[s].',
logging.info(callback_data) # callback_data contains all info from callback data query.from_user.id,
likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage query.message.message_id,
amount_of_likes = likes[query.from_user.id] reply_markup=get_keyboard(),
)
await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.',
query.from_user.id,
query.message.message_id,
reply_markup=get_keyboard())
@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
async def message_not_modified_handler(update, error): async def message_not_modified_handler(update, error):
# pass
return True return True

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ from pathlib import Path
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.i18n import I18nMiddleware from aiogram.contrib.middlewares.i18n import I18nMiddleware
TOKEN = 'BOT TOKEN HERE' TOKEN = 'BOT_TOKEN_HERE'
I18N_DOMAIN = 'mybot' I18N_DOMAIN = 'mybot'
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
@ -54,14 +54,16 @@ dp.middleware.setup(i18n)
_ = i18n.gettext _ = i18n.gettext
@dp.message_handler(commands=['start']) @dp.message_handler(commands='start')
async def cmd_start(message: types.Message): async def cmd_start(message: types.Message):
# Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts. # Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts.
await message.reply(_('Hello, <b>{user}</b>!').format(user=message.from_user.full_name)) await message.reply(_('Hello, <b>{user}</b>!').format(user=message.from_user.full_name))
@dp.message_handler(commands=['lang']) @dp.message_handler(commands='lang')
async def cmd_lang(message: types.Message, locale): async def cmd_lang(message: types.Message, locale):
# For setting custom lang you have to modify i18n middleware, like this:
# https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py
await message.reply(_('Your current language: <i>{language}</i>').format(language=locale)) await message.reply(_('Your current language: <i>{language}</i>').format(language=locale))
# If you care about pluralization, here's small handler # If you care about pluralization, here's small handler
@ -70,15 +72,27 @@ async def cmd_lang(message: types.Message, locale):
# Alias for gettext method, parser will understand double underscore as plural (aka ngettext) # Alias for gettext method, parser will understand double underscore as plural (aka ngettext)
__ = i18n.gettext __ = i18n.gettext
# Some pseudo numeric value
TOTAL_LIKES = 0
@dp.message_handler(commands=['like']) # some likes manager
LIKES_STORAGE = {'count': 0}
def get_likes() -> int:
return LIKES_STORAGE['count']
def increase_likes() -> int:
LIKES_STORAGE['count'] += 1
return get_likes()
#
@dp.message_handler(commands='like')
async def cmd_like(message: types.Message, locale): async def cmd_like(message: types.Message, locale):
TOTAL_LIKES += 1 likes = increase_likes()
# NOTE: This is comment for a translator # NOTE: This is comment for a translator
await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES)) await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes))
if __name__ == '__main__': if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True) executor.start_polling(dp, skip_updates=True)

View file

@ -1,37 +1,35 @@
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
from aiogram.dispatcher.handler import SkipHandler from aiogram.dispatcher.handler import SkipHandler
API_TOKEN = 'API_TOKE_HERE'
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN) bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot) dp = Dispatcher(bot)
user_id_to_test = None # todo: Set id here user_id_required = None # TODO: Set id here
chat_id_to_test = user_id_to_test chat_id_required = user_id_required # Change for use in groups (user_id == chat_id in pm)
@dp.message_handler(user_id=user_id_to_test) @dp.message_handler(user_id=user_id_required)
async def handler1(msg: types.Message): async def handler1(msg: types.Message):
await bot.send_message(msg.chat.id, await bot.send_message(msg.chat.id, "Hello, checking with user_id=")
"Hello, checking with user_id=") raise SkipHandler # just for demo
raise SkipHandler
@dp.message_handler(chat_id=chat_id_to_test) @dp.message_handler(chat_id=chat_id_required)
async def handler2(msg: types.Message): async def handler2(msg: types.Message):
await bot.send_message(msg.chat.id, await bot.send_message(msg.chat.id, "Hello, checking with chat_id=")
"Hello, checking with chat_id=") raise SkipHandler # just for demo
raise SkipHandler
@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test) @dp.message_handler(user_id=user_id_required, chat_id=chat_id_required)
async def handler3(msg: types.Message): async def handler3(msg: types.Message):
await bot.send_message(msg.chat.id, await msg.reply("Hello from user= & chat_id=", reply=False)
"Hello from user= & chat_id=")
@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here @dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here
async def handler4(msg: types.Message): async def handler4(msg: types.Message):
print("Checked user_id with list!") await msg.reply("Checked user_id with list!", reply=False)
if __name__ == '__main__': if __name__ == '__main__':

View file

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

View file

@ -6,50 +6,56 @@ import logging
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE' API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Initialize bot and dispatcher # Initialize bot and dispatcher
bot = Bot(token=API_TOKEN) bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot) dp = Dispatcher(bot)
@dp.message_handler(commands=['start']) @dp.message_handler(commands='start')
async def start_cmd_handler(message: types.Message): async def start_cmd_handler(message: types.Message):
keyboard_markup = types.InlineKeyboardMarkup(row_width=3) keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually # default row_width is 3, so here we can omit it actually
# kept for clearness # kept for clearness
keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'), text_and_data = (
# in real life for the callback_data the callback data factory should be used ('Yes!', 'yes'),
# here the raw string is used for the simplicity ('No!', 'no'),
types.InlineKeyboardButton("No!", callback_data='no')) )
# in real life for the callback_data the callback data factory should be used
# here the raw string is used for the simplicity
row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data)
keyboard_markup.add(types.InlineKeyboardButton("aiogram link", keyboard_markup.row(*row_btns)
url='https://github.com/aiogram/aiogram')) keyboard_markup.add(
# url buttons has no callback data # url buttons have no callback data
types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'),
)
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) 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' # Use multiple registrators. Handler will execute when one of the filters is OK
# @dp.callback_query_handler(text='yes') # if cb.data == 'yes' @dp.callback_query_handler(text='no') # if cb.data == 'no'
@dp.callback_query_handler(text='yes') # if cb.data == 'yes'
async def inline_kb_answer_callback_handler(query: types.CallbackQuery): async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
await query.answer() # send answer to close the rounding circle
answer_data = query.data answer_data = query.data
logger.debug(f"answer_data={answer_data}") # always answer callback queries, even if you have nothing to say
# here we can work with query.data await query.answer(f'You answered with {answer_data!r}')
if answer_data == 'yes': if answer_data == 'yes':
await bot.send_message(query.from_user.id, "That's great!") text = 'Great, me too!'
elif answer_data == 'no': elif answer_data == 'no':
await bot.send_message(query.from_user.id, "Oh no...Why so?") text = 'Oh no...Why so?'
else: else:
await bot.send_message(query.from_user.id, "Invalid callback data!") text = f'Unexpected callback data {answer_data!r}!'
await bot.send_message(query.from_user.id, text)
if __name__ == '__main__': if __name__ == '__main__':

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled from aiogram.utils.exceptions import Throttled
TOKEN = 'BOT TOKEN HERE' TOKEN = 'BOT_TOKEN_HERE'
# In this example Redis storage is used # In this example Redis storage is used
storage = RedisStorage2(db=5) storage = RedisStorage2(db=5)

View file

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

View file

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

View file

@ -2,14 +2,28 @@ from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher, filters from aiogram.dispatcher import Dispatcher, filters
from aiogram.utils import executor from aiogram.utils import executor
bot = Bot(token='TOKEN')
bot = Bot(token='BOT_TOKEN_HERE', parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot) dp = Dispatcher(bot)
@dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)'])) @dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)']))
async def send_welcome(message: types.Message, regexp_command): async def send_welcome(message: types.Message, regexp_command):
await message.reply("You have requested an item with number: {}".format(regexp_command.group(1))) await message.reply(f"You have requested an item with id <code>{regexp_command.group(1)}</code>")
@dp.message_handler(commands='start')
async def create_deeplink(message: types.Message):
bot_user = await bot.me
bot_username = bot_user.username
deeplink = f'https://t.me/{bot_username}?start=item_12345'
text = (
f'Either send a command /item_1234 or follow this link {deeplink} and then click start\n'
'It also can be hidden in a inline button\n\n'
'Or just send <code>/start item_123</code>'
)
await message.reply(text, disable_web_page_preview=True)
if __name__ == '__main__': if __name__ == '__main__':
executor.start_polling(dp) executor.start_polling(dp, skip_updates=True)

View file

@ -6,6 +6,7 @@ import logging
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE' API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging # Configure logging
@ -18,24 +19,27 @@ bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot) dp = Dispatcher(bot)
@dp.message_handler(commands=['start']) @dp.message_handler(commands='start')
async def start_cmd_handler(message: types.Message): async def start_cmd_handler(message: types.Message):
keyboard_markup = types.ReplyKeyboardMarkup(row_width=3) keyboard_markup = types.ReplyKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually # default row_width is 3, so here we can omit it actually
# kept for clearness # kept for clearness
keyboard_markup.row(types.KeyboardButton("Yes!"), btns_text = ('Yes!', 'No!')
types.KeyboardButton("No!")) keyboard_markup.row(*(types.KeyboardButton(text) for text in btns_text))
# adds buttons as a new row to the existing keyboard # adds buttons as a new row to the existing keyboard
# the behaviour doesn't depend on row_width attribute # the behaviour doesn't depend on row_width attribute
keyboard_markup.add(types.KeyboardButton("I don't know"), more_btns_text = (
types.KeyboardButton("Who am i?"), "I don't know",
types.KeyboardButton("Where am i?"), "Who am i?",
types.KeyboardButton("Who is there?")) "Where am i?",
# adds buttons. New rows is formed according to row_width parameter "Who is there?",
)
keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text))
# adds buttons. New rows are formed according to row_width parameter
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) await message.reply("Hi!\nDo you like aiogram?", reply_markup=keyboard_markup)
@dp.message_handler() @dp.message_handler()
@ -45,15 +49,17 @@ async def all_msg_handler(message: types.Message):
# in real bot, it's better to define message_handler(text="...") for each button # in real bot, it's better to define message_handler(text="...") for each button
# but here for the simplicity only one handler is defined # but here for the simplicity only one handler is defined
text_of_button = message.text button_text = message.text
logger.debug(text_of_button) # print the text we got logger.debug('The answer is %r', button_text) # print the text we've got
if text_of_button == 'Yes!': if button_text == 'Yes!':
await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove()) reply_text = "That's great"
elif text_of_button == 'No!': elif button_text == 'No!':
await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove()) reply_text = "Oh no! Why?"
else: else:
await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove()) reply_text = "Keep calm...Everything is fine"
await message.reply(reply_text, reply_markup=types.ReplyKeyboardRemove())
# with message, we send types.ReplyKeyboardRemove() to hide the keyboard # with message, we send types.ReplyKeyboardRemove() to hide the keyboard

View file

@ -0,0 +1,53 @@
"""
This is a bot to show the usage of the builtin Text filter
Instead of a list, a single element can be passed to any filter, it will be treated as list with an element
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
# if the text from user in the list
@dp.message_handler(text=['text1', 'text2'])
async def text_in_handler(message: types.Message):
await message.answer("The message text equals to one of in the list!")
# if the text contains any string
@dp.message_handler(text_contains='example1')
@dp.message_handler(text_contains='example2')
async def text_contains_any_handler(message: types.Message):
await message.answer("The message text contains any of strings")
# if the text contains all the strings from the list
@dp.message_handler(text_contains=['str1', 'str2'])
async def text_contains_all_handler(message: types.Message):
await message.answer("The message text contains all strings from the list")
# if the text starts with any string from the list
@dp.message_handler(text_startswith=['prefix1', 'prefix2'])
async def text_startswith_handler(message: types.Message):
await message.answer("The message text starts with any of prefixes")
# if the text ends with any string from the list
@dp.message_handler(text_endswith=['postfix1', 'postfix2'])
async def text_endswith_handler(message: types.Message):
await message.answer("The message text ends with any of postfixes")
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -12,6 +12,7 @@ from aiogram.dispatcher import Dispatcher
from aiogram.utils.exceptions import Throttled from aiogram.utils.exceptions import Throttled
from aiogram.utils.executor import start_polling from aiogram.utils.executor import start_polling
API_TOKEN = 'BOT_TOKEN_HERE' API_TOKEN = 'BOT_TOKEN_HERE'
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -19,7 +20,7 @@ logging.basicConfig(level=logging.INFO)
bot = Bot(token=API_TOKEN) bot = Bot(token=API_TOKEN)
# Throttling manager does not work without Leaky Bucket. # Throttling manager does not work without Leaky Bucket.
# Then need to use storages. For example use simple in-memory storage. # You need to use a storage. For example use simple in-memory storage.
storage = MemoryStorage() storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage) dp = Dispatcher(bot, storage=storage)

View file

@ -1,176 +1,66 @@
""" import logging
Example outdated
"""
import asyncio
import ssl
import sys
from aiohttp import web
import aiogram
from aiogram import Bot, types from aiogram import Bot, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.dispatcher import Dispatcher from aiogram.dispatcher import Dispatcher
from aiogram.dispatcher.webhook import get_new_configured_app, SendMessage from aiogram.dispatcher.webhook import SendMessage
from aiogram.types import ChatType, ParseMode, ContentTypes from aiogram.utils.executor import start_webhook
from aiogram.utils.markdown import hbold, bold, text, link
TOKEN = 'BOT TOKEN HERE'
WEBHOOK_HOST = 'example.com' # Domain name or IP addres which your bot is located. API_TOKEN = 'BOT_TOKEN_HERE'
WEBHOOK_PORT = 443 # Telegram Bot API allows only for usage next ports: 443, 80, 88 or 8443
WEBHOOK_URL_PATH = '/webhook' # Part of URL
# This options needed if you use self-signed SSL certificate # webhook settings
# Instructions: https://core.telegram.org/bots/self-signed WEBHOOK_HOST = 'https://your.domain'
WEBHOOK_SSL_CERT = './webhook_cert.pem' # Path to the ssl certificate WEBHOOK_PATH = '/path/to/api'
WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
WEBHOOK_URL = f"https://{WEBHOOK_HOST}:{WEBHOOK_PORT}{WEBHOOK_URL_PATH}" # webserver settings
WEBAPP_HOST = 'localhost' # or ip
# Web app settings:
# Use LAN address to listen webhooks
# User any available port in range from 1024 to 49151 if you're using proxy, or WEBHOOK_PORT if you're using direct webhook handling
WEBAPP_HOST = 'localhost'
WEBAPP_PORT = 3001 WEBAPP_PORT = 3001
BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO logging.basicConfig(level=logging.INFO)
bot = Bot(TOKEN) bot = Bot(token=API_TOKEN)
storage = MemoryStorage() dp = Dispatcher(bot)
dp = Dispatcher(bot, storage=storage) dp.middleware.setup(LoggingMiddleware())
async def cmd_start(message: types.Message): @dp.message_handler()
# Yep. aiogram allows to respond into webhook. async def echo(message: types.Message):
# https://core.telegram.org/bots/api#making-requests-when-getting-updates # Regular request
return SendMessage(chat_id=message.chat.id, text='Hi from webhook!', # await bot.send_message(message.chat.id, message.text)
reply_to_message_id=message.message_id)
# or reply INTO webhook
return SendMessage(message.chat.id, message.text)
async def cmd_about(message: types.Message): async def on_startup(dp):
# In this function markdown utils are userd for formatting message text await bot.set_webhook(WEBHOOK_URL)
return SendMessage(message.chat.id, text( # insert code here to run it after start
bold('Hi! I\'m just a simple telegram bot.'),
'',
text('I\'m powered by', bold('Python', Version(*sys.version_info[:]))),
text('With', link(text('aiogram', aiogram.VERSION), 'https://github.com/aiogram/aiogram')),
sep='\n'
), parse_mode=ParseMode.MARKDOWN)
async def cancel(message: types.Message): async def on_shutdown(dp):
# Get current state context logging.warning('Shutting down..')
state = dp.current_state(chat=message.chat.id, user=message.from_user.id)
# If current user in any state - cancel it. # insert code here to run it before shutdown
if await state.get_state() is not None:
await state.set_state(state=None)
return SendMessage(message.chat.id, 'Current action is canceled.')
# Otherwise do nothing
# Remove webhook (not acceptable in some cases)
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")
async def cmd_id(message: types.Message):
"""
Return info about user.
"""
if message.reply_to_message:
target = message.reply_to_message.from_user
chat = message.chat
elif message.forward_from and message.chat.type == ChatType.PRIVATE:
target = message.forward_from
chat = message.forward_from or message.chat
else:
target = message.from_user
chat = message.chat
result_msg = [hbold('Info about user:'),
f"First name: {target.first_name}"]
if target.last_name:
result_msg.append(f"Last name: {target.last_name}")
if target.username:
result_msg.append(f"Username: {target.mention}")
result_msg.append(f"User ID: {target.id}")
result_msg.extend([hbold('Chat:'),
f"Type: {chat.type}",
f"Chat ID: {chat.id}"])
if chat.type != ChatType.PRIVATE:
result_msg.append(f"Title: {chat.title}")
else:
result_msg.append(f"Title: {chat.full_name}")
return SendMessage(message.chat.id, '\n'.join(result_msg), reply_to_message_id=message.message_id,
parse_mode=ParseMode.HTML)
async def on_startup(app):
# Demonstrate one of the available methods for registering handlers
# This command available only in main state (state=None)
dp.register_message_handler(cmd_start, commands=['start'])
# This handler is available in all states at any time.
dp.register_message_handler(cmd_about, commands=['help', 'about'], state='*')
dp.register_message_handler(unknown, content_types=BAD_CONTENT,
func=lambda message: message.chat.type == ChatType.PRIVATE)
# You are able to register one function handler for multiple conditions
dp.register_message_handler(cancel, commands=['cancel'], state='*')
dp.register_message_handler(cancel, func=lambda message: message.text.lower().strip() in ['cancel'], state='*')
dp.register_message_handler(cmd_id, commands=['id'], state='*')
dp.register_message_handler(cmd_id, func=lambda message: message.forward_from or
message.reply_to_message and
message.chat.type == ChatType.PRIVATE, state='*')
# Get current webhook status
webhook = await bot.get_webhook_info()
# If URL is bad
if webhook.url != WEBHOOK_URL:
# If URL doesnt match current - remove webhook
if not webhook.url:
await bot.delete_webhook()
# Set new URL for webhook
await bot.set_webhook(WEBHOOK_URL, certificate=open(WEBHOOK_SSL_CERT, 'rb'))
# If you want to use free certificate signed by LetsEncrypt you need to set only URL without sending certificate.
async def on_shutdown(app):
"""
Graceful shutdown. This method is recommended by aiohttp docs.
"""
# Remove webhook.
await bot.delete_webhook() await bot.delete_webhook()
# Close Redis connection. # Close DB connection (if used)
await dp.storage.close() await dp.storage.close()
await dp.storage.wait_closed() await dp.storage.wait_closed()
logging.warning('Bye!')
if __name__ == '__main__': if __name__ == '__main__':
# Get instance of :class:`aiohttp.web.Application` with configured router. start_webhook(
app = get_new_configured_app(dispatcher=dp, path=WEBHOOK_URL_PATH) dispatcher=dp,
webhook_path=WEBHOOK_PATH,
# Setup event handlers. on_startup=on_startup,
app.on_startup.append(on_startup) on_shutdown=on_shutdown,
app.on_shutdown.append(on_shutdown) skip_updates=True,
host=WEBAPP_HOST,
# Generate SSL context port=WEBAPP_PORT,
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) )
context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV)
# Start web-application.
web.run_app(app, host=WEBAPP_HOST, port=WEBAPP_PORT, ssl_context=context)
# Note:
# If you start your bot using nginx or Apache web server, SSL context is not required.
# Otherwise you need to set `ssl_context` parameter.

View file

@ -1,42 +0,0 @@
import asyncio
import logging
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils.executor import start_webhook
API_TOKEN = 'BOT TOKEN HERE'
# webhook settings
WEBHOOK_HOST = 'https://your.domain'
WEBHOOK_PATH = '/path/to/api'
WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
# webserver settings
WEBAPP_HOST = 'localhost' # or ip
WEBAPP_PORT = 3001
logging.basicConfig(level=logging.INFO)
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler()
async def echo(message: types.Message):
await bot.send_message(message.chat.id, message.text)
async def on_startup(dp):
await bot.set_webhook(WEBHOOK_URL)
# insert code here to run it after start
async def on_shutdown(dp):
# insert code here to run it before shutdown
pass
if __name__ == '__main__':
start_webhook(dispatcher=dp, webhook_path=WEBHOOK_PATH, on_startup=on_startup, on_shutdown=on_shutdown,
skip_updates=True, host=WEBAPP_HOST, port=WEBAPP_PORT)

View file

@ -0,0 +1,176 @@
"""
Example outdated
"""
import asyncio
import ssl
import sys
from aiohttp import web
import aiogram
from aiogram import Bot, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import Dispatcher
from aiogram.dispatcher.webhook import get_new_configured_app, SendMessage
from aiogram.types import ChatType, ParseMode, ContentTypes
from aiogram.utils.markdown import hbold, bold, text, link
TOKEN = 'BOT TOKEN HERE'
WEBHOOK_HOST = 'example.com' # Domain name or IP addres which your bot is located.
WEBHOOK_PORT = 443 # Telegram Bot API allows only for usage next ports: 443, 80, 88 or 8443
WEBHOOK_URL_PATH = '/webhook' # Part of URL
# This options needed if you use self-signed SSL certificate
# Instructions: https://core.telegram.org/bots/self-signed
WEBHOOK_SSL_CERT = './webhook_cert.pem' # Path to the ssl certificate
WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key
WEBHOOK_URL = f"https://{WEBHOOK_HOST}:{WEBHOOK_PORT}{WEBHOOK_URL_PATH}"
# Web app settings:
# Use LAN address to listen webhooks
# User any available port in range from 1024 to 49151 if you're using proxy, or WEBHOOK_PORT if you're using direct webhook handling
WEBAPP_HOST = 'localhost'
WEBAPP_PORT = 3001
BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO
bot = Bot(TOKEN)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
async def cmd_start(message: types.Message):
# Yep. aiogram allows to respond into webhook.
# https://core.telegram.org/bots/api#making-requests-when-getting-updates
return SendMessage(chat_id=message.chat.id, text='Hi from webhook!',
reply_to_message_id=message.message_id)
async def cmd_about(message: types.Message):
# In this function markdown utils are userd for formatting message text
return SendMessage(message.chat.id, text(
bold('Hi! I\'m just a simple telegram bot.'),
'',
text('I\'m powered by', bold('Python', Version(*sys.version_info[:]))),
text('With', link(text('aiogram', aiogram.VERSION), 'https://github.com/aiogram/aiogram')),
sep='\n'
), parse_mode=ParseMode.MARKDOWN)
async def cancel(message: types.Message):
# Get current state context
state = dp.current_state(chat=message.chat.id, user=message.from_user.id)
# If current user in any state - cancel it.
if await state.get_state() is not None:
await state.set_state(state=None)
return SendMessage(message.chat.id, 'Current action is canceled.')
# Otherwise do nothing
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")
async def cmd_id(message: types.Message):
"""
Return info about user.
"""
if message.reply_to_message:
target = message.reply_to_message.from_user
chat = message.chat
elif message.forward_from and message.chat.type == ChatType.PRIVATE:
target = message.forward_from
chat = message.forward_from or message.chat
else:
target = message.from_user
chat = message.chat
result_msg = [hbold('Info about user:'),
f"First name: {target.first_name}"]
if target.last_name:
result_msg.append(f"Last name: {target.last_name}")
if target.username:
result_msg.append(f"Username: {target.mention}")
result_msg.append(f"User ID: {target.id}")
result_msg.extend([hbold('Chat:'),
f"Type: {chat.type}",
f"Chat ID: {chat.id}"])
if chat.type != ChatType.PRIVATE:
result_msg.append(f"Title: {chat.title}")
else:
result_msg.append(f"Title: {chat.full_name}")
return SendMessage(message.chat.id, '\n'.join(result_msg), reply_to_message_id=message.message_id,
parse_mode=ParseMode.HTML)
async def on_startup(app):
# Demonstrate one of the available methods for registering handlers
# This command available only in main state (state=None)
dp.register_message_handler(cmd_start, commands=['start'])
# This handler is available in all states at any time.
dp.register_message_handler(cmd_about, commands=['help', 'about'], state='*')
dp.register_message_handler(unknown, content_types=BAD_CONTENT,
func=lambda message: message.chat.type == ChatType.PRIVATE)
# You are able to register one function handler for multiple conditions
dp.register_message_handler(cancel, commands=['cancel'], state='*')
dp.register_message_handler(cancel, func=lambda message: message.text.lower().strip() in ['cancel'], state='*')
dp.register_message_handler(cmd_id, commands=['id'], state='*')
dp.register_message_handler(cmd_id, func=lambda message: message.forward_from or
message.reply_to_message and
message.chat.type == ChatType.PRIVATE, state='*')
# Get current webhook status
webhook = await bot.get_webhook_info()
# If URL is bad
if webhook.url != WEBHOOK_URL:
# If URL doesnt match current - remove webhook
if not webhook.url:
await bot.delete_webhook()
# Set new URL for webhook
await bot.set_webhook(WEBHOOK_URL, certificate=open(WEBHOOK_SSL_CERT, 'rb'))
# If you want to use free certificate signed by LetsEncrypt you need to set only URL without sending certificate.
async def on_shutdown(app):
"""
Graceful shutdown. This method is recommended by aiohttp docs.
"""
# Remove webhook.
await bot.delete_webhook()
# Close Redis connection.
await dp.storage.close()
await dp.storage.wait_closed()
if __name__ == '__main__':
# Get instance of :class:`aiohttp.web.Application` with configured router.
app = get_new_configured_app(dispatcher=dp, path=WEBHOOK_URL_PATH)
# Setup event handlers.
app.on_startup.append(on_startup)
app.on_shutdown.append(on_shutdown)
# Generate SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV)
# Start web-application.
web.run_app(app, host=WEBAPP_HOST, port=WEBAPP_PORT, ssl_context=context)
# Note:
# If you start your bot using nginx or Apache web server, SSL context is not required.
# Otherwise you need to set `ssl_context` parameter.

View file

@ -0,0 +1,18 @@
import pytest
from aiogram.dispatcher.filters.builtin import Text
class TestText:
@pytest.mark.parametrize('param, key', [
('text', 'equals'),
('text_contains', 'contains'),
('text_startswith', 'startswith'),
('text_endswith', 'endswith'),
])
def test_validate(self, param, key):
value = 'spam and eggs'
config = {param: value}
res = Text.validate(config)
assert res == {key: value}

View file

@ -0,0 +1,18 @@
from aiogram.dispatcher.filters.state import StatesGroup
class TestStatesGroup:
def test_all_childs(self):
class InnerState1(StatesGroup):
pass
class InnerState2(InnerState1):
pass
class Form(StatesGroup):
inner1 = InnerState1
inner2 = InnerState2
form_childs = Form.all_childs
assert form_childs == (InnerState1, InnerState2)

View file

@ -4,38 +4,35 @@ from aiogram.dispatcher.filters import Text
from aiogram.types import Message, CallbackQuery, InlineQuery, Poll from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
def data_sample_1():
return [
('', ''),
('', 'exAmple_string'),
('example_string', 'example_string'),
('example_string', 'exAmple_string'),
('exAmple_string', 'example_string'),
('example_string', 'example_string_dsf'),
('example_string', 'example_striNG_dsf'),
('example_striNG', 'example_string_dsf'),
('example_string', 'not_example_string'),
('example_string', 'not_eXample_string'),
('EXample_string', 'not_example_string'),
]
class TestTextFilter: class TestTextFilter:
async def _run_check(self, check, test_text):
assert await check(Message(text=test_text))
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text))
assert await check(Poll(question=test_text))
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("test_prefix, test_text, ignore_case", @pytest.mark.parametrize('ignore_case', (True, False))
[('', '', True), @pytest.mark.parametrize("test_prefix, test_text", data_sample_1())
('', 'exAmple_string', True),
('', '', False),
('', 'exAmple_string', False),
('example_string', 'example_string', True),
('example_string', 'exAmple_string', True),
('exAmple_string', 'example_string', True),
('example_string', 'example_string', False),
('example_string', 'exAmple_string', False),
('exAmple_string', 'example_string', False),
('example_string', 'example_string_dsf', True),
('example_string', 'example_striNG_dsf', True),
('example_striNG', 'example_string_dsf', True),
('example_string', 'example_string_dsf', False),
('example_string', 'example_striNG_dsf', False),
('example_striNG', 'example_string_dsf', False),
('example_string', 'not_example_string', True),
('example_string', 'not_eXample_string', True),
('EXample_string', 'not_example_string', True),
('example_string', 'not_example_string', False),
('example_string', 'not_eXample_string', False),
('EXample_string', 'not_example_string', False),
])
async def test_startswith(self, test_prefix, test_text, ignore_case): async def test_startswith(self, test_prefix, test_text, ignore_case):
test_filter = Text(startswith=test_prefix, ignore_case=ignore_case) test_filter = Text(startswith=test_prefix, ignore_case=ignore_case)
@ -50,42 +47,45 @@ class TestTextFilter:
return result is _test_text.startswith(_test_prefix) return result is _test_text.startswith(_test_prefix)
assert await check(Message(text=test_text)) await self._run_check(check, test_text)
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text))
assert await check(Poll(question=test_text))
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("test_postfix, test_text, ignore_case", @pytest.mark.parametrize('ignore_case', (True, False))
[('', '', True), @pytest.mark.parametrize("test_prefix_list, test_text", [
('', 'exAmple_string', True), (['not_example', ''], ''),
('', '', False), (['', 'not_example'], 'exAmple_string'),
('', 'exAmple_string', False),
('example_string', 'example_string', True), (['not_example', 'example_string'], 'example_string'),
('example_string', 'exAmple_string', True), (['example_string', 'not_example'], 'exAmple_string'),
('exAmple_string', 'example_string', True), (['not_example', 'exAmple_string'], 'example_string'),
('example_string', 'example_string', False), (['not_example', 'example_string'], 'example_string_dsf'),
('example_string', 'exAmple_string', False), (['example_string', 'not_example'], 'example_striNG_dsf'),
('exAmple_string', 'example_string', False), (['not_example', 'example_striNG'], 'example_string_dsf'),
('example_string', 'example_string_dsf', True), (['not_example', 'example_string'], 'not_example_string'),
('example_string', 'example_striNG_dsf', True), (['example_string', 'not_example'], 'not_eXample_string'),
('example_striNG', 'example_string_dsf', True), (['not_example', 'EXample_string'], 'not_example_string'),
])
async def test_startswith_list(self, test_prefix_list, test_text, ignore_case):
test_filter = Text(startswith=test_prefix_list, ignore_case=ignore_case)
('example_string', 'example_string_dsf', False), async def check(obj):
('example_string', 'example_striNG_dsf', False), result = await test_filter.check(obj)
('example_striNG', 'example_string_dsf', False), if ignore_case:
_test_prefix_list = map(str.lower, test_prefix_list)
_test_text = test_text.lower()
else:
_test_prefix_list = test_prefix_list
_test_text = test_text
('example_string', 'not_example_string', True), return result is any(map(_test_text.startswith, _test_prefix_list))
('example_string', 'not_eXample_string', True),
('EXample_string', 'not_eXample_string', True),
('example_string', 'not_example_string', False), await self._run_check(check, test_text)
('example_string', 'not_eXample_string', False),
('EXample_string', 'not_example_string', False), @pytest.mark.asyncio
]) @pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_postfix, test_text", data_sample_1())
async def test_endswith(self, test_postfix, test_text, ignore_case): async def test_endswith(self, test_postfix, test_text, ignore_case):
test_filter = Text(endswith=test_postfix, ignore_case=ignore_case) test_filter = Text(endswith=test_postfix, ignore_case=ignore_case)
@ -100,42 +100,59 @@ class TestTextFilter:
return result is _test_text.endswith(_test_postfix) return result is _test_text.endswith(_test_postfix)
assert await check(Message(text=test_text)) await self._run_check(check, test_text)
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text))
assert await check(Poll(question=test_text))
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("test_string, test_text, ignore_case", @pytest.mark.parametrize('ignore_case', (True, False))
[('', '', True), @pytest.mark.parametrize("test_postfix_list, test_text", [
('', 'exAmple_string', True), (['', 'not_example'], ''),
('', '', False), (['not_example', ''], 'exAmple_string'),
('', 'exAmple_string', False),
('example_string', 'example_string', True), (['example_string', 'not_example'], 'example_string'),
('example_string', 'exAmple_string', True), (['not_example', 'example_string'], 'exAmple_string'),
('exAmple_string', 'example_string', True), (['exAmple_string', 'not_example'], 'example_string'),
('example_string', 'example_string', False), (['not_example', 'example_string'], 'example_string_dsf'),
('example_string', 'exAmple_string', False), (['example_string', 'not_example'], 'example_striNG_dsf'),
('exAmple_string', 'example_string', False), (['not_example', 'example_striNG'], 'example_string_dsf'),
('example_string', 'example_string_dsf', True), (['not_example', 'example_string'], 'not_example_string'),
('example_string', 'example_striNG_dsf', True), (['example_string', 'not_example'], 'not_eXample_string'),
('example_striNG', 'example_string_dsf', True), (['not_example', 'EXample_string'], 'not_example_string'),
])
async def test_endswith_list(self, test_postfix_list, test_text, ignore_case):
test_filter = Text(endswith=test_postfix_list, ignore_case=ignore_case)
('example_string', 'example_string_dsf', False), async def check(obj):
('example_string', 'example_striNG_dsf', False), result = await test_filter.check(obj)
('example_striNG', 'example_string_dsf', False), if ignore_case:
_test_postfix_list = map(str.lower, test_postfix_list)
_test_text = test_text.lower()
else:
_test_postfix_list = test_postfix_list
_test_text = test_text
('example_string', 'not_example_strin', True), return result is any(map(_test_text.endswith, _test_postfix_list))
('example_string', 'not_eXample_strin', True), await self._run_check(check, test_text)
('EXample_string', 'not_eXample_strin', True),
('example_string', 'not_example_strin', False), @pytest.mark.asyncio
('example_string', 'not_eXample_strin', False), @pytest.mark.parametrize('ignore_case', (True, False))
('EXample_string', 'not_example_strin', False), @pytest.mark.parametrize("test_string, test_text", [
]) ('', ''),
('', 'exAmple_string'),
('example_string', 'example_string'),
('example_string', 'exAmple_string'),
('exAmple_string', 'example_string'),
('example_string', 'example_string_dsf'),
('example_string', 'example_striNG_dsf'),
('example_striNG', 'example_string_dsf'),
('example_string', 'not_example_strin'),
('example_string', 'not_eXample_strin'),
('EXample_string', 'not_example_strin'),
])
async def test_contains(self, test_string, test_text, ignore_case): async def test_contains(self, test_string, test_text, ignore_case):
test_filter = Text(contains=test_string, ignore_case=ignore_case) test_filter = Text(contains=test_string, ignore_case=ignore_case)
@ -150,34 +167,46 @@ class TestTextFilter:
return result is (_test_string in _test_text) return result is (_test_string in _test_text)
assert await check(Message(text=test_text)) await self._run_check(check, test_text)
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text))
assert await check(Poll(question=test_text))
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("test_filter_text, test_text, ignore_case", @pytest.mark.parametrize('ignore_case', (True, False))
[('', '', True), @pytest.mark.parametrize("test_filter_list, test_text", [
('', 'exAmple_string', True), (['a', 'ab', 'abc'], 'A'),
('', '', False), (['a', 'ab', 'abc'], 'ab'),
('', 'exAmple_string', False), (['a', 'ab', 'abc'], 'aBc'),
(['a', 'ab', 'abc'], 'd'),
])
async def test_contains_list(self, test_filter_list, test_text, ignore_case):
test_filter = Text(contains=test_filter_list, ignore_case=ignore_case)
('example_string', 'example_string', True), async def check(obj):
('example_string', 'exAmple_string', True), result = await test_filter.check(obj)
('exAmple_string', 'example_string', True), if ignore_case:
_test_filter_list = list(map(str.lower, test_filter_list))
_test_text = test_text.lower()
else:
_test_filter_list = test_filter_list
_test_text = test_text
('example_string', 'example_string', False), return result is all(map(_test_text.__contains__, _test_filter_list))
('example_string', 'exAmple_string', False),
('exAmple_string', 'example_string', False),
('example_string', 'not_example_string', True), await self._run_check(check, test_text)
('example_string', 'not_eXample_string', True),
('EXample_string', 'not_eXample_string', True),
('example_string', 'not_example_string', False), @pytest.mark.asyncio
('example_string', 'not_eXample_string', False), @pytest.mark.parametrize('ignore_case', (True, False))
('EXample_string', 'not_example_string', False), @pytest.mark.parametrize("test_filter_text, test_text", [
]) ('', ''),
('', 'exAmple_string'),
('example_string', 'example_string'),
('example_string', 'exAmple_string'),
('exAmple_string', 'example_string'),
('example_string', 'not_example_string'),
('example_string', 'not_eXample_string'),
('EXample_string', 'not_example_string'),
])
async def test_equals_string(self, test_filter_text, test_text, ignore_case): async def test_equals_string(self, test_filter_text, test_text, ignore_case):
test_filter = Text(equals=test_filter_text, ignore_case=ignore_case) test_filter = Text(equals=test_filter_text, ignore_case=ignore_case)
@ -191,7 +220,44 @@ class TestTextFilter:
_test_text = test_text _test_text = test_text
return result is (_test_text == _test_filter_text) return result is (_test_text == _test_filter_text)
assert await check(Message(text=test_text)) await self._run_check(check, test_text)
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text)) @pytest.mark.asyncio
assert await check(Poll(question=test_text)) @pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_filter_list, test_text", [
(['new_string', ''], ''),
(['', 'new_string'], 'exAmple_string'),
(['example_string'], 'example_string'),
(['example_string'], 'exAmple_string'),
(['exAmple_string'], 'example_string'),
(['example_string'], 'not_example_string'),
(['example_string'], 'not_eXample_string'),
(['EXample_string'], 'not_example_string'),
(['example_string', 'new_string'], 'example_string'),
(['new_string', 'example_string'], 'exAmple_string'),
(['exAmple_string', 'new_string'], 'example_string'),
(['example_string', 'new_string'], 'not_example_string'),
(['new_string', 'example_string'], 'not_eXample_string'),
(['EXample_string', 'new_string'], 'not_example_string'),
])
async def test_equals_list(self, test_filter_list, test_text, ignore_case):
test_filter = Text(equals=test_filter_list, ignore_case=ignore_case)
async def check(obj):
result = await test_filter.check(obj)
if ignore_case:
_test_filter_list = list(map(str.lower, test_filter_list))
_test_text = test_text.lower()
else:
_test_filter_list = test_filter_list
_test_text = test_text
assert result is (_test_text in _test_filter_list)
await check(Message(text=test_text))
await check(CallbackQuery(data=test_text))
await check(InlineQuery(query=test_text))
await check(Poll(question=test_text))

View file

@ -8,7 +8,7 @@ USER = {
"first_name": "FirstName", "first_name": "FirstName",
"last_name": "LastName", "last_name": "LastName",
"username": "username", "username": "username",
"language_code": "ru" "language_code": "ru",
} }
CHAT = { CHAT = {
@ -16,14 +16,14 @@ CHAT = {
"first_name": "FirstName", "first_name": "FirstName",
"last_name": "LastName", "last_name": "LastName",
"username": "username", "username": "username",
"type": "private" "type": "private",
} }
PHOTO = { PHOTO = {
"file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg",
"file_size": 1101, "file_size": 1101,
"width": 90, "width": 90,
"height": 51 "height": 51,
} }
AUDIO = { AUDIO = {
@ -32,7 +32,7 @@ AUDIO = {
"title": "The Best Song", "title": "The Best Song",
"performer": "The Best Singer", "performer": "The Best Singer",
"file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI", "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI",
"file_size": 9507774 "file_size": 9507774,
} }
CHAT_MEMBER = { CHAT_MEMBER = {
@ -44,7 +44,7 @@ CHAT_MEMBER = {
"can_invite_users": True, "can_invite_users": True,
"can_restrict_members": True, "can_restrict_members": True,
"can_pin_messages": True, "can_pin_messages": True,
"can_promote_members": False "can_promote_members": False,
} }
CONTACT = { CONTACT = {
@ -57,7 +57,7 @@ DOCUMENT = {
"file_name": "test.docx", "file_name": "test.docx",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg", "file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg",
"file_size": 21331 "file_size": 21331,
} }
ANIMATION = { ANIMATION = {
@ -65,51 +65,51 @@ ANIMATION = {
"mime_type": "video/mp4", "mime_type": "video/mp4",
"thumb": PHOTO, "thumb": PHOTO,
"file_id": "CgADBAAD4DUAAoceZAe2WiE9y0crrAI", "file_id": "CgADBAAD4DUAAoceZAe2WiE9y0crrAI",
"file_size": 65837 "file_size": 65837,
} }
ENTITY_BOLD = { ENTITY_BOLD = {
"offset": 5, "offset": 5,
"length": 2, "length": 2,
"type": "bold" "type": "bold",
} }
ENTITY_ITALIC = { ENTITY_ITALIC = {
"offset": 8, "offset": 8,
"length": 1, "length": 1,
"type": "italic" "type": "italic",
} }
ENTITY_LINK = { ENTITY_LINK = {
"offset": 10, "offset": 10,
"length": 6, "length": 6,
"type": "text_link", "type": "text_link",
"url": "http://google.com/" "url": "http://google.com/",
} }
ENTITY_CODE = { ENTITY_CODE = {
"offset": 17, "offset": 17,
"length": 7, "length": 7,
"type": "code" "type": "code",
} }
ENTITY_PRE = { ENTITY_PRE = {
"offset": 30, "offset": 30,
"length": 4, "length": 4,
"type": "pre" "type": "pre",
} }
ENTITY_MENTION = { ENTITY_MENTION = {
"offset": 47, "offset": 47,
"length": 9, "length": 9,
"type": "mention" "type": "mention",
} }
GAME = { GAME = {
"title": "Karate Kido", "title": "Karate Kido",
"description": "No trees were harmed in the making of this game :)", "description": "No trees were harmed in the making of this game :)",
"photo": [PHOTO, PHOTO, PHOTO], "photo": [PHOTO, PHOTO, PHOTO],
"animation": ANIMATION "animation": ANIMATION,
} }
INVOICE = { INVOICE = {
@ -120,19 +120,19 @@ INVOICE = {
"Order our Working Time Machine today!", "Order our Working Time Machine today!",
"start_parameter": "time-machine-example", "start_parameter": "time-machine-example",
"currency": "USD", "currency": "USD",
"total_amount": 6250 "total_amount": 6250,
} }
LOCATION = { LOCATION = {
"latitude": 50.693416, "latitude": 50.693416,
"longitude": 30.624605 "longitude": 30.624605,
} }
VENUE = { VENUE = {
"location": LOCATION, "location": LOCATION,
"title": "Venue Name", "title": "Venue Name",
"address": "Venue Address", "address": "Venue Address",
"foursquare_id": "4e6f2cec483bad563d150f98" "foursquare_id": "4e6f2cec483bad563d150f98",
} }
SHIPPING_ADDRESS = { SHIPPING_ADDRESS = {
@ -141,7 +141,7 @@ SHIPPING_ADDRESS = {
"city": "DefaultCity", "city": "DefaultCity",
"street_line1": "Central", "street_line1": "Central",
"street_line2": "Middle", "street_line2": "Middle",
"post_code": "424242" "post_code": "424242",
} }
STICKER = { STICKER = {
@ -156,7 +156,7 @@ STICKER = {
"height": 128 "height": 128
}, },
"file_id": "AAbbCCddEEffGGhh1234567890", "file_id": "AAbbCCddEEffGGhh1234567890",
"file_size": 12345 "file_size": 12345,
} }
SUCCESSFUL_PAYMENT = { SUCCESSFUL_PAYMENT = {
@ -164,7 +164,7 @@ SUCCESSFUL_PAYMENT = {
"total_amount": 6250, "total_amount": 6250,
"invoice_payload": "HAPPY FRIDAYS COUPON", "invoice_payload": "HAPPY FRIDAYS COUPON",
"telegram_payment_charge_id": "_", "telegram_payment_charge_id": "_",
"provider_payment_charge_id": "12345678901234_test" "provider_payment_charge_id": "12345678901234_test",
} }
VIDEO = { VIDEO = {
@ -174,7 +174,7 @@ VIDEO = {
"mime_type": "video/quicktime", "mime_type": "video/quicktime",
"thumb": PHOTO, "thumb": PHOTO,
"file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg", "file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg",
"file_size": 10099782 "file_size": 10099782,
} }
VIDEO_NOTE = { VIDEO_NOTE = {
@ -182,14 +182,14 @@ VIDEO_NOTE = {
"length": 240, "length": 240,
"thumb": PHOTO, "thumb": PHOTO,
"file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz", "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz",
"file_size": 186562 "file_size": 186562,
} }
VOICE = { VOICE = {
"duration": 1, "duration": 1,
"mime_type": "audio/ogg", "mime_type": "audio/ogg",
"file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg", "file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg",
"file_size": 4321 "file_size": 4321,
} }
CALLBACK_QUERY = {} CALLBACK_QUERY = {}
@ -206,7 +206,7 @@ EDITED_MESSAGE = {
"chat": CHAT, "chat": CHAT,
"date": 1508825372, "date": 1508825372,
"edit_date": 1508825379, "edit_date": 1508825379,
"text": "hi there (edited)" "text": "hi there (edited)",
} }
FORWARDED_MESSAGE = { FORWARDED_MESSAGE = {
@ -219,7 +219,7 @@ FORWARDED_MESSAGE = {
"forward_date": 1522749037, "forward_date": 1522749037,
"text": "Forwarded text with entities from public channel ", "text": "Forwarded text with entities from public channel ",
"entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK, "entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK,
ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE] ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE],
} }
INLINE_QUERY = {} INLINE_QUERY = {}
@ -229,7 +229,7 @@ MESSAGE = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508709711, "date": 1508709711,
"text": "Hi, world!" "text": "Hi, world!",
} }
MESSAGE_WITH_AUDIO = { MESSAGE_WITH_AUDIO = {
@ -238,7 +238,7 @@ MESSAGE_WITH_AUDIO = {
"chat": CHAT, "chat": CHAT,
"date": 1508739776, "date": 1508739776,
"audio": AUDIO, "audio": AUDIO,
"caption": "This is my favourite song" "caption": "This is my favourite song",
} }
MESSAGE_WITH_AUTHOR_SIGNATURE = {} MESSAGE_WITH_AUTHOR_SIGNATURE = {}
@ -250,7 +250,7 @@ MESSAGE_WITH_CONTACT = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1522850298, "date": 1522850298,
"contact": CONTACT "contact": CONTACT,
} }
MESSAGE_WITH_DELETE_CHAT_PHOTO = {} MESSAGE_WITH_DELETE_CHAT_PHOTO = {}
@ -261,7 +261,7 @@ MESSAGE_WITH_DOCUMENT = {
"chat": CHAT, "chat": CHAT,
"date": 1508768012, "date": 1508768012,
"document": DOCUMENT, "document": DOCUMENT,
"caption": "Read my document" "caption": "Read my document",
} }
MESSAGE_WITH_EDIT_DATE = {} MESSAGE_WITH_EDIT_DATE = {}
@ -273,7 +273,7 @@ MESSAGE_WITH_GAME = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508824810, "date": 1508824810,
"game": GAME "game": GAME,
} }
MESSAGE_WITH_GROUP_CHAT_CREATED = {} MESSAGE_WITH_GROUP_CHAT_CREATED = {}
@ -283,7 +283,7 @@ MESSAGE_WITH_INVOICE = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508761719, "date": 1508761719,
"invoice": INVOICE "invoice": INVOICE,
} }
MESSAGE_WITH_LEFT_CHAT_MEMBER = {} MESSAGE_WITH_LEFT_CHAT_MEMBER = {}
@ -293,7 +293,7 @@ MESSAGE_WITH_LOCATION = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508755473, "date": 1508755473,
"location": LOCATION "location": LOCATION,
} }
MESSAGE_WITH_MIGRATE_TO_CHAT_ID = { MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {
@ -301,7 +301,7 @@ MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1526943253, "date": 1526943253,
"migrate_to_chat_id": -1234567890987 "migrate_to_chat_id": -1234567890987,
} }
MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = { MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {
@ -309,7 +309,7 @@ MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1526943253, "date": 1526943253,
"migrate_from_chat_id": -123456789 "migrate_from_chat_id": -123456789,
} }
MESSAGE_WITH_NEW_CHAT_MEMBERS = {} MESSAGE_WITH_NEW_CHAT_MEMBERS = {}
@ -324,7 +324,7 @@ MESSAGE_WITH_PHOTO = {
"chat": CHAT, "chat": CHAT,
"date": 1508825154, "date": 1508825154,
"photo": [PHOTO, PHOTO, PHOTO, PHOTO], "photo": [PHOTO, PHOTO, PHOTO, PHOTO],
"caption": "photo description" "caption": "photo description",
} }
MESSAGE_WITH_MEDIA_GROUP = { MESSAGE_WITH_MEDIA_GROUP = {
@ -333,7 +333,7 @@ MESSAGE_WITH_MEDIA_GROUP = {
"chat": CHAT, "chat": CHAT,
"date": 1522843665, "date": 1522843665,
"media_group_id": "12182749320567362", "media_group_id": "12182749320567362",
"photo": [PHOTO, PHOTO, PHOTO, PHOTO] "photo": [PHOTO, PHOTO, PHOTO, PHOTO],
} }
MESSAGE_WITH_PINNED_MESSAGE = {} MESSAGE_WITH_PINNED_MESSAGE = {}
@ -345,7 +345,7 @@ MESSAGE_WITH_STICKER = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508771450, "date": 1508771450,
"sticker": STICKER "sticker": STICKER,
} }
MESSAGE_WITH_SUCCESSFUL_PAYMENT = { MESSAGE_WITH_SUCCESSFUL_PAYMENT = {
@ -353,7 +353,7 @@ MESSAGE_WITH_SUCCESSFUL_PAYMENT = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508761169, "date": 1508761169,
"successful_payment": SUCCESSFUL_PAYMENT "successful_payment": SUCCESSFUL_PAYMENT,
} }
MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {}
@ -364,7 +364,7 @@ MESSAGE_WITH_VENUE = {
"chat": CHAT, "chat": CHAT,
"date": 1522849819, "date": 1522849819,
"location": LOCATION, "location": LOCATION,
"venue": VENUE "venue": VENUE,
} }
MESSAGE_WITH_VIDEO = { MESSAGE_WITH_VIDEO = {
@ -373,7 +373,7 @@ MESSAGE_WITH_VIDEO = {
"chat": CHAT, "chat": CHAT,
"date": 1508756494, "date": 1508756494,
"video": VIDEO, "video": VIDEO,
"caption": "description" "caption": "description",
} }
MESSAGE_WITH_VIDEO_NOTE = { MESSAGE_WITH_VIDEO_NOTE = {
@ -381,7 +381,7 @@ MESSAGE_WITH_VIDEO_NOTE = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1522835890, "date": 1522835890,
"video_note": VIDEO_NOTE "video_note": VIDEO_NOTE,
} }
MESSAGE_WITH_VOICE = { MESSAGE_WITH_VOICE = {
@ -389,7 +389,7 @@ MESSAGE_WITH_VOICE = {
"from": USER, "from": USER,
"chat": CHAT, "chat": CHAT,
"date": 1508768403, "date": 1508768403,
"voice": VOICE "voice": VOICE,
} }
PRE_CHECKOUT_QUERY = { PRE_CHECKOUT_QUERY = {
@ -397,7 +397,7 @@ PRE_CHECKOUT_QUERY = {
"from": USER, "from": USER,
"currency": "USD", "currency": "USD",
"total_amount": 6250, "total_amount": 6250,
"invoice_payload": "HAPPY FRIDAYS COUPON" "invoice_payload": "HAPPY FRIDAYS COUPON",
} }
REPLY_MESSAGE = { REPLY_MESSAGE = {
@ -406,37 +406,37 @@ REPLY_MESSAGE = {
"chat": CHAT, "chat": CHAT,
"date": 1508751866, "date": 1508751866,
"reply_to_message": MESSAGE, "reply_to_message": MESSAGE,
"text": "Reply to quoted message" "text": "Reply to quoted message",
} }
SHIPPING_QUERY = { SHIPPING_QUERY = {
"id": "262181558684397422", "id": "262181558684397422",
"from": USER, "from": USER,
"invoice_payload": "HAPPY FRIDAYS COUPON", "invoice_payload": "HAPPY FRIDAYS COUPON",
"shipping_address": SHIPPING_ADDRESS "shipping_address": SHIPPING_ADDRESS,
} }
USER_PROFILE_PHOTOS = { USER_PROFILE_PHOTOS = {
"total_count": 1, "photos": [ "total_count": 1, "photos": [
[PHOTO, PHOTO, PHOTO] [PHOTO, PHOTO, PHOTO],
] ],
} }
FILE = { FILE = {
"file_id": "XXXYYYZZZ", "file_id": "XXXYYYZZZ",
"file_size": 5254, "file_size": 5254,
"file_path": "voice\/file_8" "file_path": "voice/file_8",
} }
INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123' INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123'
UPDATE = { UPDATE = {
"update_id": 123456789, "update_id": 123456789,
"message": MESSAGE "message": MESSAGE,
} }
WEBHOOK_INFO = { WEBHOOK_INFO = {
"url": "", "url": "",
"has_custom_certificate": False, "has_custom_certificate": False,
"pending_update_count": 0 "pending_update_count": 0,
} }

View file

@ -0,0 +1,77 @@
from aiogram import types
from .dataset import CHAT_MEMBER
chat_member = types.ChatMember(**CHAT_MEMBER)
def test_export():
exported = chat_member.to_python()
assert isinstance(exported, dict)
assert exported == CHAT_MEMBER
def test_user():
assert isinstance(chat_member.user, types.User)
def test_status():
assert isinstance(chat_member.status, str)
assert chat_member.status == CHAT_MEMBER['status']
def test_privileges():
assert isinstance(chat_member.can_be_edited, bool)
assert chat_member.can_be_edited == CHAT_MEMBER['can_be_edited']
assert isinstance(chat_member.can_change_info, bool)
assert chat_member.can_change_info == CHAT_MEMBER['can_change_info']
assert isinstance(chat_member.can_delete_messages, bool)
assert chat_member.can_delete_messages == CHAT_MEMBER['can_delete_messages']
assert isinstance(chat_member.can_invite_users, bool)
assert chat_member.can_invite_users == CHAT_MEMBER['can_invite_users']
assert isinstance(chat_member.can_restrict_members, bool)
assert chat_member.can_restrict_members == CHAT_MEMBER['can_restrict_members']
assert isinstance(chat_member.can_pin_messages, bool)
assert chat_member.can_pin_messages == CHAT_MEMBER['can_pin_messages']
assert isinstance(chat_member.can_promote_members, bool)
assert chat_member.can_promote_members == CHAT_MEMBER['can_promote_members']
def test_int():
assert int(chat_member) == chat_member.user.id
assert isinstance(int(chat_member), int)
def test_chat_member_status():
assert types.ChatMemberStatus.CREATOR == 'creator'
assert types.ChatMemberStatus.ADMINISTRATOR == 'administrator'
assert types.ChatMemberStatus.MEMBER == 'member'
assert types.ChatMemberStatus.RESTRICTED == 'restricted'
assert types.ChatMemberStatus.LEFT == 'left'
assert types.ChatMemberStatus.KICKED == 'kicked'
def test_chat_member_status_filters():
assert types.ChatMemberStatus.is_chat_admin(chat_member.status)
assert types.ChatMemberStatus.is_chat_member(chat_member.status)
assert types.ChatMemberStatus.is_chat_admin(types.ChatMemberStatus.CREATOR)
assert types.ChatMemberStatus.is_chat_admin(types.ChatMemberStatus.ADMINISTRATOR)
assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.CREATOR)
assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.ADMINISTRATOR)
assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.MEMBER)
assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED)
assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.LEFT)
assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.KICKED)
def test_chat_member_filters():
assert chat_member.is_chat_admin()
assert chat_member.is_chat_member()