diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index ab12d702..82ea7257 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,8 +1,2 @@
-# These are supported funding model platforms
-
-github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
-patreon: # Replace with a single Patreon username
-open_collective: aiogram # Replace with a single Open Collective username
-ko_fi: # Replace with a single Ko-fi username
-tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
-custom: # Replace with a single custom sponsorship URL
+github: [JRootJunior]
+open_collective: aiogram
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 65baad51..1c33729f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -8,7 +8,7 @@ Fixes # (issue)
Please delete options that are not relevant.
-- [ ] Documentstion (typos, code examples or any documentation update)
+- [ ] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
diff --git a/README.md b/README.md
index 9f977023..155b2848 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
# AIOGram
+[](https://opencollective.com/aiogram)
[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live)
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
-[](https://core.telegram.org/bots/api)
+[](https://core.telegram.org/bots/api)
[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
[](https://github.com/aiogram/aiogram/issues)
[](https://opensource.org/licenses/MIT)
@@ -24,3 +25,33 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
- Source: [Github repo](https://github.com/aiogram/aiogram)
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
+
+## Contributors
+
+### Code Contributors
+
+This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)].
+
+
+### Financial Contributors
+
+Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)]
+
+#### Individuals
+
+
+
+#### Organizations
+
+Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)]
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.rst b/README.rst
index 0377aad9..e4768f68 100644
--- a/README.rst
+++ b/README.rst
@@ -21,7 +21,7 @@ AIOGramBot
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
-.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram
+.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
diff --git a/aiogram/__init__.py b/aiogram/__init__.py
index 6fe7d1c6..b81ceedb 100644
--- a/aiogram/__init__.py
+++ b/aiogram/__init__.py
@@ -38,5 +38,5 @@ __all__ = [
'utils'
]
-__version__ = '2.2.dev1'
-__api_version__ = '4.3'
+__version__ = '2.3.dev1'
+__api_version__ = '4.4'
diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py
index 6c51b295..675626ac 100644
--- a/aiogram/bot/api.py
+++ b/aiogram/bot/api.py
@@ -147,7 +147,7 @@ class Methods(Helper):
"""
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
- List is updated to Bot API 4.3
+ List is updated to Bot API 4.4
"""
mode = HelperMode.lowerCamelCase
@@ -182,6 +182,7 @@ class Methods(Helper):
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
+ SET_CHAT_PERMISSIONS = Item() # setChatPermissions
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
SET_CHAT_PHOTO = Item() # setChatPhoto
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py
index 85773e30..608abd06 100644
--- a/aiogram/bot/base.py
+++ b/aiogram/bot/base.py
@@ -13,7 +13,7 @@ from aiohttp.helpers import sentinel
from . import api
from ..types import ParseMode, base
from ..utils import json
-from ..utils.auth_widget import check_token
+from ..utils.auth_widget import check_integrity
class BaseBot:
@@ -99,6 +99,12 @@ class BaseBot:
self.parse_mode = parse_mode
+ def __del__(self):
+ if self.loop.is_running():
+ self.loop.create_task(self.close())
+ else:
+ self.loop.run_until_complete(self.close())
+
@staticmethod
def _prepare_timeout(
value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]
@@ -266,4 +272,4 @@ class BaseBot:
self.parse_mode = None
def check_auth_widget(self, data):
- return check_token(data, self.__token)
+ return check_integrity(self.__token, data)
diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py
index b0fc3725..b30e5309 100644
--- a/aiogram/bot/bot.py
+++ b/aiogram/bot/bot.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import typing
+import warnings
from .base import BaseBot, api
from .. import types
@@ -337,12 +338,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`types.Message`
"""
reply_markup = prepare_arg(reply_markup)
- payload = generate_payload(**locals(), exclude=['audio'])
+ payload = generate_payload(**locals(), exclude=['audio', 'thumb'])
if self.parse_mode:
payload.setdefault('parse_mode', self.parse_mode)
files = {}
prepare_file(payload, files, 'audio', audio)
+ prepare_attachment(payload, files, 'thumb', thumb)
result = await self.request(api.Methods.SEND_AUDIO, payload, files)
return types.Message(**result)
@@ -1014,6 +1016,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
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,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
@@ -1030,6 +1034,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
+ :param permissions: New user permissions
+ :type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time
:type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@@ -1047,8 +1053,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
until_date = prepare_arg(until_date)
+ permissions = prepare_arg(permissions)
payload = generate_payload(**locals())
+ for permission in ['can_send_messages',
+ 'can_send_media_messages',
+ 'can_send_other_messages',
+ 'can_add_web_page_previews']:
+ if permission in payload:
+ warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions "
+ f"in a single argument of the type ChatPermissions instead of "
+ f"passing regular argument {payload[permission]}",
+ DeprecationWarning, stacklevel=2)
+
result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload)
return result
@@ -1099,6 +1116,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
return result
+ async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
+ permissions: types.ChatPermissions) -> base.Boolean:
+ """
+ Use this method to set default chat permissions for all members.
+ The bot must be an administrator in the group or a supergroup for this to work and must have the
+ can_restrict_members admin rights.
+
+ Returns True on success.
+
+ :param chat_id: Unique identifier for the target chat or username of the target supergroup
+ :param permissions: New default chat permissions
+ :return: True on success.
+ """
+ permissions = prepare_arg(permissions)
+ payload = generate_payload(**locals())
+
+ result = await self.request(api.Methods.SET_CHAT_PERMISSIONS, payload)
+ return result
+
async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String:
"""
Use this method to generate a new invite link for a chat; any previously generated link is revoked.
diff --git a/aiogram/contrib/fsm_storage/files.py b/aiogram/contrib/fsm_storage/files.py
index f67a6f69..455ca3f0 100644
--- a/aiogram/contrib/fsm_storage/files.py
+++ b/aiogram/contrib/fsm_storage/files.py
@@ -20,7 +20,8 @@ class _FileStorage(MemoryStorage):
pass
async def close(self):
- self.write(self.path)
+ if self.data:
+ self.write(self.path)
await super(_FileStorage, self).close()
def read(self, path: pathlib.Path):
diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py
new file mode 100644
index 00000000..9ec18090
--- /dev/null
+++ b/aiogram/contrib/fsm_storage/mongo.py
@@ -0,0 +1,200 @@
+"""
+This module has mongo storage for finite-state machine
+ based on `aiomongo 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
diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py
index c3a91f00..106a7b97 100644
--- a/aiogram/contrib/fsm_storage/redis.py
+++ b/aiogram/contrib/fsm_storage/redis.py
@@ -35,7 +35,6 @@ class RedisStorage(BaseStorage):
await dp.storage.wait_closed()
"""
-
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
self._host = host
self._port = port
@@ -62,8 +61,6 @@ class RedisStorage(BaseStorage):
async def redis(self) -> aioredis.RedisConnection:
"""
Get Redis connection
-
- This property is awaitable.
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
@@ -222,9 +219,12 @@ class RedisStorage2(BaseStorage):
await dp.storage.wait_closed()
"""
-
- def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None,
- pool_size=10, loop=None, prefix='fsm', **kwargs):
+ def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
+ ssl=None, pool_size=10, loop=None, prefix='fsm',
+ state_ttl: int = 0,
+ data_ttl: int = 0,
+ bucket_ttl: int = 0,
+ **kwargs):
self._host = host
self._port = port
self._db = db
@@ -235,14 +235,16 @@ class RedisStorage2(BaseStorage):
self._kwargs = kwargs
self._prefix = (prefix,)
+ self._state_ttl = state_ttl
+ self._data_ttl = data_ttl
+ self._bucket_ttl = bucket_ttl
+
self._redis: aioredis.RedisConnection = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def redis(self) -> aioredis.Redis:
"""
Get Redis connection
-
- This property is awaitable.
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
@@ -294,14 +296,14 @@ class RedisStorage2(BaseStorage):
if state is None:
await redis.delete(key)
else:
- await redis.set(key, state)
+ await redis.set(key, state, expire=self._state_ttl)
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
- await redis.set(key, json.dumps(data))
+ await redis.set(key, json.dumps(data), expire=self._data_ttl)
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
@@ -329,7 +331,7 @@ class RedisStorage2(BaseStorage):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
redis = await self.redis()
- await redis.set(key, json.dumps(bucket))
+ await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
@@ -338,7 +340,7 @@ class RedisStorage2(BaseStorage):
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
- await self.set_bucket(chat=chat, user=user, data=temp_bucket)
+ await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py
index 264bc653..0bb10680 100644
--- a/aiogram/contrib/middlewares/i18n.py
+++ b/aiogram/contrib/middlewares/i18n.py
@@ -97,17 +97,15 @@ class I18nMiddleware(BaseMiddleware):
if locale not in self.locales:
if n is 1:
return singular
- else:
- return plural
+ return plural
translator = self.locales[locale]
if plural is None:
return translator.gettext(singular)
- else:
- return translator.ngettext(singular, plural, n)
+ return translator.ngettext(singular, plural, n)
- def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
+ def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy:
"""
Lazy get text
@@ -115,9 +113,10 @@ class I18nMiddleware(BaseMiddleware):
:param plural:
:param n:
:param locale:
+ :param enable_cache:
:return:
"""
- return LazyProxy(self.gettext, singular, plural, n, locale)
+ return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py
index 1a3566c6..9f389b60 100644
--- a/aiogram/contrib/middlewares/logging.py
+++ b/aiogram/contrib/middlewares/logging.py
@@ -89,34 +89,39 @@ class LoggingMiddleware(BaseMiddleware):
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
if callback_query.message:
+ text = (f"Received callback query [ID:{callback_query.id}] "
+ f"from user [ID:{callback_query.from_user.id}] "
+ f"for message [ID:{callback_query.message.message_id}] "
+ f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
+
if callback_query.message.from_user:
- self.logger.info(f"Received callback query [ID:{callback_query.id}] "
- f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
- f"from user [ID:{callback_query.message.from_user.id}]")
- else:
- self.logger.info(f"Received callback query [ID:{callback_query.id}] "
- f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
+ text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
+
+ self.logger.info(text)
+
else:
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
- f"from inline message [ID:{callback_query.inline_message_id}] "
- f"from user [ID:{callback_query.from_user.id}]")
+ f"from user [ID:{callback_query.from_user.id}] "
+ f"for inline message [ID:{callback_query.inline_message_id}] ")
async def on_post_process_callback_query(self, callback_query, results, data: dict):
if callback_query.message:
+ text = (f"{HANDLED_STR[bool(len(results))]} "
+ f"callback query [ID:{callback_query.id}] "
+ f"from user [ID:{callback_query.from_user.id}] "
+ f"for message [ID:{callback_query.message.message_id}] "
+ f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
+
if callback_query.message.from_user:
- self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
- f"callback query [ID:{callback_query.id}] "
- f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
- f"from user [ID:{callback_query.message.from_user.id}]")
- else:
- self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
- f"callback query [ID:{callback_query.id}] "
- f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
+ text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
+
+ self.logger.info(text)
+
else:
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
- f"from inline message [ID:{callback_query.inline_message_id}] "
- f"from user [ID:{callback_query.from_user.id}]")
+ f"from user [ID:{callback_query.from_user.id}]"
+ f"from inline message [ID:{callback_query.inline_message_id}]")
async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict):
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index e11ff536..6d16a005 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -8,8 +8,9 @@ import typing
import aiohttp
from aiohttp.helpers import sentinel
+from aiogram.utils.deprecated import renamed_argument
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
- RegexpCommandsFilter, StateFilter, Text
+ RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter
from .handler import Handler
from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@@ -85,34 +86,64 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(StateFilter, exclude_event_handlers=[
self.errors_handlers,
- self.poll_handlers
+ self.poll_handlers,
])
filters_factory.bind(ContentTypeFilter, event_handlers=[
- self.message_handlers, self.edited_message_handlers,
- self.channel_post_handlers, self.edited_channel_post_handlers,
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
]),
filters_factory.bind(Command, event_handlers=[
- self.message_handlers, self.edited_message_handlers
+ self.message_handlers,
+ self.edited_message_handlers
])
filters_factory.bind(Text, event_handlers=[
- self.message_handlers, self.edited_message_handlers,
- self.channel_post_handlers, self.edited_channel_post_handlers,
- self.callback_query_handlers, self.poll_handlers
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
+ self.callback_query_handlers,
+ self.poll_handlers,
+ self.inline_query_handlers,
])
filters_factory.bind(HashTag, event_handlers=[
- self.message_handlers, self.edited_message_handlers,
- self.channel_post_handlers, self.edited_channel_post_handlers
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
])
filters_factory.bind(Regexp, event_handlers=[
- self.message_handlers, self.edited_message_handlers,
- self.channel_post_handlers, self.edited_channel_post_handlers,
- self.callback_query_handlers, self.poll_handlers
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
+ self.callback_query_handlers,
+ self.poll_handlers,
+ self.inline_query_handlers,
])
filters_factory.bind(RegexpCommandsFilter, event_handlers=[
- self.message_handlers, self.edited_message_handlers
+ self.message_handlers,
+ self.edited_message_handlers,
])
filters_factory.bind(ExceptionsFilter, event_handlers=[
- self.errors_handlers
+ self.errors_handlers,
+ ])
+ filters_factory.bind(AdminFilter, event_handlers=[
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
+ self.callback_query_handlers,
+ self.inline_query_handlers,
+ ])
+ filters_factory.bind(IDFilter, event_handlers=[
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
+ self.callback_query_handlers,
+ self.inline_query_handlers,
])
def __del__(self):
@@ -884,15 +915,17 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
return FSMContext(storage=self.storage, chat=chat, user=user)
- async def throttle(self, key, *, rate=None, user=None, chat=None, no_error=None) -> bool:
+ @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
+ @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
+ async def throttle(self, key, *, rate=None, user_id=None, chat_id=None, no_error=None) -> bool:
"""
Execute throttling manager.
Returns True if limit has not exceeded otherwise raises ThrottleError or returns False
:param key: key in storage
:param rate: limit (by default is equal to default rate limit)
- :param user: user id
- :param chat: chat id
+ :param user_id: user id
+ :param chat_id: chat id
:param no_error: return boolean value instead of raising error
:return: bool
"""
@@ -903,14 +936,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
no_error = self.no_throttle_error
if rate is None:
rate = self.throttling_rate_limit
- if user is None and chat is None:
- user = types.User.get_current()
- chat = types.Chat.get_current()
+ if user_id is None and chat_id is None:
+ user_id = types.User.get_current().id
+ chat_id = types.Chat.get_current().id
# Detect current time
now = time.time()
- bucket = await self.storage.get_bucket(chat=chat, user=user)
+ bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
# Fix bucket
if bucket is None:
@@ -934,53 +967,57 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
else:
data[EXCEEDED_COUNT] = 1
bucket[key].update(data)
- await self.storage.set_bucket(chat=chat, user=user, bucket=bucket)
+ await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket)
if not result and not no_error:
# Raise if it is allowed
- raise Throttled(key=key, chat=chat, user=user, **data)
+ raise Throttled(key=key, chat=chat_id, user=user_id, **data)
return result
- async def check_key(self, key, chat=None, user=None):
+ @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
+ @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
+ async def check_key(self, key, chat_id=None, user_id=None):
"""
Get information about key in bucket
:param key:
- :param chat:
- :param user:
+ :param chat_id:
+ :param user_id:
:return:
"""
if not self.storage.has_bucket():
raise RuntimeError('This storage does not provide Leaky Bucket')
- if user is None and chat is None:
- user = types.User.get_current()
- chat = types.Chat.get_current()
+ if user_id is None and chat_id is None:
+ user_id = types.User.get_current()
+ chat_id = types.Chat.get_current()
- bucket = await self.storage.get_bucket(chat=chat, user=user)
+ bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
data = bucket.get(key, {})
- return Throttled(key=key, chat=chat, user=user, **data)
+ return Throttled(key=key, chat=chat_id, user=user_id, **data)
- async def release_key(self, key, chat=None, user=None):
+ @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3)
+ @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4)
+ async def release_key(self, key, chat_id=None, user_id=None):
"""
Release blocked key
:param key:
- :param chat:
- :param user:
+ :param chat_id:
+ :param user_id:
:return:
"""
if not self.storage.has_bucket():
raise RuntimeError('This storage does not provide Leaky Bucket')
- if user is None and chat is None:
- user = types.User.get_current()
- chat = types.Chat.get_current()
+ if user_id is None and chat_id is None:
+ user_id = types.User.get_current()
+ chat_id = types.Chat.get_current()
- bucket = await self.storage.get_bucket(chat=chat, user=user)
+ bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
if bucket and key in bucket:
del bucket['key']
- await self.storage.set_bucket(chat=chat, user=user, bucket=bucket)
+ await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket)
return True
return False
@@ -1025,3 +1062,64 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
if run_task:
return self.async_task(callback)
return callback
+
+ def throttled(self, on_throttled: typing.Optional[typing.Callable] = None,
+ key=None, rate=None,
+ user_id=None, chat_id=None):
+ """
+ Meta-decorator for throttling.
+ Invokes on_throttled if the handler was throttled.
+
+ Example:
+
+ .. code-block:: python3
+
+ async def handler_throttled(message: types.Message, **kwargs):
+ await message.answer("Throttled!")
+
+ @dp.throttled(handler_throttled)
+ async def some_handler(message: types.Message):
+ await message.answer("Didn't throttled!")
+
+ :param on_throttled: the callable object that should be either a function or return a coroutine
+ :param key: key in storage
+ :param rate: limit (by default is equal to default rate limit)
+ :param user_id: user id
+ :param chat_id: chat id
+ :return: decorator
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ async def wrapped(*args, **kwargs):
+ is_not_throttled = await self.throttle(key if key is not None else func.__name__,
+ rate=rate,
+ user_id=user_id, chat_id=chat_id,
+ no_error=True)
+ if is_not_throttled:
+ return await func(*args, **kwargs)
+ else:
+ kwargs.update(
+ {
+ 'rate': rate,
+ 'key': key,
+ 'user_id': user_id,
+ 'chat_id': chat_id
+ }
+ ) # update kwargs with parameters which were given to throttled
+
+ if on_throttled:
+ if asyncio.iscoroutinefunction(on_throttled):
+ await on_throttled(*args, **kwargs)
+ else:
+ kwargs.update(
+ {
+ 'loop': asyncio.get_running_loop()
+ }
+ )
+ partial_func = functools.partial(on_throttled, *args, **kwargs)
+ asyncio.get_running_loop().run_in_executor(None,
+ partial_func
+ )
+ return wrapped
+
+ return decorator
diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py
index 2ae959cf..277db03a 100644
--- a/aiogram/dispatcher/filters/__init__.py
+++ b/aiogram/dispatcher/filters/__init__.py
@@ -1,5 +1,5 @@
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
- ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
+ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
@@ -23,8 +23,10 @@ __all__ = [
'Regexp',
'StateFilter',
'Text',
+ 'IDFilter',
+ 'AdminFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
- 'check_filters'
+ 'check_filters',
]
diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py
index 011b9b67..e15b98de 100644
--- a/aiogram/dispatcher/filters/builtin.py
+++ b/aiogram/dispatcher/filters/builtin.py
@@ -9,7 +9,7 @@ from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
-from aiogram.types import CallbackQuery, Message, InlineQuery, Poll
+from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
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():
return False
- elif prefix not in prefixes:
+ if prefix not in prefixes:
return False
- elif (command.lower() if ignore_case else command) not in commands:
+ if (command.lower() if ignore_case else command) not in commands:
return False
return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)}
@@ -149,7 +149,7 @@ class CommandStart(Command):
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
"""
- super(CommandStart, self).__init__(['start'])
+ super().__init__(['start'])
self.deep_link = deep_link
async def check(self, message: types.Message):
@@ -159,7 +159,7 @@ class CommandStart(Command):
:param message:
:return:
"""
- check = await super(CommandStart, self).check(message)
+ check = await super().check(message)
if check and self.deep_link is not None:
if not isinstance(self.deep_link, re.Pattern):
@@ -179,7 +179,7 @@ class CommandHelp(Command):
"""
def __init__(self):
- super(CommandHelp, self).__init__(['help'])
+ super().__init__(['help'])
class CommandSettings(Command):
@@ -188,7 +188,7 @@ class CommandSettings(Command):
"""
def __init__(self):
- super(CommandSettings, self).__init__(['settings'])
+ super().__init__(['settings'])
class CommandPrivacy(Command):
@@ -197,7 +197,7 @@ class CommandPrivacy(Command):
"""
def __init__(self):
- super(CommandPrivacy, self).__init__(['privacy'])
+ super().__init__(['privacy'])
class Text(Filter):
@@ -205,33 +205,44 @@ class Text(Filter):
Simple text filter
"""
+ _default_params = (
+ ('text', 'equals'),
+ ('text_contains', 'contains'),
+ ('text_startswith', 'startswith'),
+ ('text_endswith', 'endswith'),
+ )
+
def __init__(self,
- equals: Optional[Union[str, LazyProxy]] = None,
- contains: Optional[Union[str, LazyProxy]] = None,
- startswith: Optional[Union[str, LazyProxy]] = None,
- endswith: Optional[Union[str, LazyProxy]] = None,
+ equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
+ contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
+ startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
+ endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
ignore_case=False):
"""
Check text for one of pattern. Only one mode can be used in one filter.
+ In every pattern, a single string is treated as a list with 1 element.
- :param equals:
- :param contains:
- :param startswith:
- :param endswith:
+ :param equals: True if object's text in the list
+ :param contains: True if object's text contains all strings from the list
+ :param startswith: True if object's text starts with any of strings from the list
+ :param endswith: True if object's text ends with any of strings from the list
:param ignore_case: case insensitive
"""
# Only one mode can be used. check it.
- check = sum(map(bool, (equals, contains, startswith, endswith)))
+ check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
if check > 1:
args = "' and '".join([arg[0] for arg in [('equals', equals),
('contains', contains),
('startswith', startswith),
('endswith', endswith)
- ] if arg[1]])
+ ] if arg[1] is not None])
raise ValueError(f"Arguments '{args}' cannot be used together.")
elif check == 0:
raise ValueError(f"No one mode is specified!")
+ equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
+ else e,
+ (equals, contains, endswith, startswith))
self.equals = equals
self.contains = contains
self.endswith = endswith
@@ -240,16 +251,11 @@ class Text(Filter):
@classmethod
def validate(cls, full_config: Dict[str, Any]):
- if 'text' in full_config:
- return {'equals': full_config.pop('text')}
- elif 'text_contains' in full_config:
- return {'contains': full_config.pop('text_contains')}
- elif 'text_startswith' in full_config:
- return {'startswith': full_config.pop('text_startswith')}
- elif 'text_endswith' in full_config:
- return {'endswith': full_config.pop('text_endswith')}
+ for param, key in cls._default_params:
+ if param in full_config:
+ return {key: full_config.pop(param)}
- async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
+ async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
text = obj.text or obj.caption or ''
if not text and obj.poll:
@@ -265,15 +271,26 @@ class Text(Filter):
if self.ignore_case:
text = text.lower()
+ _pre_process_func = lambda s: str(s).lower()
+ else:
+ _pre_process_func = str
- if self.equals:
- return text == str(self.equals)
- elif self.contains:
- return str(self.contains) in text
- elif self.startswith:
- return text.startswith(str(self.startswith))
- elif self.endswith:
- return text.endswith(str(self.endswith))
+ # now check
+ if self.equals is not None:
+ equals = list(map(_pre_process_func, self.equals))
+ return text in equals
+
+ if self.contains is not None:
+ contains = list(map(_pre_process_func, self.contains))
+ return all(map(text.__contains__, contains))
+
+ if self.startswith is not None:
+ startswith = list(map(_pre_process_func, self.startswith))
+ return any(map(text.startswith, startswith))
+
+ if self.endswith is not None:
+ endswith = list(map(_pre_process_func, self.endswith))
+ return any(map(text.endswith, endswith))
return False
@@ -359,13 +376,17 @@ class Regexp(Filter):
if 'regexp' in full_config:
return {'regexp': full_config.pop('regexp')}
- async def check(self, obj: Union[Message, CallbackQuery]):
+ async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
content = obj.text or obj.caption or ''
if not content and obj.poll:
content = obj.poll.question
elif isinstance(obj, CallbackQuery) and obj.data:
content = obj.data
+ elif isinstance(obj, InlineQuery):
+ content = obj.query
+ elif isinstance(obj, Poll):
+ content = obj.question
else:
return False
@@ -487,3 +508,120 @@ class ExceptionsFilter(BoundFilter):
return True
except:
return False
+
+
+class IDFilter(Filter):
+
+ def __init__(self,
+ user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
+ chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
+ ):
+ """
+ :param user_id:
+ :param chat_id:
+ """
+ if user_id is None and chat_id is None:
+ raise ValueError("Both user_id and chat_id can't be None")
+
+ self.user_id = None
+ self.chat_id = None
+ if user_id:
+ if isinstance(user_id, Iterable):
+ self.user_id = list(map(int, user_id))
+ else:
+ self.user_id = [int(user_id), ]
+ if chat_id:
+ if isinstance(chat_id, Iterable):
+ self.chat_id = list(map(int, chat_id))
+ else:
+ self.chat_id = [int(chat_id), ]
+
+ @classmethod
+ def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
+ result = {}
+ if 'user_id' in full_config:
+ result['user_id'] = full_config.pop('user_id')
+
+ if 'chat_id' in full_config:
+ result['chat_id'] = full_config.pop('chat_id')
+
+ return result
+
+ async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
+ if isinstance(obj, Message):
+ user_id = obj.from_user.id
+ chat_id = obj.chat.id
+ elif isinstance(obj, CallbackQuery):
+ user_id = obj.from_user.id
+ chat_id = None
+ if obj.message is not None:
+ # if the button was sent with message
+ chat_id = obj.message.chat.id
+ elif isinstance(obj, InlineQuery):
+ user_id = obj.from_user.id
+ chat_id = None
+ else:
+ return False
+
+ if self.user_id and self.chat_id:
+ return user_id in self.user_id and chat_id in self.chat_id
+ if self.user_id:
+ return user_id in self.user_id
+ if self.chat_id:
+ return chat_id in self.chat_id
+
+ return False
+
+
+class AdminFilter(Filter):
+ """
+ Checks if user is admin in a chat.
+ If is_chat_admin is not set, the filter will check in the current chat (correct only for messages).
+ is_chat_admin is required for InlineQuery.
+ """
+
+ def __init__(self, is_chat_admin: Optional[Union[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
diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py
index 89e3e792..13b188ff 100644
--- a/aiogram/dispatcher/filters/factory.py
+++ b/aiogram/dispatcher/filters/factory.py
@@ -70,4 +70,4 @@ class FiltersFactory:
yield filter_
if full_config:
- raise NameError('Invalid filter name(s): \'' + '\', '.join(full_config.keys()) + '\'')
+ raise NameError("Invalid filter name(s): '" + "', ".join(full_config.keys()) + "'")
diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py
index 46e44fc9..220ef96c 100644
--- a/aiogram/dispatcher/filters/filters.py
+++ b/aiogram/dispatcher/filters/filters.py
@@ -82,7 +82,7 @@ class FilterRecord:
Filters record for factory
"""
- def __init__(self, callback: typing.Callable,
+ def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'],
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None):
@@ -202,14 +202,14 @@ class BoundFilter(Filter):
You need to implement ``__init__`` method with single argument related with key attribute
and ``check`` method where you need to implement filter logic.
"""
-
- """Unique name of the filter argument. You need to override this attribute."""
+
key = None
- """If :obj:`True` this filter will be added to the all of the registered handlers"""
+ """Unique name of the filter argument. You need to override this attribute."""
required = False
- """Default value for configure required filters"""
+ """If :obj:`True` this filter will be added to the all of the registered handlers"""
default = None
-
+ """Default value for configure required filters"""
+
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
"""
diff --git a/aiogram/dispatcher/filters/state.py b/aiogram/dispatcher/filters/state.py
index afe08e64..16937e1c 100644
--- a/aiogram/dispatcher/filters/state.py
+++ b/aiogram/dispatcher/filters/state.py
@@ -25,17 +25,17 @@ class State:
@property
def state(self):
- if self._state is None:
- return None
- elif self._state == '*':
+ if self._state is None or self._state == '*':
return self._state
- elif self._group_name is None and self._group:
+
+ if self._group_name is None and self._group:
group = self._group.__full_group_name__
elif self._group_name:
group = self._group_name
else:
group = '@'
- return f"{group}:{self._state}"
+
+ return f'{group}:{self._state}'
def set_parent(self, group):
if not issubclass(group, StatesGroup):
@@ -73,7 +73,6 @@ class StatesGroupMeta(type):
elif inspect.isclass(prop) and issubclass(prop, StatesGroup):
childs.append(prop)
prop._parent = cls
- # continue
cls._parent = None
cls._childs = tuple(childs)
@@ -83,13 +82,13 @@ class StatesGroupMeta(type):
return cls
@property
- def __group_name__(cls):
+ def __group_name__(cls) -> str:
return cls._group_name
@property
- def __full_group_name__(cls):
+ def __full_group_name__(cls) -> str:
if cls._parent:
- return cls._parent.__full_group_name__ + '.' + cls._group_name
+ return '.'.join((cls._parent.__full_group_name__, cls._group_name))
return cls._group_name
@property
@@ -97,7 +96,7 @@ class StatesGroupMeta(type):
return cls._states
@property
- def childs(cls):
+ def childs(cls) -> tuple:
return cls._childs
@property
@@ -130,9 +129,9 @@ class StatesGroupMeta(type):
def __contains__(cls, item):
if isinstance(item, str):
return item in cls.all_states_names
- elif isinstance(item, State):
+ if isinstance(item, State):
return item in cls.all_states
- elif isinstance(item, StatesGroup):
+ if isinstance(item, StatesGroup):
return item in cls.all_childs
return False
diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py
index 17b715d1..cd5e9b50 100644
--- a/aiogram/dispatcher/handler.py
+++ b/aiogram/dispatcher/handler.py
@@ -1,7 +1,7 @@
import inspect
from contextvars import ContextVar
from dataclasses import dataclass
-from typing import Optional, Iterable
+from typing import Optional, Iterable, List
ctx_data = ContextVar('ctx_handler_data')
current_handler = ContextVar('current_handler')
@@ -25,9 +25,8 @@ class CancelHandler(Exception):
def _get_spec(func: callable):
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
func = func.__wrapped__
-
spec = inspect.getfullargspec(func)
- return spec, func
+ return spec
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
@@ -42,11 +41,10 @@ class Handler:
self.dispatcher = dispatcher
self.once = once
- self.handlers = []
+ self.handlers: List[Handler.HandlerObj] = []
self.middleware_key = middleware_key
def register(self, handler, filters=None, index=None):
- from .filters import get_filters_spec
"""
Register callback
@@ -56,7 +54,9 @@ class Handler:
:param filters: list of filters
:param index: you can reorder handlers
"""
- spec, handler = _get_spec(handler)
+ from .filters import get_filters_spec
+
+ spec = _get_spec(handler)
if filters and not isinstance(filters, (list, tuple, set)):
filters = [filters]
diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py
index 4c06c2af..135fe21e 100644
--- a/aiogram/dispatcher/webhook.py
+++ b/aiogram/dispatcher/webhook.py
@@ -5,6 +5,7 @@ import functools
import ipaddress
import itertools
import typing
+import logging
from typing import Dict, List, Optional, Union
from aiohttp import web
@@ -35,6 +36,8 @@ TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22')
allowed_ips = set()
+log = logging.getLogger(__name__)
+
def _check_ip(ip: str) -> bool:
"""
@@ -77,7 +80,7 @@ class WebhookRequestHandler(web.View):
.. code-block:: python3
- app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler')
+ app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler')
But first you need to configure application for getting Dispatcher instance from request handler!
It must always be with key 'BOT_DISPATCHER'
@@ -258,7 +261,9 @@ class WebhookRequestHandler(web.View):
if self.request.app.get('_check_ip', False):
ip_address, accept = self.check_ip()
if not accept:
+ log.warning(f"Blocking request from an unauthorized IP: {ip_address}")
raise web.HTTPUnauthorized()
+
# context.set_value('TELEGRAM_IP', ip_address)
@@ -518,7 +523,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio
'disable_web_page_preview': self.disable_web_page_preview,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
def write(self, *text, sep=' '):
@@ -637,7 +642,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -699,7 +704,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'title': self.title,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -812,7 +817,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'caption': self.caption,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -866,7 +871,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'duration': self.duration,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -919,7 +924,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'length': self.length,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1045,7 +1050,7 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'longitude': self.longitude,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1104,7 +1109,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'foursquare_id': self.foursquare_id,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1155,7 +1160,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'last_name': self.last_name,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1215,7 +1220,7 @@ class KickChatMember(BaseResponse):
return {
'chat_id': self.chat_id,
'user_id': self.user_id,
- 'until_date': prepare_arg(self.until_date)
+ 'until_date': prepare_arg(self.until_date),
}
@@ -1603,7 +1608,7 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin):
'text': self.text,
'parse_mode': self.parse_mode,
'disable_web_page_preview': self.disable_web_page_preview,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1644,7 +1649,7 @@ class EditMessageCaption(BaseResponse):
'message_id': self.message_id,
'inline_message_id': self.inline_message_id,
'caption': self.caption,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1680,7 +1685,7 @@ class EditMessageReplyMarkup(BaseResponse):
'chat_id': self.chat_id,
'message_id': self.message_id,
'inline_message_id': self.inline_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1751,7 +1756,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'sticker': self.sticker,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
@@ -1843,7 +1848,7 @@ class AddStickerToSet(BaseResponse):
'name': self.name,
'png_sticker': self.png_sticker,
'emojis': self.emojis,
- 'mask_position': prepare_arg(self.mask_position)
+ 'mask_position': prepare_arg(self.mask_position),
}
@@ -2172,5 +2177,5 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'game_short_name': self.game_short_name,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
- 'reply_markup': prepare_arg(self.reply_markup)
+ 'reply_markup': prepare_arg(self.reply_markup),
}
diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py
index 5395e486..37dc4b3e 100644
--- a/aiogram/types/__init__.py
+++ b/aiogram/types/__init__.py
@@ -7,6 +7,7 @@ from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
from .chat_member import ChatMember, ChatMemberStatus
+from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py
index cd34f1be..f5c521a5 100644
--- a/aiogram/types/chat.py
+++ b/aiogram/types/chat.py
@@ -5,6 +5,7 @@ import typing
from . import base
from . import fields
+from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from ..utils import helper
from ..utils import markdown
@@ -27,6 +28,7 @@ class Chat(base.TelegramObject):
description: base.String = fields.Field()
invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message')
+ permissions: ChatPermissions = fields.Field(base=ChatPermissions)
sticker_set_name: base.String = 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)
async def restrict(self, user_id: base.Integer,
+ permissions: typing.Optional[ChatPermissions] = None,
until_date: typing.Union[base.Integer, None] = None,
can_send_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
:type user_id: :obj:`base.Integer`
+ :param permissions: New user permissions
+ :type permissions: :obj:`ChatPermissions`
:param until_date: Date when restrictions will be lifted for the user, unix time.
:type until_date: :obj:`typing.Union[base.Integer, None]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@@ -232,7 +237,9 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
- return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date,
+ return await self.bot.restrict_chat_member(self.id, user_id=user_id,
+ permissions=permissions,
+ until_date=until_date,
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_other_messages=can_send_other_messages,
diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py
index 6679c5e0..7e05a33f 100644
--- a/aiogram/types/chat_member.py
+++ b/aiogram/types/chat_member.py
@@ -1,5 +1,6 @@
import datetime
import warnings
+from typing import Optional
from . import base
from . import fields
@@ -28,22 +29,17 @@ class ChatMember(base.TelegramObject):
is_member: base.Boolean = fields.Field()
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
+ can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
- def is_admin(self):
- warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. '
- 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2',
- DeprecationWarning, stacklevel=2)
- return self.is_chat_admin()
+ def is_chat_admin(self) -> bool:
+ return ChatMemberStatus.is_chat_admin(self.status)
- def is_chat_admin(self):
- return ChatMemberStatus.is_admin(self.status)
+ def is_chat_member(self) -> bool:
+ return ChatMemberStatus.is_chat_member(self.status)
- def is_chat_member(self):
- return ChatMemberStatus.is_member(self.status)
-
- def __int__(self):
+ def __int__(self) -> int:
return self.user.id
@@ -51,33 +47,19 @@ class ChatMemberStatus(helper.Helper):
"""
Chat member status
"""
-
mode = helper.HelperMode.lowercase
CREATOR = helper.Item() # creator
ADMINISTRATOR = helper.Item() # administrator
MEMBER = helper.Item() # member
+ RESTRICTED = helper.Item() # restricted
LEFT = helper.Item() # left
KICKED = helper.Item() # kicked
@classmethod
- def is_admin(cls, role):
- warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. '
- 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2',
- DeprecationWarning, stacklevel=2)
- return cls.is_chat_admin(role)
-
- @classmethod
- def is_member(cls, role):
- warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. '
- 'This method renamed to `is_chat_member` and will be available until aiogram 2.2',
- DeprecationWarning, stacklevel=2)
- return cls.is_chat_member(role)
-
- @classmethod
- def is_chat_admin(cls, role):
+ def is_chat_admin(cls, role: str) -> bool:
return role in [cls.ADMINISTRATOR, cls.CREATOR]
@classmethod
- def is_chat_member(cls, role):
- return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR]
+ def is_chat_member(cls, role: str) -> bool:
+ return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED]
diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py
new file mode 100644
index 00000000..9d44653e
--- /dev/null
+++ b/aiogram/types/chat_permissions.py
@@ -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,
+ )
diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py
index 7bb58a7a..95ca75ae 100644
--- a/aiogram/types/input_media.py
+++ b/aiogram/types/input_media.py
@@ -26,7 +26,7 @@ class InputMedia(base.TelegramObject):
media: base.String = fields.Field(alias='media', on_change='_media_changed')
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
caption: base.String = fields.Field()
- parse_mode: base.Boolean = fields.Field()
+ parse_mode: base.String = fields.Field()
def __init__(self, *args, **kwargs):
self._thumb_file = None
@@ -110,7 +110,7 @@ class InputMediaAnimation(InputMedia):
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
- parse_mode: base.Boolean = None, **kwargs):
+ parse_mode: base.String = None, **kwargs):
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, conf=kwargs)
@@ -124,7 +124,7 @@ class InputMediaDocument(InputMedia):
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
- caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
+ caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
@@ -150,7 +150,7 @@ class InputMediaAudio(InputMedia):
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
- parse_mode: base.Boolean = None, **kwargs):
+ parse_mode: base.String = None, **kwargs):
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
performer=performer, title=title,
@@ -165,7 +165,7 @@ class InputMediaPhoto(InputMedia):
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
- caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
+ caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
@@ -186,7 +186,7 @@ class InputMediaVideo(InputMedia):
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
- parse_mode: base.Boolean = None,
+ parse_mode: base.String = None,
supports_streaming: base.Boolean = None, **kwargs):
super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
@@ -277,7 +277,7 @@ class MediaGroup(base.TelegramObject):
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
- parse_mode: base.Boolean = None):
+ parse_mode: base.String = None):
"""
Attach animation
@@ -299,7 +299,7 @@ class MediaGroup(base.TelegramObject):
self.attach(audio)
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
- caption: base.String = None, parse_mode: base.Boolean = None):
+ caption: base.String = None, parse_mode: base.String = None):
"""
Attach document
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index a8e65a32..5347027c 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -14,7 +14,7 @@ from .contact import Contact
from .document import Document
from .force_reply import ForceReply
from .game import Game
-from .inline_keyboard import InlineKeyboardMarkup, InlineKeyboardButton
+from .inline_keyboard import InlineKeyboardMarkup
from .input_media import MediaGroup, InputMedia
from .invoice import Invoice
from .location import Location
@@ -32,7 +32,6 @@ from .video_note import VideoNote
from .voice import Voice
from ..utils import helper
from ..utils import markdown as md
-from ..utils.deprecated import warn_deprecated
class Message(base.TelegramObject):
@@ -94,60 +93,60 @@ class Message(base.TelegramObject):
def content_type(self):
if self.text:
return ContentType.TEXT
- elif self.audio:
+ if self.audio:
return ContentType.AUDIO
- elif self.animation:
+ if self.animation:
return ContentType.ANIMATION
- elif self.document:
+ if self.document:
return ContentType.DOCUMENT
- elif self.game:
+ if self.game:
return ContentType.GAME
- elif self.photo:
+ if self.photo:
return ContentType.PHOTO
- elif self.sticker:
+ if self.sticker:
return ContentType.STICKER
- elif self.video:
+ if self.video:
return ContentType.VIDEO
- elif self.video_note:
+ if self.video_note:
return ContentType.VIDEO_NOTE
- elif self.voice:
+ if self.voice:
return ContentType.VOICE
- elif self.contact:
+ if self.contact:
return ContentType.CONTACT
- elif self.venue:
+ if self.venue:
return ContentType.VENUE
- elif self.location:
+ if self.location:
return ContentType.LOCATION
- elif self.new_chat_members:
+ if self.new_chat_members:
return ContentType.NEW_CHAT_MEMBERS
- elif self.left_chat_member:
+ if self.left_chat_member:
return ContentType.LEFT_CHAT_MEMBER
- elif self.invoice:
+ if self.invoice:
return ContentType.INVOICE
- elif self.successful_payment:
+ if self.successful_payment:
return ContentType.SUCCESSFUL_PAYMENT
- elif self.connected_website:
+ if self.connected_website:
return ContentType.CONNECTED_WEBSITE
- elif self.migrate_from_chat_id:
+ if self.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
- elif self.pinned_message:
+ if self.pinned_message:
return ContentType.PINNED_MESSAGE
- elif self.new_chat_title:
+ if self.new_chat_title:
return ContentType.NEW_CHAT_TITLE
- elif self.new_chat_photo:
+ if self.new_chat_photo:
return ContentType.NEW_CHAT_PHOTO
- elif self.delete_chat_photo:
+ if self.delete_chat_photo:
return ContentType.DELETE_CHAT_PHOTO
- elif self.group_chat_created:
+ if self.group_chat_created:
return ContentType.GROUP_CHAT_CREATED
- elif self.passport_data:
+ if self.passport_data:
return ContentType.PASSPORT_DATA
- elif self.poll:
+ if self.poll:
return ContentType.POLL
- else:
- return ContentType.UNKNOWN
+
+ return ContentType.UNKNOWN
def is_command(self):
"""
@@ -959,71 +958,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
- async def send_animation(self, animation: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- width: typing.Union[base.Integer, None] = None,
- height: typing.Union[base.Integer, None] = None,
- thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
- """
- Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
-
- On success, the sent Message is returned.
- Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
-
- Source https://core.telegram.org/bots/api#sendanimation
-
- :param animation: Animation to send. Pass a file_id as String to send an animation that exists
- on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation
- from the Internet, or upload a new animation using multipart/form-data
- :type animation: :obj:`typing.Union[base.InputFile, base.String]`
- :param duration: Duration of sent animation in seconds
- :type duration: :obj:`typing.Union[base.Integer, None]`
- :param width: Animation width
- :type width: :obj:`typing.Union[base.Integer, None]`
- :param height: Animation height
- :type height: :obj:`typing.Union[base.Integer, None]`
- :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail‘s width and height should not exceed 90.
- :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
- :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters
- :type caption: :obj:`typing.Union[base.String, None]`
- :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
- fixed-width text or inline URLs in the media caption
- :type parse_mode: :obj:`typing.Union[base.String, None]`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound
- :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
- :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
- custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
- :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
- types.ReplyKeyboardRemove, types.ForceReply], None]`
- :param reply: fill 'reply_to_message_id'
- :return: On success, the sent Message is returned
- :rtype: :obj:`types.Message`
- """
- warn_deprecated('"Message.send_animation" method will be removed in 2.2 version.\n'
- 'Use "Message.reply_animation" instead.',
- stacklevel=8)
-
- return await self.bot.send_animation(self.chat.id,
- animation=animation,
- duration=duration,
- width=width,
- height=height,
- thumb=thumb,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
-
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
@@ -1323,55 +1257,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
- async def send_venue(self,
- latitude: base.Float, longitude: base.Float,
- title: base.String, address: base.String,
- foursquare_id: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
- """
- Use this method to send information about a venue.
-
- Source: https://core.telegram.org/bots/api#sendvenue
-
- :param latitude: Latitude of the venue
- :type latitude: :obj:`base.Float`
- :param longitude: Longitude of the venue
- :type longitude: :obj:`base.Float`
- :param title: Name of the venue
- :type title: :obj:`base.String`
- :param address: Address of the venue
- :type address: :obj:`base.String`
- :param foursquare_id: Foursquare identifier of the venue
- :type foursquare_id: :obj:`typing.Union[base.String, None]`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
- :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
- :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
- custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
- :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
- types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
- :param reply: fill 'reply_to_message_id'
- :return: On success, the sent Message is returned.
- :rtype: :obj:`types.Message`
- """
- warn_deprecated('"Message.send_venue" method will be removed in 2.2 version.\n'
- 'Use "Message.reply_venue" instead.',
- stacklevel=8)
-
- return await self.bot.send_venue(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- title=title,
- address=address,
- foursquare_id=foursquare_id,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
-
async def reply_venue(self,
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
@@ -1417,46 +1302,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
- async def send_contact(self, phone_number: base.String,
- first_name: base.String, last_name: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
- """
- Use this method to send phone contacts.
-
- Source: https://core.telegram.org/bots/api#sendcontact
-
- :param phone_number: Contact's phone number
- :type phone_number: :obj:`base.String`
- :param first_name: Contact's first name
- :type first_name: :obj:`base.String`
- :param last_name: Contact's last name
- :type last_name: :obj:`typing.Union[base.String, None]`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
- :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
- :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
- custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
- :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
- types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
- :param reply: fill 'reply_to_message_id'
- :return: On success, the sent Message is returned.
- :rtype: :obj:`types.Message`
- """
- warn_deprecated('"Message.send_contact" method will be removed in 2.2 version.\n'
- 'Use "Message.reply_contact" instead.',
- stacklevel=8)
-
- return await self.bot.send_contact(chat_id=self.chat.id,
- phone_number=phone_number,
- first_name=first_name, last_name=last_name,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
-
async def reply_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
@@ -1719,6 +1564,103 @@ class Message(base.TelegramObject):
"""
return await self.chat.pin_message(self.message_id, disable_notification)
+ async def send_copy(
+ self: Message,
+ chat_id: typing.Union[str, int],
+ with_markup: bool = False,
+ disable_notification: typing.Optional[bool] = None,
+ reply_to_message_id: typing.Optional[int] = None,
+ ) -> Message:
+ """
+ Send copy of current message
+
+ :param chat_id:
+ :param with_markup:
+ :param disable_notification:
+ :param reply_to_message_id:
+ :return:
+ """
+ kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML}
+
+ if disable_notification is not None:
+ kwargs["disable_notification"] = disable_notification
+ if reply_to_message_id is not None:
+ kwargs["reply_to_message_id"] = reply_to_message_id
+ if with_markup and self.reply_markup:
+ kwargs["reply_markup"] = self.reply_markup
+
+ text = self.html_text if (self.text or self.caption) else None
+
+ if self.text:
+ return await self.bot.send_message(text=text, **kwargs)
+ elif self.audio:
+ return await self.bot.send_audio(
+ audio=self.audio.file_id,
+ caption=text,
+ title=self.audio.title,
+ performer=self.audio.performer,
+ duration=self.audio.duration,
+ **kwargs
+ )
+ elif self.animation:
+ return await self.bot.send_animation(
+ animation=self.animation.file_id, caption=text, **kwargs
+ )
+ elif self.document:
+ return await self.bot.send_document(
+ document=self.document.file_id, caption=text, **kwargs
+ )
+ elif self.photo:
+ return await self.bot.send_photo(
+ photo=self.photo[-1].file_id, caption=text, **kwargs
+ )
+ elif self.sticker:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_sticker(sticker=self.sticker.file_id, **kwargs)
+ elif self.video:
+ return await self.bot.send_video(
+ video=self.video.file_id, caption=text, **kwargs
+ )
+ elif self.video_note:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_video_note(
+ video_note=self.video_note.file_id, **kwargs
+ )
+ elif self.voice:
+ return await self.bot.send_voice(voice=self.voice.file_id, **kwargs)
+ elif self.contact:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_contact(
+ phone_number=self.contact.phone_number,
+ first_name=self.contact.first_name,
+ last_name=self.contact.last_name,
+ vcard=self.contact.vcard,
+ **kwargs
+ )
+ elif self.venue:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_venue(
+ latitude=self.venue.location.latitude,
+ longitude=self.venue.location.longitude,
+ title=self.venue.title,
+ address=self.venue.address,
+ foursquare_id=self.venue.foursquare_id,
+ foursquare_type=self.venue.foursquare_type,
+ **kwargs
+ )
+ elif self.location:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_location(
+ latitude=self.location.latitude, longitude=self.location.longitude, **kwargs
+ )
+ elif self.poll:
+ kwargs.pop("parse_mode")
+ return await self.bot.send_poll(
+ question=self.poll.question, options=self.poll.options, **kwargs
+ )
+ else:
+ raise TypeError("This type of message can't be copied.")
+
def __int__(self):
return self.message_id
diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py
index ae2f174f..f0ad75d6 100644
--- a/aiogram/types/message_entity.py
+++ b/aiogram/types/message_entity.py
@@ -49,31 +49,26 @@ class MessageEntity(base.TelegramObject):
entity_text = self.get_text(text)
if self.type == MessageEntityType.BOLD:
- if as_html:
- return markdown.hbold(entity_text)
- return markdown.bold(entity_text)
- elif self.type == MessageEntityType.ITALIC:
- if as_html:
- return markdown.hitalic(entity_text)
- return markdown.italic(entity_text)
- elif self.type == MessageEntityType.PRE:
- if as_html:
- return markdown.hpre(entity_text)
- return markdown.pre(entity_text)
- elif self.type == MessageEntityType.CODE:
- if as_html:
- return markdown.hcode(entity_text)
- return markdown.code(entity_text)
- elif self.type == MessageEntityType.URL:
- if as_html:
- return markdown.hlink(entity_text, entity_text)
- return markdown.link(entity_text, entity_text)
- elif self.type == MessageEntityType.TEXT_LINK:
- if as_html:
- return markdown.hlink(entity_text, self.url)
- return markdown.link(entity_text, self.url)
- elif self.type == MessageEntityType.TEXT_MENTION and self.user:
+ method = markdown.hbold if as_html else markdown.bold
+ return method(entity_text)
+ if self.type == MessageEntityType.ITALIC:
+ method = markdown.hitalic if as_html else markdown.italic
+ return method(entity_text)
+ if self.type == MessageEntityType.PRE:
+ method = markdown.hpre if as_html else markdown.pre
+ return method(entity_text)
+ if self.type == MessageEntityType.CODE:
+ method = markdown.hcode if as_html else markdown.code
+ return method(entity_text)
+ if self.type == MessageEntityType.URL:
+ method = markdown.hlink if as_html else markdown.link
+ return method(entity_text, entity_text)
+ if self.type == MessageEntityType.TEXT_LINK:
+ method = markdown.hlink if as_html else markdown.link
+ return method(entity_text, self.url)
+ if self.type == MessageEntityType.TEXT_MENTION and self.user:
return self.user.get_mention(entity_text, as_html=as_html)
+
return entity_text
diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py
index f11a1760..13f8412f 100644
--- a/aiogram/types/mixins.py
+++ b/aiogram/types/mixins.py
@@ -24,7 +24,7 @@ class Downloadable:
if destination is None:
destination = file.file_path
elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination):
- os.path.join(destination, file.file_path)
+ destination = os.path.join(destination, file.file_path)
else:
is_path = False
diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py
index b2fd7ef6..8da1e9eb 100644
--- a/aiogram/types/sticker.py
+++ b/aiogram/types/sticker.py
@@ -14,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
file_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
+ is_animated: base.Boolean = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
emoji: base.String = fields.Field()
set_name: base.String = fields.Field()
diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py
index 9d302bae..cb30abe6 100644
--- a/aiogram/types/sticker_set.py
+++ b/aiogram/types/sticker_set.py
@@ -13,5 +13,6 @@ class StickerSet(base.TelegramObject):
"""
name: base.String = fields.Field()
title: base.String = fields.Field()
+ is_animated: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field()
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
diff --git a/aiogram/types/user.py b/aiogram/types/user.py
index 441c275f..27ee27e0 100644
--- a/aiogram/types/user.py
+++ b/aiogram/types/user.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from typing import Optional
+
import babel
from . import base
@@ -45,7 +47,7 @@ class User(base.TelegramObject):
return self.full_name
@property
- def locale(self) -> babel.core.Locale or None:
+ def locale(self) -> Optional[babel.core.Locale]:
"""
Get user's locale
diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py
index b9084eb1..a39a0eed 100644
--- a/aiogram/utils/auth_widget.py
+++ b/aiogram/utils/auth_widget.py
@@ -8,7 +8,10 @@ import collections
import hashlib
import hmac
+from aiogram.utils.deprecated import deprecated
+
+@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`', stacklevel=3)
def generate_hash(data: dict, token: str) -> str:
"""
Generate secret hash
@@ -24,6 +27,7 @@ def generate_hash(data: dict, token: str) -> str:
return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
+@deprecated('`check_token` helper was renamed to `check_integrity`', stacklevel=3)
def check_token(data: dict, token: str) -> bool:
"""
Validate auth token
@@ -34,3 +38,32 @@ def check_token(data: dict, token: str) -> bool:
"""
param_hash = data.get('hash', '') or ''
return param_hash == generate_hash(data, token)
+
+
+def check_signature(token: str, hash: str, **kwargs) -> bool:
+ """
+ Generate hexadecimal representation
+ of the HMAC-SHA-256 signature of the data-check-string
+ with the SHA256 hash of the bot's token used as a secret key
+
+ :param token:
+ :param hash:
+ :param kwargs: all params received on auth
+ :return:
+ """
+ secret = hashlib.sha256(token.encode('utf-8'))
+ check_string = '\n'.join(map(lambda k: f'{k}={kwargs[k]}', sorted(kwargs)))
+ hmac_string = hmac.new(secret.digest(), check_string.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
+ return hmac_string == hash
+
+
+def check_integrity(token: str, data: dict) -> bool:
+ """
+ Verify the authentication and the integrity
+ of the data received on user's auth
+
+ :param token: Bot's token
+ :param data: all data that came on auth
+ :return:
+ """
+ return check_signature(token, **data)
diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py
index 916d08c4..b0162a7e 100644
--- a/aiogram/utils/callback_data.py
+++ b/aiogram/utils/callback_data.py
@@ -28,13 +28,13 @@ class CallbackData:
def __init__(self, prefix, *parts, sep=':'):
if not isinstance(prefix, str):
- raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}")
- elif not prefix:
- raise ValueError('Prefix can\'t be empty')
- elif sep in prefix:
- raise ValueError(f"Separator '{sep}' can't be used in prefix")
- elif not parts:
- raise TypeError('Parts is not passed!')
+ raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
+ if not prefix:
+ raise ValueError("Prefix can't be empty")
+ if sep in prefix:
+ raise ValueError(f"Separator {sep!r} can't be used in prefix")
+ if not parts:
+ raise TypeError('Parts were not passed!')
self.prefix = prefix
self.sep = sep
@@ -59,20 +59,20 @@ class CallbackData:
if args:
value = args.pop(0)
else:
- raise ValueError(f"Value for '{part}' is not passed!")
+ raise ValueError(f'Value for {part!r} was not passed!')
if value is not None and not isinstance(value, str):
value = str(value)
if not value:
- raise ValueError(f"Value for part {part} can't be empty!'")
- elif self.sep in value:
- raise ValueError(f"Symbol defined as separator can't be used in values of parts")
+ raise ValueError(f"Value for part {part!r} can't be empty!'")
+ if self.sep in value:
+ raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
data.append(value)
if args or kwargs:
- raise TypeError('Too many arguments is passed!')
+ raise TypeError('Too many arguments were passed!')
callback_data = self.sep.join(data)
if len(callback_data) > 64:
@@ -106,30 +106,31 @@ class CallbackData:
"""
for key in config.keys():
if key not in self._part_names:
- raise ValueError(f"Invalid field name '{key}'")
+ raise ValueError(f'Invalid field name {key!r}')
return CallbackDataFilter(self, config)
class CallbackDataFilter(Filter):
+
def __init__(self, factory: CallbackData, config: typing.Dict[str, str]):
self.config = config
self.factory = factory
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]):
- raise ValueError('That filter can\'t be used in filters factory!')
+ raise ValueError("That filter can't be used in filters factory!")
async def check(self, query: types.CallbackQuery):
try:
data = self.factory.parse(query.data)
except ValueError:
return False
- else:
- for key, value in self.config.items():
- if isinstance(value, (list, tuple, set)):
- if data.get(key) not in value:
- return False
- else:
- if value != data.get(key):
- return False
- return {'callback_data': data}
+
+ for key, value in self.config.items():
+ if isinstance(value, (list, tuple, set, frozenset)):
+ if data.get(key) not in value:
+ return False
+ else:
+ if data.get(key) != value:
+ return False
+ return {'callback_data': data}
diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py
index 1ea2561d..cb22c506 100644
--- a/aiogram/utils/deprecated.py
+++ b/aiogram/utils/deprecated.py
@@ -1,17 +1,17 @@
-"""
-Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
-"""
-
-import functools
+import asyncio
import inspect
import warnings
+import functools
+from typing import Callable
-def deprecated(reason):
+def deprecated(reason, stacklevel=2) -> Callable:
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
+
+ Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
"""
if isinstance(reason, str):
@@ -33,7 +33,7 @@ def deprecated(reason):
@functools.wraps(func)
def wrapper(*args, **kwargs):
- warn_deprecated(msg.format(name=func.__name__, reason=reason))
+ warn_deprecated(msg.format(name=func.__name__, reason=reason), stacklevel=stacklevel)
warnings.simplefilter('default', DeprecationWarning)
return func(*args, **kwargs)
@@ -41,7 +41,7 @@ def deprecated(reason):
return decorator
- elif inspect.isclass(reason) or inspect.isfunction(reason):
+ if inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'.
#
@@ -60,16 +60,76 @@ def deprecated(reason):
@functools.wraps(func1)
def wrapper1(*args, **kwargs):
- warn_deprecated(msg1.format(name=func1.__name__))
+ warn_deprecated(msg1.format(name=func1.__name__), stacklevel=stacklevel)
return func1(*args, **kwargs)
return wrapper1
- else:
- raise TypeError(repr(type(reason)))
+ raise TypeError(repr(type(reason)))
def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2):
warnings.simplefilter('always', warning)
warnings.warn(message, category=warning, stacklevel=stacklevel)
warnings.simplefilter('default', warning)
+
+
+def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3):
+ """
+ A meta-decorator to mark an argument as deprecated.
+
+ .. code-block:: python3
+
+ @renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default
+ @renamed_argument("user", "user_id", "3.0", stacklevel=4)
+ def some_function(user_id, chat_id=None):
+ print(f"user_id={user_id}, chat_id={chat_id}")
+
+ some_function(user=123) # prints 'user_id=123, chat_id=None' with warning
+ some_function(123) # prints 'user_id=123, chat_id=None' without warning
+ some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning
+
+
+ :param old_name:
+ :param new_name:
+ :param until_version: the version in which the argument is scheduled to be removed
+ :param stacklevel: leave it to default if it's the first decorator used.
+ Increment with any new decorator used.
+ :return: decorator
+ """
+
+ def decorator(func):
+ 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)
+ return await func(*args, **kwargs)
+ else:
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ if old_name in kwargs:
+ warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` "
+ f"is renamed to `{new_name}` "
+ f"and will be removed in aiogram {until_version}",
+ stacklevel=stacklevel)
+ kwargs.update(
+ {
+ new_name: kwargs[old_name],
+ }
+ )
+ kwargs.pop(old_name)
+ return func(*args, **kwargs)
+
+ return wrapped
+
+ return decorator
diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py
index afd623cc..f77fe257 100644
--- a/aiogram/utils/exceptions.py
+++ b/aiogram/utils/exceptions.py
@@ -1,94 +1,92 @@
"""
-TelegramAPIError
- ValidationError
- Throttled
- BadRequest
- MessageError
- MessageNotModified
- MessageToForwardNotFound
- MessageToDeleteNotFound
- MessageIdentifierNotSpecified
- MessageTextIsEmpty
- MessageCantBeEdited
- MessageCantBeDeleted
- MessageToEditNotFound
- MessageToReplyNotFound
- ToMuchMessages
- PollError
- PollCantBeStopped
- PollHasAlreadyClosed
- PollsCantBeSentToPrivateChats
- PollSizeError
- PollMustHaveMoreOptions
- PollCantHaveMoreOptions
- PollsOptionsLengthTooLong
- PollOptionsMustBeNonEmpty
- PollQuestionMustBeNonEmpty
- MessageWithPollNotFound (with MessageError)
- MessageIsNotAPoll (with MessageError)
- ObjectExpectedAsReplyMarkup
- InlineKeyboardExpected
- ChatNotFound
- ChatDescriptionIsNotModified
- InvalidQueryID
- InvalidPeerID
- InvalidHTTPUrlContent
- ButtonURLInvalid
- URLHostIsEmpty
- StartParamInvalid
- ButtonDataInvalid
- WrongFileIdentifier
- GroupDeactivated
- BadWebhook
- WebhookRequireHTTPS
- BadWebhookPort
- BadWebhookAddrInfo
- BadWebhookNoAddressAssociatedWithHostname
- NotFound
- MethodNotKnown
- PhotoAsInputFileRequired
- InvalidStickersSet
- NoStickerInRequest
- ChatAdminRequired
- NeedAdministratorRightsInTheChannel
- MethodNotAvailableInPrivateChats
- CantDemoteChatCreator
- CantRestrictSelf
- NotEnoughRightsToRestrict
- PhotoDimensions
- UnavailableMembers
- TypeOfFileMismatch
- WrongRemoteFileIdSpecified
- PaymentProviderInvalid
- CurrencyTotalAmountInvalid
- CantParseUrl
- UnsupportedUrlProtocol
- CantParseEntities
- ResultIdDuplicate
- ConflictError
- TerminatedByOtherGetUpdates
- CantGetUpdates
- Unauthorized
- BotKicked
- BotBlocked
- UserDeactivated
- CantInitiateConversation
- CantTalkWithBots
- NetworkError
- RetryAfter
- MigrateToChat
- RestartingTelegram
+- TelegramAPIError
+ - ValidationError
+ - Throttled
+ - BadRequest
+ - MessageError
+ - MessageNotModified
+ - MessageToForwardNotFound
+ - MessageToDeleteNotFound
+ - MessageIdentifierNotSpecified
+ - MessageTextIsEmpty
+ - MessageCantBeEdited
+ - MessageCantBeDeleted
+ - MessageToEditNotFound
+ - MessageToReplyNotFound
+ - ToMuchMessages
+ - PollError
+ - PollCantBeStopped
+ - PollHasAlreadyClosed
+ - PollsCantBeSentToPrivateChats
+ - PollSizeError
+ - PollMustHaveMoreOptions
+ - PollCantHaveMoreOptions
+ - PollsOptionsLengthTooLong
+ - PollOptionsMustBeNonEmpty
+ - PollQuestionMustBeNonEmpty
+ - MessageWithPollNotFound (with MessageError)
+ - MessageIsNotAPoll (with MessageError)
+ - ObjectExpectedAsReplyMarkup
+ - InlineKeyboardExpected
+ - ChatNotFound
+ - ChatDescriptionIsNotModified
+ - InvalidQueryID
+ - InvalidPeerID
+ - InvalidHTTPUrlContent
+ - ButtonURLInvalid
+ - URLHostIsEmpty
+ - StartParamInvalid
+ - ButtonDataInvalid
+ - WrongFileIdentifier
+ - GroupDeactivated
+ - BadWebhook
+ - WebhookRequireHTTPS
+ - BadWebhookPort
+ - BadWebhookAddrInfo
+ - BadWebhookNoAddressAssociatedWithHostname
+ - NotFound
+ - MethodNotKnown
+ - PhotoAsInputFileRequired
+ - InvalidStickersSet
+ - NoStickerInRequest
+ - ChatAdminRequired
+ - NeedAdministratorRightsInTheChannel
+ - MethodNotAvailableInPrivateChats
+ - CantDemoteChatCreator
+ - CantRestrictSelf
+ - NotEnoughRightsToRestrict
+ - PhotoDimensions
+ - UnavailableMembers
+ - TypeOfFileMismatch
+ - WrongRemoteFileIdSpecified
+ - PaymentProviderInvalid
+ - CurrencyTotalAmountInvalid
+ - CantParseUrl
+ - UnsupportedUrlProtocol
+ - CantParseEntities
+ - ResultIdDuplicate
+ - ConflictError
+ - TerminatedByOtherGetUpdates
+ - CantGetUpdates
+ - Unauthorized
+ - BotKicked
+ - BotBlocked
+ - UserDeactivated
+ - CantInitiateConversation
+ - CantTalkWithBots
+ - NetworkError
+ - RetryAfter
+ - MigrateToChat
+ - RestartingTelegram
-
-TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
-TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
-
-AIOGramWarning
- TimeoutWarning
+- AIOGramWarning
+ - TimeoutWarning
"""
import time
# TODO: Use exceptions detector from `aiograph`.
+# TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
+# TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: ']
@@ -490,7 +488,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
class BotKicked(Unauthorized):
- match = 'Bot was kicked from a chat'
+ match = 'bot was kicked from a chat'
class BotBlocked(Unauthorized):
diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py
index 65594371..33f80684 100644
--- a/aiogram/utils/executor.py
+++ b/aiogram/utils/executor.py
@@ -15,7 +15,7 @@ from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, Webhook
APP_EXECUTOR_KEY = 'APP_EXECUTOR'
-def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
+def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
if on_startup is not None:
executor.on_startup(on_startup)
if on_shutdown is not None:
@@ -23,7 +23,7 @@ def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
- on_startup=None, on_shutdown=None, timeout=20, fast=True):
+ on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@@ -38,7 +38,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
- executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)
+ executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
@@ -291,7 +291,7 @@ class Executor:
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
self.run_app(**kwargs)
- def start_polling(self, reset_webhook=None, timeout=20, fast=True):
+ def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@@ -303,7 +303,8 @@ class Executor:
try:
loop.run_until_complete(self._startup_polling())
- loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast))
+ loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
+ relax=relax, fast=fast))
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
# loop.stop()
@@ -339,7 +340,7 @@ class Executor:
async def _skip_updates(self):
await self.dispatcher.reset_webhook(True)
await self.dispatcher.skip_updates()
- log.warning(f"Updates are skipped successfully.")
+ log.warning(f'Updates were skipped successfully.')
async def _welcome(self):
user = await self.dispatcher.bot.me
diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py
index eeabca7c..443a2ffe 100644
--- a/aiogram/utils/helper.py
+++ b/aiogram/utils/helper.py
@@ -120,15 +120,15 @@ class HelperMode(Helper):
"""
if mode == cls.SCREAMING_SNAKE_CASE:
return cls._screaming_snake_case(text)
- elif mode == cls.snake_case:
+ if mode == cls.snake_case:
return cls._snake_case(text)
- elif mode == cls.lowercase:
+ if mode == cls.lowercase:
return cls._snake_case(text).replace('_', '')
- elif mode == cls.lowerCamelCase:
+ if mode == cls.lowerCamelCase:
return cls._camel_case(text)
- elif mode == cls.CamelCase:
+ if mode == cls.CamelCase:
return cls._camel_case(text, True)
- elif callable(mode):
+ if callable(mode):
return mode(text)
return text
diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py
index 776479bd..e6857263 100644
--- a/aiogram/utils/mixins.py
+++ b/aiogram/utils/mixins.py
@@ -31,7 +31,7 @@ T = TypeVar('T')
class ContextInstanceMixin:
def __init_subclass__(cls, **kwargs):
- cls.__context_instance = contextvars.ContextVar('instance_' + cls.__name__)
+ cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}')
return cls
@classmethod
@@ -43,5 +43,5 @@ class ContextInstanceMixin:
@classmethod
def set_current(cls: Type[T], value: T):
if not isinstance(value, cls):
- raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'")
+ raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}')
cls.__context_instance.set(value)
diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py
index 45643553..0c5e8ae9 100644
--- a/aiogram/utils/payload.py
+++ b/aiogram/utils/payload.py
@@ -52,14 +52,14 @@ def prepare_arg(value):
"""
if value is None:
return value
- elif isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
+ if isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
return json.dumps(_normalize(value))
- elif isinstance(value, datetime.timedelta):
+ if isinstance(value, datetime.timedelta):
now = datetime.datetime.now()
return int((now + value).timestamp())
- elif isinstance(value, datetime.datetime):
+ if isinstance(value, datetime.datetime):
return round(value.timestamp())
- elif isinstance(value, LazyProxy):
+ if isinstance(value, LazyProxy):
return str(value)
return value
diff --git a/dev_requirements.txt b/dev_requirements.txt
index 79adc949..06bc3e9c 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -15,3 +15,4 @@ sphinx-rtd-theme>=0.4.3
sphinxcontrib-programoutput>=0.14
aiohttp-socks>=0.2.2
rethinkdb>=2.4.1
+coverage==4.5.3
diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst
index d103ac36..059a4f06 100644
--- a/docs/source/dispatcher/filters.rst
+++ b/docs/source/dispatcher/filters.rst
@@ -111,6 +111,22 @@ ExceptionsFilter
:show-inheritance:
+IDFilter
+----------------
+
+.. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter
+ :members:
+ :show-inheritance:
+
+
+AdminFilter
+----------------
+
+.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter
+ :members:
+ :show-inheritance:
+
+
Making own filters (Custom filters)
===================================
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 89cdbf79..543c793a 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
- .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram
+ .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
diff --git a/docs/source/install.rst b/docs/source/install.rst
index de199af6..cd89dc54 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -7,6 +7,10 @@ Using PIP
$ pip install -U aiogram
+Using AUR
+---------
+*aiogram* is also available in Arch User Repository, so you can install this library on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram `_ package.
+
From sources
------------
.. code-block:: bash
diff --git a/docs/source/utils/auth_widget.rst b/docs/source/utils/auth_widget.rst
index e3a90ef6..95cb3913 100644
--- a/docs/source/utils/auth_widget.rst
+++ b/docs/source/utils/auth_widget.rst
@@ -1,4 +1,6 @@
===========
Auth Widget
===========
-Coming soon...
+
+.. automodule:: aiogram.utils.auth_widget
+ :members:
diff --git a/docs/source/utils/context.rst b/docs/source/utils/context.rst
deleted file mode 100644
index 7a930a7e..00000000
--- a/docs/source/utils/context.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-=======
-Context
-=======
-Coming soon...
diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst
index 0a7b4089..619224f8 100644
--- a/docs/source/utils/deprecated.rst
+++ b/docs/source/utils/deprecated.rst
@@ -1,4 +1,6 @@
==========
Deprecated
==========
-Coming soon...
+
+.. automodule:: aiogram.utils.deprecated
+ :members:
diff --git a/docs/source/utils/emoji.rst b/docs/source/utils/emoji.rst
index 27382dd6..1be210e3 100644
--- a/docs/source/utils/emoji.rst
+++ b/docs/source/utils/emoji.rst
@@ -1,4 +1,6 @@
=====
Emoji
=====
-Coming soon...
+
+.. automodule:: aiogram.utils.emoji
+ :members:
diff --git a/docs/source/utils/exceptions.rst b/docs/source/utils/exceptions.rst
index 199e67aa..b296afd3 100644
--- a/docs/source/utils/exceptions.rst
+++ b/docs/source/utils/exceptions.rst
@@ -1,4 +1,6 @@
==========
Exceptions
==========
-Coming soon...
+
+.. automodule:: aiogram.utils.exceptions
+ :members:
diff --git a/docs/source/utils/executor.rst b/docs/source/utils/executor.rst
index 2cb8eaa1..f88dd8c5 100644
--- a/docs/source/utils/executor.rst
+++ b/docs/source/utils/executor.rst
@@ -1,4 +1,7 @@
========
Executor
========
-Coming soon...
+
+.. automodule:: aiogram.utils.executor
+ :members:
+
diff --git a/docs/source/utils/helper.rst b/docs/source/utils/helper.rst
index 4ffc74ab..ba8bf016 100644
--- a/docs/source/utils/helper.rst
+++ b/docs/source/utils/helper.rst
@@ -1,4 +1,6 @@
======
Helper
======
-Coming soon...
+
+.. automodule:: aiogram.utils.helper
+ :members:
diff --git a/docs/source/utils/index.rst b/docs/source/utils/index.rst
index bc4a52ed..1ac3777c 100644
--- a/docs/source/utils/index.rst
+++ b/docs/source/utils/index.rst
@@ -3,14 +3,13 @@ Utils
.. toctree::
+ auth_widget
executor
exceptions
- context
markdown
helper
- auth_widget
+ deprecated
payload
parts
json
emoji
- deprecated
diff --git a/docs/source/utils/json.rst b/docs/source/utils/json.rst
index 84833031..68577ff4 100644
--- a/docs/source/utils/json.rst
+++ b/docs/source/utils/json.rst
@@ -1,4 +1,6 @@
====
JSON
====
-Coming soon...
+
+.. automodule:: aiogram.utils.json
+ :members:
diff --git a/docs/source/utils/markdown.rst b/docs/source/utils/markdown.rst
index ee32dfd4..bcbe0497 100644
--- a/docs/source/utils/markdown.rst
+++ b/docs/source/utils/markdown.rst
@@ -1,4 +1,6 @@
========
Markdown
========
-Coming soon...
+
+.. automodule:: aiogram.utils.markdown
+ :members:
diff --git a/docs/source/utils/parts.rst b/docs/source/utils/parts.rst
index 845d017e..fd2e91de 100644
--- a/docs/source/utils/parts.rst
+++ b/docs/source/utils/parts.rst
@@ -1,4 +1,6 @@
=====
Parts
=====
-Coming soon...
+
+.. automodule:: aiogram.utils.parts
+ :members:
diff --git a/docs/source/utils/payload.rst b/docs/source/utils/payload.rst
index b3427906..e3e0331a 100644
--- a/docs/source/utils/payload.rst
+++ b/docs/source/utils/payload.rst
@@ -1,4 +1,6 @@
=======
Payload
=======
-Coming soon...
+
+.. automodule:: aiogram.utils.payload
+ :members:
diff --git a/examples/admin_filter_example.py b/examples/admin_filter_example.py
new file mode 100644
index 00000000..ec8746bb
--- /dev/null
+++ b/examples/admin_filter_example.py
@@ -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)
diff --git a/examples/broadcast_example.py b/examples/broadcast_example.py
index 9e654d44..2891bb30 100644
--- a/examples/broadcast_example.py
+++ b/examples/broadcast_example.py
@@ -9,9 +9,8 @@ API_TOKEN = 'BOT TOKEN HERE'
logging.basicConfig(level=logging.INFO)
log = logging.getLogger('broadcast')
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
-dp = Dispatcher(bot, loop=loop)
+bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
+dp = Dispatcher(bot)
def get_users():
diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py
index 3dd7d35e..9a8affe9 100644
--- a/examples/callback_data_factory.py
+++ b/examples/callback_data_factory.py
@@ -1,4 +1,3 @@
-import asyncio
import logging
import random
import uuid
@@ -13,20 +12,20 @@ logging.basicConfig(level=logging.INFO)
API_TOKEN = 'BOT TOKEN HERE'
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
+
+bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
dp.middleware.setup(LoggingMiddleware())
POSTS = {
str(uuid.uuid4()): {
- 'title': f"Post {index}",
+ 'title': f'Post {index}',
'body': 'Lorem ipsum dolor sit amet, '
'consectetur adipiscing elit, '
'sed do eiusmod tempor incididunt ut '
'labore et dolore magna aliqua',
- 'votes': random.randint(-2, 5)
+ 'votes': random.randint(-2, 5),
} for index in range(1, 6)
}
@@ -42,21 +41,24 @@ def get_keyboard() -> types.InlineKeyboardMarkup:
markup.add(
types.InlineKeyboardButton(
post['title'],
- callback_data=posts_cb.new(id=post_id, action='view'))
+ callback_data=posts_cb.new(id=post_id, action='view')),
)
return markup
def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup):
- text = f"{md.hbold(post['title'])}\n" \
- f"{md.quote_html(post['body'])}\n" \
- f"\n" \
- f"Votes: {post['votes']}"
+ text = md.text(
+ md.hbold(post['title']),
+ md.quote_html(post['body']),
+ '', # just new empty line
+ f"Votes: {post['votes']}",
+ sep = '\n',
+ )
markup = types.InlineKeyboardMarkup()
markup.row(
types.InlineKeyboardButton('👍', callback_data=posts_cb.new(id=post_id, action='like')),
- types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='unlike')),
+ types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='dislike')),
)
markup.add(types.InlineKeyboardButton('<< Back', callback_data=posts_cb.new(id='-', action='list')))
return text, markup
@@ -84,7 +86,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict):
await query.message.edit_text(text, reply_markup=markup)
-@dp.callback_query_handler(posts_cb.filter(action=['like', 'unlike']))
+@dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike']))
async def query_post_vote(query: types.CallbackQuery, callback_data: dict):
try:
await dp.throttle('vote', rate=1)
@@ -100,10 +102,10 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict):
if action == 'like':
post['votes'] += 1
- elif action == 'unlike':
+ elif action == 'dislike':
post['votes'] -= 1
- await query.answer('Voted.')
+ await query.answer('Vote accepted')
text, markup = format_post(post_id, post)
await query.message.edit_text(text, reply_markup=markup)
@@ -114,4 +116,4 @@ async def message_not_modified_handler(update, error):
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py
new file mode 100644
index 00000000..5fc9c548
--- /dev/null
+++ b/examples/callback_data_factory_simple.py
@@ -0,0 +1,68 @@
+"""
+This is a simple example of usage of CallbackData factory
+For more comprehensive example see callback_data_factory.py
+"""
+
+import logging
+
+from aiogram import Bot, Dispatcher, executor, types
+from aiogram.contrib.middlewares.logging import LoggingMiddleware
+from aiogram.utils.callback_data import CallbackData
+from aiogram.utils.exceptions import MessageNotModified
+
+logging.basicConfig(level=logging.INFO)
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+
+bot = Bot(token=API_TOKEN)
+
+dp = Dispatcher(bot)
+dp.middleware.setup(LoggingMiddleware())
+
+vote_cb = CallbackData('vote', 'action') # vote:
+likes = {} # user_id: amount_of_likes
+
+
+def get_keyboard():
+ return types.InlineKeyboardMarkup().row(
+ types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')),
+ types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')),
+ )
+
+
+@dp.message_handler(commands=['start'])
+async def cmd_start(message: types.Message):
+ amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0
+ await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard())
+
+
+@dp.callback_query_handler(vote_cb.filter(action=['up', 'down']))
+async def callback_vote_action(query: types.CallbackQuery, callback_data: dict):
+ logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data
+ await query.answer() # don't forget to answer callback query as soon as possible
+ callback_data_action = callback_data['action']
+ likes_count = likes.get(query.from_user.id, 0)
+
+ if callback_data_action == 'up':
+ likes_count += 1
+ else:
+ likes_count -= 1
+
+ likes[query.from_user.id] = likes_count # update amount of likes in storage
+
+ await bot.edit_message_text(
+ f'You voted {callback_data_action}! Now you have {likes_count} vote[s].',
+ query.from_user.id,
+ query.message.message_id,
+ reply_markup=get_keyboard(),
+ )
+
+
+@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
+async def message_not_modified_handler(update, error):
+ return True
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/check_user_language.py b/examples/check_user_language.py
index bd0ba7f9..98bed8a6 100644
--- a/examples/check_user_language.py
+++ b/examples/check_user_language.py
@@ -2,7 +2,6 @@
Babel is required.
"""
-import asyncio
import logging
from aiogram import Bot, Dispatcher, executor, md, types
@@ -11,8 +10,8 @@ API_TOKEN = 'BOT TOKEN HERE'
logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.MARKDOWN)
+
+bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN)
dp = Dispatcher(bot)
@@ -22,12 +21,13 @@ async def check_language(message: types.Message):
await message.reply(md.text(
md.bold('Info about your language:'),
- md.text(' 🔸', md.bold('Code:'), md.italic(locale.locale)),
- md.text(' 🔸', md.bold('Territory:'), md.italic(locale.territory or 'Unknown')),
- md.text(' 🔸', md.bold('Language name:'), md.italic(locale.language_name)),
- md.text(' 🔸', md.bold('English language name:'), md.italic(locale.english_name)),
- sep='\n'))
+ md.text('🔸', md.bold('Code:'), md.code(locale.language)),
+ md.text('🔸', md.bold('Territory:'), md.code(locale.territory or 'Unknown')),
+ md.text('🔸', md.bold('Language name:'), md.code(locale.language_name)),
+ md.text('🔸', md.bold('English language name:'), md.code(locale.english_name)),
+ sep='\n',
+ ))
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/echo_bot.py b/examples/echo_bot.py
index 27dc70d9..00046f3a 100644
--- a/examples/echo_bot.py
+++ b/examples/echo_bot.py
@@ -20,7 +20,7 @@ dp = Dispatcher(bot)
@dp.message_handler(commands=['start', 'help'])
async def send_welcome(message: types.Message):
"""
- This handler will be called when client send `/start` or `/help` commands.
+ This handler will be called when user sends `/start` or `/help` command
"""
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
@@ -28,13 +28,25 @@ async def send_welcome(message: types.Message):
@dp.message_handler(regexp='(^cat[s]?$|puss)')
async def cats(message: types.Message):
with open('data/cats.jpg', 'rb') as photo:
- await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺',
- reply_to_message_id=message.message_id)
+ '''
+ # Old fashioned way:
+ await bot.send_photo(
+ message.chat.id,
+ photo,
+ caption='Cats are here 😺',
+ reply_to_message_id=message.message_id,
+ )
+ '''
+
+ await message.reply_photo(photo, caption='Cats are here 😺')
@dp.message_handler()
async def echo(message: types.Message):
- await bot.send_message(message.chat.id, message.text)
+ # old style:
+ # await bot.send_message(message.chat.id, message.text)
+
+ await message.reply(message.text, reply=False)
if __name__ == '__main__':
diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py
index 58b8053c..7c0536a7 100644
--- a/examples/finite_state_machine_example.py
+++ b/examples/finite_state_machine_example.py
@@ -1,19 +1,20 @@
-import asyncio
-from typing import Optional
+import logging
import aiogram.utils.markdown as md
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
+from aiogram.dispatcher.filters import Text
from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram.types import ParseMode
from aiogram.utils import executor
+logging.basicConfig(level=logging.INFO)
+
API_TOKEN = 'BOT TOKEN HERE'
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
+bot = Bot(token=API_TOKEN)
# For example use simple MemoryStorage for Dispatcher.
storage = MemoryStorage()
@@ -27,7 +28,7 @@ class Form(StatesGroup):
gender = State() # Will be represented in storage as 'Form:gender'
-@dp.message_handler(commands=['start'])
+@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
"""
Conversation's entry point
@@ -39,19 +40,21 @@ async def cmd_start(message: types.Message):
# You can use state '*' if you need to handle all states
-@dp.message_handler(state='*', commands=['cancel'])
-@dp.message_handler(lambda message: message.text.lower() == 'cancel', state='*')
-async def cancel_handler(message: types.Message, state: FSMContext, raw_state: Optional[str] = None):
+@dp.message_handler(state='*', commands='cancel')
+@dp.message_handler(Text(equals='cancel', ignore_case=True), state='*')
+async def cancel_handler(message: types.Message, state: FSMContext):
"""
Allow user to cancel any action
"""
- if raw_state is None:
+ current_state = await state.get_state()
+ if current_state is None:
return
+ logging.info('Cancelling state %r', current_state)
# Cancel state and inform user about it
await state.finish()
# And remove keyboard (just in case)
- await message.reply('Canceled.', reply_markup=types.ReplyKeyboardRemove())
+ await message.reply('Cancelled.', reply_markup=types.ReplyKeyboardRemove())
@dp.message_handler(state=Form.name)
@@ -68,7 +71,7 @@ async def process_name(message: types.Message, state: FSMContext):
# Check age. Age gotta be digit
@dp.message_handler(lambda message: not message.text.isdigit(), state=Form.age)
-async def failed_process_age(message: types.Message):
+async def process_age_invalid(message: types.Message):
"""
If age is invalid
"""
@@ -90,11 +93,11 @@ async def process_age(message: types.Message, state: FSMContext):
@dp.message_handler(lambda message: message.text not in ["Male", "Female", "Other"], state=Form.gender)
-async def failed_process_gender(message: types.Message):
+async def process_gender_invalid(message: types.Message):
"""
In this example gender has to be one of: Male, Female, Other.
"""
- return await message.reply("Bad gender name. Choose you gender from keyboard.")
+ return await message.reply("Bad gender name. Choose your gender from the keyboard.")
@dp.message_handler(state=Form.gender)
@@ -106,15 +109,21 @@ async def process_gender(message: types.Message, state: FSMContext):
markup = types.ReplyKeyboardRemove()
# And send message
- await bot.send_message(message.chat.id, md.text(
- md.text('Hi! Nice to meet you,', md.bold(data['name'])),
- md.text('Age:', data['age']),
- md.text('Gender:', data['gender']),
- sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN)
+ await bot.send_message(
+ message.chat.id,
+ md.text(
+ md.text('Hi! Nice to meet you,', md.bold(data['name'])),
+ md.text('Age:', md.code(data['age'])),
+ md.text('Gender:', data['gender']),
+ sep='\n',
+ ),
+ reply_markup=markup,
+ parse_mode=ParseMode.MARKDOWN,
+ )
- # Finish conversation
- data.state = None
+ # Finish conversation
+ await state.finish()
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/i18n_example.py b/examples/i18n_example.py
index 6469ed5b..3bb624bd 100644
--- a/examples/i18n_example.py
+++ b/examples/i18n_example.py
@@ -3,6 +3,19 @@ Internalize your bot
Step 1: extract texts
# pybabel extract i18n_example.py -o locales/mybot.pot
+
+ Some useful options:
+ - Extract texts with pluralization support
+ # -k __:1,2
+ - Add comments for translators, you can use another tag if you want (TR)
+ # --add-comments=NOTE
+ - Disable comments with string location in code
+ # --no-location
+ - Set project name
+ # --project=MySuperBot
+ - Set version
+ # --version=2.2
+
Step 2: create *.po files. For e.g. create en, ru, uk locales.
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
Step 3: translate texts
@@ -24,7 +37,7 @@ from pathlib import Path
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.i18n import I18nMiddleware
-TOKEN = 'BOT TOKEN HERE'
+TOKEN = 'BOT_TOKEN_HERE'
I18N_DOMAIN = 'mybot'
BASE_DIR = Path(__file__).parent
@@ -41,16 +54,45 @@ dp.middleware.setup(i18n)
_ = i18n.gettext
-@dp.message_handler(commands=['start'])
+@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
# Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts.
await message.reply(_('Hello, {user}!').format(user=message.from_user.full_name))
-@dp.message_handler(commands=['lang'])
+@dp.message_handler(commands='lang')
async def cmd_lang(message: types.Message, locale):
+ # For setting custom lang you have to modify i18n middleware, like this:
+ # https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py
await message.reply(_('Your current language: {language}').format(language=locale))
+# If you care about pluralization, here's small handler
+# And also, there's and example of comments for translators. Most translation tools support them.
+
+# Alias for gettext method, parser will understand double underscore as plural (aka ngettext)
+__ = i18n.gettext
+
+
+# some likes manager
+LIKES_STORAGE = {'count': 0}
+
+
+def get_likes() -> int:
+ return LIKES_STORAGE['count']
+
+
+def increase_likes() -> int:
+ LIKES_STORAGE['count'] += 1
+ return get_likes()
+#
+
+
+@dp.message_handler(commands='like')
+async def cmd_like(message: types.Message, locale):
+ likes = increase_likes()
+
+ # NOTE: This is comment for a translator
+ await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes))
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)
diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py
new file mode 100644
index 00000000..343253e3
--- /dev/null
+++ b/examples/id_filter_example.py
@@ -0,0 +1,36 @@
+from aiogram import Bot, Dispatcher, executor, types
+from aiogram.dispatcher.handler import SkipHandler
+
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+user_id_required = None # TODO: Set id here
+chat_id_required = user_id_required # Change for use in groups (user_id == chat_id in pm)
+
+
+@dp.message_handler(user_id=user_id_required)
+async def handler1(msg: types.Message):
+ await bot.send_message(msg.chat.id, "Hello, checking with user_id=")
+ raise SkipHandler # just for demo
+
+
+@dp.message_handler(chat_id=chat_id_required)
+async def handler2(msg: types.Message):
+ await bot.send_message(msg.chat.id, "Hello, checking with chat_id=")
+ raise SkipHandler # just for demo
+
+
+@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required)
+async def handler3(msg: types.Message):
+ await msg.reply("Hello from user= & chat_id=", reply=False)
+
+
+@dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here
+async def handler4(msg: types.Message):
+ await msg.reply("Checked user_id with list!", reply=False)
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp)
diff --git a/examples/inline_bot.py b/examples/inline_bot.py
index 4a771210..28f83e43 100644
--- a/examples/inline_bot.py
+++ b/examples/inline_bot.py
@@ -1,24 +1,37 @@
-import asyncio
+import hashlib
import logging
-from aiogram import Bot, types, Dispatcher, executor
+from aiogram import Bot, Dispatcher, executor
+from aiogram.types import InlineQuery, \
+ InputTextMessageContent, InlineQueryResultArticle
-API_TOKEN = 'BOT TOKEN HERE'
+API_TOKEN = 'BOT_TOKEN_HERE'
logging.basicConfig(level=logging.DEBUG)
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
+bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.inline_handler()
-async def inline_echo(inline_query: types.InlineQuery):
- input_content = types.InputTextMessageContent(inline_query.query or 'echo')
- item = types.InlineQueryResultArticle(id='1', title='echo',
- input_message_content=input_content)
+async def inline_echo(inline_query: InlineQuery):
+ # id affects both preview and content,
+ # so it has to be unique for each result
+ # (Unique identifier for this result, 1-64 Bytes)
+ # you can set your unique id's
+ # but for example i'll generate it based on text because I know, that
+ # only text will be passed in this example
+ text = inline_query.query or 'echo'
+ input_content = InputTextMessageContent(text)
+ result_id: str = hashlib.md5(text.encode()).hexdigest()
+ item = InlineQueryResultArticle(
+ id=result_id,
+ title=f'Result {text!r}',
+ input_message_content=input_content,
+ )
+ # don't forget to set cache_time=1 for testing (default is 300s or 5m)
await bot.answer_inline_query(inline_query.id, results=[item], cache_time=1)
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py
new file mode 100644
index 00000000..8b950f98
--- /dev/null
+++ b/examples/inline_keyboard_example.py
@@ -0,0 +1,62 @@
+"""
+This bot is created for the demonstration of a usage of inline keyboards.
+"""
+
+import logging
+
+from aiogram import Bot, Dispatcher, executor, types
+
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+
+# Initialize bot and dispatcher
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+
+@dp.message_handler(commands='start')
+async def start_cmd_handler(message: types.Message):
+ keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
+ # default row_width is 3, so here we can omit it actually
+ # kept for clearness
+
+ text_and_data = (
+ ('Yes!', 'yes'),
+ ('No!', 'no'),
+ )
+ # in real life for the callback_data the callback data factory should be used
+ # here the raw string is used for the simplicity
+ row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data)
+
+ keyboard_markup.row(*row_btns)
+ keyboard_markup.add(
+ # url buttons have no callback data
+ types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'),
+ )
+
+ await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
+
+
+# Use multiple registrators. Handler will execute when one of the filters is OK
+@dp.callback_query_handler(text='no') # if cb.data == 'no'
+@dp.callback_query_handler(text='yes') # if cb.data == 'yes'
+async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
+ answer_data = query.data
+ # always answer callback queries, even if you have nothing to say
+ await query.answer(f'You answered with {answer_data!r}')
+
+ if answer_data == 'yes':
+ text = 'Great, me too!'
+ elif answer_data == 'no':
+ text = 'Oh no...Why so?'
+ else:
+ text = f'Unexpected callback data {answer_data!r}!'
+
+ await bot.send_message(query.from_user.id, text)
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot
index 988ed463..62b2d425 100644
--- a/examples/locales/mybot.pot
+++ b/examples/locales/mybot.pot
@@ -1,27 +1,26 @@
# Translations template for PROJECT.
-# Copyright (C) 2018 ORGANIZATION
+# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
-# FIRST AUTHOR , 2018.
+# FIRST AUTHOR , 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2018-06-30 03:50+0300\n"
+"POT-Creation-Date: 2019-08-10 17:51+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.6.0\n"
+"Generated-By: Babel 2.7.0\n"
-#: i18n_example.py:48
+#: i18n_example.py:60
msgid "Hello, {user}!"
msgstr ""
-#: i18n_example.py:53
+#: i18n_example.py:67
msgid "Your current language: {language}"
msgstr ""
-
diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po
index 73876f30..9064bc0e 100644
--- a/examples/locales/ru/LC_MESSAGES/mybot.po
+++ b/examples/locales/ru/LC_MESSAGES/mybot.po
@@ -1,14 +1,14 @@
# Russian translations for PROJECT.
-# Copyright (C) 2018 ORGANIZATION
+# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
-# FIRST AUTHOR , 2018.
+# FIRST AUTHOR , 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2018-06-30 03:50+0300\n"
-"PO-Revision-Date: 2018-06-30 03:43+0300\n"
+"POT-Creation-Date: 2019-08-10 17:51+0300\n"
+"PO-Revision-Date: 2019-08-10 17:52+0300\n"
"Last-Translator: FULL NAME \n"
"Language: ru\n"
"Language-Team: ru \n"
@@ -17,13 +17,19 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.6.0\n"
+"Generated-By: Babel 2.7.0\n"
-#: i18n_example.py:48
+#: i18n_example.py:60
msgid "Hello, {user}!"
msgstr "Привет, {user}!"
-#: i18n_example.py:53
+#: i18n_example.py:67
msgid "Your current language: {language}"
msgstr "Твой язык: {language}"
+#: i18n_example.py:95
+msgid "Aiogram has {number} like!"
+msgid_plural "Aiogram has {number} likes!"
+msgstr[0] "Aiogram имеет {number} лайк!"
+msgstr[1] "Aiogram имеет {number} лайка!"
+msgstr[2] "Aiogram имеет {number} лайков!"
diff --git a/examples/media_group.py b/examples/media_group.py
index b1f5246a..3d488364 100644
--- a/examples/media_group.py
+++ b/examples/media_group.py
@@ -2,10 +2,10 @@ import asyncio
from aiogram import Bot, Dispatcher, executor, filters, types
-API_TOKEN = 'BOT TOKEN HERE'
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@@ -14,10 +14,10 @@ async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Do you want to see many pussies? Are you ready?")
- # And wait few seconds...
+ # Wait a little...
await asyncio.sleep(1)
- # Good bots should send chat actions. Or not.
+ # Good bots should send chat actions...
await types.ChatActions.upload_photo()
# Create media group
@@ -40,4 +40,4 @@ async def send_welcome(message: types.Message):
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/middleware_and_antiflood.py b/examples/middleware_and_antiflood.py
index c579aecc..72fabf55 100644
--- a/examples/middleware_and_antiflood.py
+++ b/examples/middleware_and_antiflood.py
@@ -7,14 +7,12 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
-TOKEN = 'BOT TOKEN HERE'
-
-loop = asyncio.get_event_loop()
+TOKEN = 'BOT_TOKEN_HERE'
# In this example Redis storage is used
storage = RedisStorage2(db=5)
-bot = Bot(token=TOKEN, loop=loop)
+bot = Bot(token=TOKEN)
dp = Dispatcher(bot, storage=storage)
@@ -119,4 +117,4 @@ if __name__ == '__main__':
dp.middleware.setup(ThrottlingMiddleware())
# Start long-polling
- executor.start_polling(dp, loop=loop)
+ executor.start_polling(dp)
diff --git a/examples/payments.py b/examples/payments.py
index e8e37011..42162578 100644
--- a/examples/payments.py
+++ b/examples/payments.py
@@ -1,28 +1,26 @@
-import asyncio
-
from aiogram import Bot
from aiogram import types
from aiogram.dispatcher import Dispatcher
from aiogram.types.message import ContentTypes
from aiogram.utils import executor
-BOT_TOKEN = 'BOT TOKEN HERE'
-PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef'
-loop = asyncio.get_event_loop()
+BOT_TOKEN = 'BOT_TOKEN_HERE'
+PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1422'
+
bot = Bot(BOT_TOKEN)
-dp = Dispatcher(bot, loop=loop)
+dp = Dispatcher(bot)
# Setup prices
prices = [
types.LabeledPrice(label='Working Time Machine', amount=5750),
- types.LabeledPrice(label='Gift wrapping', amount=500)
+ types.LabeledPrice(label='Gift wrapping', amount=500),
]
# Setup shipping options
shipping_options = [
types.ShippingOption(id='instant', title='WorldWide Teleporter').add(types.LabeledPrice('Teleporter', 1000)),
- types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300))
+ types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)),
]
@@ -60,7 +58,7 @@ async def cmd_buy(message: types.Message):
' Order our Working Time Machine today!',
provider_token=PAYMENTS_PROVIDER_TOKEN,
currency='usd',
- photo_url='https://images.fineartamerica.com/images-medium-large/2-the-time-machine-dmitriy-khristenko.jpg',
+ photo_url='https://telegra.ph/file/d08ff863531f10bf2ea4b.jpg',
photo_height=512, # !=0/None or picture won't be shown
photo_width=512,
photo_size=512,
@@ -70,14 +68,14 @@ async def cmd_buy(message: types.Message):
payload='HAPPY FRIDAYS COUPON')
-@dp.shipping_query_handler(func=lambda query: True)
+@dp.shipping_query_handler(lambda query: True)
async def shipping(shipping_query: types.ShippingQuery):
await bot.answer_shipping_query(shipping_query.id, ok=True, shipping_options=shipping_options,
error_message='Oh, seems like our Dog couriers are having a lunch right now.'
' Try again later!')
-@dp.pre_checkout_query_handler(func=lambda query: True)
+@dp.pre_checkout_query_handler(lambda query: True)
async def checkout(pre_checkout_query: types.PreCheckoutQuery):
await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True,
error_message="Aliens tried to steal your card's CVV,"
@@ -96,4 +94,4 @@ async def got_payment(message: types.Message):
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py
index 7e4452ee..5ef40608 100644
--- a/examples/proxy_and_emojize.py
+++ b/examples/proxy_and_emojize.py
@@ -1,4 +1,3 @@
-import asyncio
import logging
import aiohttp
@@ -11,13 +10,13 @@ from aiogram.utils.executor import start_polling
from aiogram.utils.markdown import bold, code, italic, text
# Configure bot here
-API_TOKEN = 'BOT TOKEN HERE'
-PROXY_URL = 'http://PROXY_URL' # Or 'socks5://...'
+API_TOKEN = 'BOT_TOKEN_HERE'
+PROXY_URL = 'http://PROXY_URL' # Or 'socks5://host:port'
-# If authentication is required in your proxy then uncomment next line and change login/password for it
+# NOTE: If authentication is required in your proxy then uncomment next line and change login/password for it
# PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password')
-# And add `proxy_auth=PROXY_AUTH` argument in line 25, like this:
-# >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
+# And add `proxy_auth=PROXY_AUTH` argument in line 30, like this:
+# >>> bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
# Also you can use Socks5 proxy but you need manually install aiohttp_socks package.
# Get my ip URL
@@ -25,28 +24,33 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/'
logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL)
+bot = Bot(token=API_TOKEN, proxy=PROXY_URL)
+
+# If auth is required:
+# bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
dp = Dispatcher(bot)
-async def fetch(url, proxy=None, proxy_auth=None):
- async with aiohttp.ClientSession() as session:
- async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response:
- return await response.text()
+async def fetch(url, session):
+ async with session.get(url) as response:
+ return await response.text()
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
+ # fetching urls will take some time, so notify user that everything is OK
+ await types.ChatActions.typing()
+
content = []
# Make request (without proxy)
- ip = await fetch(GET_IP_URL)
+ async with aiohttp.ClientSession() as session:
+ ip = await fetch(GET_IP_URL, session)
content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip)))
# This line is formatted to '🌎 *IP:* `YOUR IP`'
- # Make request through proxy
- ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth)
+ # Make request through bot's proxy
+ ip = await fetch(GET_IP_URL, bot.session)
content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy')))
# This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_'
@@ -62,4 +66,4 @@ async def cmd_start(message: types.Message):
if __name__ == '__main__':
- start_polling(dp, loop=loop, skip_updates=True)
+ start_polling(dp, skip_updates=True)
diff --git a/examples/regexp_commands_filter_example.py b/examples/regexp_commands_filter_example.py
index 86ccba55..05de9dd8 100644
--- a/examples/regexp_commands_filter_example.py
+++ b/examples/regexp_commands_filter_example.py
@@ -2,14 +2,28 @@ from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher, filters
from aiogram.utils import executor
-bot = Bot(token='TOKEN')
+
+bot = Bot(token='BOT_TOKEN_HERE', parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot)
@dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)']))
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 {regexp_command.group(1)}")
+
+
+@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 /start item_123'
+ )
+ await message.reply(text, disable_web_page_preview=True)
if __name__ == '__main__':
- executor.start_polling(dp)
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/regular_keyboard_example.py b/examples/regular_keyboard_example.py
new file mode 100644
index 00000000..d111053c
--- /dev/null
+++ b/examples/regular_keyboard_example.py
@@ -0,0 +1,67 @@
+"""
+This bot is created for the demonstration of a usage of regular keyboards.
+"""
+
+import logging
+
+from aiogram import Bot, Dispatcher, executor, types
+
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# Initialize bot and dispatcher
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+
+@dp.message_handler(commands='start')
+async def start_cmd_handler(message: types.Message):
+ keyboard_markup = types.ReplyKeyboardMarkup(row_width=3)
+ # default row_width is 3, so here we can omit it actually
+ # kept for clearness
+
+ btns_text = ('Yes!', 'No!')
+ keyboard_markup.row(*(types.KeyboardButton(text) for text in btns_text))
+ # adds buttons as a new row to the existing keyboard
+ # the behaviour doesn't depend on row_width attribute
+
+ more_btns_text = (
+ "I don't know",
+ "Who am i?",
+ "Where am i?",
+ "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 like aiogram?", reply_markup=keyboard_markup)
+
+
+@dp.message_handler()
+async def all_msg_handler(message: types.Message):
+ # pressing of a KeyboardButton is the same as sending the regular message with the same text
+ # so, to handle the responses from the keyboard, we need to use a message_handler
+ # in real bot, it's better to define message_handler(text="...") for each button
+ # but here for the simplicity only one handler is defined
+
+ button_text = message.text
+ logger.debug('The answer is %r', button_text) # print the text we've got
+
+ if button_text == 'Yes!':
+ reply_text = "That's great"
+ elif button_text == 'No!':
+ reply_text = "Oh no! Why?"
+ else:
+ 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
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py
new file mode 100644
index 00000000..60d631e3
--- /dev/null
+++ b/examples/text_filter_example.py
@@ -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)
diff --git a/examples/throtling_example.py b/examples/throtling_example.py
deleted file mode 100644
index b979a979..00000000
--- a/examples/throtling_example.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""
-Example for throttling manager.
-
-You can use that for flood controlling.
-"""
-
-import asyncio
-import logging
-
-from aiogram import Bot, types
-from aiogram.contrib.fsm_storage.memory import MemoryStorage
-from aiogram.dispatcher import Dispatcher
-from aiogram.utils.exceptions import Throttled
-from aiogram.utils.executor import start_polling
-
-API_TOKEN = 'BOT TOKEN HERE'
-
-logging.basicConfig(level=logging.INFO)
-
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
-
-# Throttling manager does not work without Leaky Bucket.
-# Then need to use storages. For example use simple in-memory storage.
-storage = MemoryStorage()
-dp = Dispatcher(bot, storage=storage)
-
-
-@dp.message_handler(commands=['start', 'help'])
-async def send_welcome(message: types.Message):
- try:
- # Execute throttling manager with rate-limit equal to 2 seconds for key "start"
- await dp.throttle('start', rate=2)
- except Throttled:
- # If request is throttled, the `Throttled` exception will be raised
- await message.reply('Too many requests!')
- else:
- # Otherwise do something
- await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
-
-
-if __name__ == '__main__':
- start_polling(dp, loop=loop, skip_updates=True)
diff --git a/examples/throttling_example.py b/examples/throttling_example.py
new file mode 100644
index 00000000..f9ad1c67
--- /dev/null
+++ b/examples/throttling_example.py
@@ -0,0 +1,71 @@
+"""
+Example for throttling manager.
+
+You can use that for flood controlling.
+"""
+
+import logging
+
+from aiogram import Bot, types
+from aiogram.contrib.fsm_storage.memory import MemoryStorage
+from aiogram.dispatcher import Dispatcher
+from aiogram.utils.exceptions import Throttled
+from aiogram.utils.executor import start_polling
+
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+logging.basicConfig(level=logging.INFO)
+
+bot = Bot(token=API_TOKEN)
+
+# Throttling manager does not work without Leaky Bucket.
+# You need to use a storage. For example use simple in-memory storage.
+storage = MemoryStorage()
+dp = Dispatcher(bot, storage=storage)
+
+
+@dp.message_handler(commands=['start'])
+async def send_welcome(message: types.Message):
+ try:
+ # Execute throttling manager with rate-limit equal to 2 seconds for key "start"
+ await dp.throttle('start', rate=2)
+ except Throttled:
+ # If request is throttled, the `Throttled` exception will be raised
+ await message.reply('Too many requests!')
+ else:
+ # Otherwise do something
+ await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
+
+
+@dp.message_handler(commands=['hi'])
+@dp.throttled(lambda msg, loop, *args, **kwargs: loop.create_task(bot.send_message(msg.from_user.id, "Throttled")),
+ rate=5)
+# loop is added to the function to run coroutines from it
+async def say_hi(message: types.Message):
+ await message.answer("Hi")
+
+
+# the on_throttled object can be either a regular function or coroutine
+async def hello_throttled(*args, **kwargs):
+ # args will be the same as in the original handler
+ # kwargs will be updated with parameters given to .throttled (rate, key, user_id, chat_id)
+ print(f"hello_throttled was called with args={args} and kwargs={kwargs}")
+ message = args[0] # as message was the first argument in the original handler
+ await message.answer("Throttled")
+
+
+@dp.message_handler(commands=['hello'])
+@dp.throttled(hello_throttled, rate=4)
+async def say_hello(message: types.Message):
+ await message.answer("Hello!")
+
+
+@dp.message_handler(commands=['help'])
+@dp.throttled(rate=5)
+# nothing will happen if the handler will be throttled
+async def help_handler(message: types.Message):
+ await message.answer('Help!')
+
+if __name__ == '__main__':
+ start_polling(dp, skip_updates=True)
diff --git a/examples/webhook_example.py b/examples/webhook_example.py
index 86520988..6efa8767 100644
--- a/examples/webhook_example.py
+++ b/examples/webhook_example.py
@@ -1,177 +1,66 @@
-"""
-Example outdated
-"""
+import logging
-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.contrib.middlewares.logging import LoggingMiddleware
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
+from aiogram.dispatcher.webhook import SendMessage
+from aiogram.utils.executor import start_webhook
-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
+API_TOKEN = 'BOT_TOKEN_HERE'
-# 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 settings
+WEBHOOK_HOST = 'https://your.domain'
+WEBHOOK_PATH = '/path/to/api'
+WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
-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'
+# webserver settings
+WEBAPP_HOST = 'localhost' # or ip
WEBAPP_PORT = 3001
-BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO
+logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-bot = Bot(TOKEN, loop=loop)
-storage = MemoryStorage()
-dp = Dispatcher(bot, storage=storage)
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+dp.middleware.setup(LoggingMiddleware())
-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)
+@dp.message_handler()
+async def echo(message: types.Message):
+ # Regular request
+ # await bot.send_message(message.chat.id, message.text)
+
+ # or reply INTO webhook
+ return SendMessage(message.chat.id, message.text)
-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 on_startup(dp):
+ await bot.set_webhook(WEBHOOK_URL)
+ # insert code here to run it after start
-async def cancel(message: types.Message):
- # Get current state context
- state = dp.current_state(chat=message.chat.id, user=message.from_user.id)
+async def on_shutdown(dp):
+ logging.warning('Shutting down..')
- # 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
+ # insert code here to run it before shutdown
-
-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.
+ # Remove webhook (not acceptable in some cases)
await bot.delete_webhook()
- # Close Redis connection.
+ # Close DB connection (if used)
await dp.storage.close()
await dp.storage.wait_closed()
+ logging.warning('Bye!')
+
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.
+ 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,
+ )
diff --git a/examples/webhook_example_2.py b/examples/webhook_example_2.py
deleted file mode 100644
index 75b29c75..00000000
--- a/examples/webhook_example_2.py
+++ /dev/null
@@ -1,43 +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)
-
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
-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)
diff --git a/examples/webhook_example_old.py b/examples/webhook_example_old.py
new file mode 100644
index 00000000..0f6ae3cd
--- /dev/null
+++ b/examples/webhook_example_old.py
@@ -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.
diff --git a/tests/test_bot.py b/tests/test_bot.py
index 448f8dda..3e48ea57 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -333,9 +333,15 @@ async def test_restrict_chat_member(bot: Bot, event_loop):
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
- result = await bot.restrict_chat_member(chat_id=chat.id, user_id=user.id, can_add_web_page_previews=False,
- can_send_media_messages=False, can_send_messages=False,
- can_send_other_messages=False, until_date=123)
+ result = await bot.restrict_chat_member(
+ chat_id=chat.id,
+ user_id=user.id,
+ permissions=types.ChatPermissions(
+ can_add_web_page_previews=False,
+ can_send_media_messages=False,
+ can_send_messages=False,
+ can_send_other_messages=False
+ ), until_date=123)
assert isinstance(result, bool)
assert result is True
diff --git a/tests/test_bot/test_api.py b/tests/test_bot/test_api.py
new file mode 100644
index 00000000..c5193bcc
--- /dev/null
+++ b/tests/test_bot/test_api.py
@@ -0,0 +1,18 @@
+import pytest
+from aiogram.bot.api import check_token
+
+from aiogram.utils.exceptions import ValidationError
+
+
+VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
+INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length
+
+
+class Test_check_token:
+
+ def test_valid(self):
+ assert check_token(VALID_TOKEN) is True
+
+ def test_invalid_token(self):
+ with pytest.raises(ValidationError):
+ check_token(INVALID_TOKEN)
diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py
new file mode 100644
index 00000000..86344cec
--- /dev/null
+++ b/tests/test_dispatcher/test_filters/test_builtin.py
@@ -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}
diff --git a/tests/test_dispatcher/test_filters/test_state.py b/tests/test_dispatcher/test_filters/test_state.py
new file mode 100644
index 00000000..b7f5a5fd
--- /dev/null
+++ b/tests/test_dispatcher/test_filters/test_state.py
@@ -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)
diff --git a/tests/test_filters.py b/tests/test_filters.py
new file mode 100644
index 00000000..609db736
--- /dev/null
+++ b/tests/test_filters.py
@@ -0,0 +1,263 @@
+import pytest
+
+from aiogram.dispatcher.filters import Text
+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:
+
+ 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.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_prefix, test_text", data_sample_1())
+ async def test_startswith(self, test_prefix, test_text, ignore_case):
+ test_filter = Text(startswith=test_prefix, ignore_case=ignore_case)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ if ignore_case:
+ _test_prefix = test_prefix.lower()
+ _test_text = test_text.lower()
+ else:
+ _test_prefix = test_prefix
+ _test_text = test_text
+
+ return result is _test_text.startswith(_test_prefix)
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_prefix_list, test_text", [
+ (['not_example', ''], ''),
+ (['', 'not_example'], 'exAmple_string'),
+
+ (['not_example', 'example_string'], 'example_string'),
+ (['example_string', 'not_example'], 'exAmple_string'),
+ (['not_example', 'exAmple_string'], 'example_string'),
+
+ (['not_example', 'example_string'], 'example_string_dsf'),
+ (['example_string', 'not_example'], 'example_striNG_dsf'),
+ (['not_example', 'example_striNG'], 'example_string_dsf'),
+
+ (['not_example', 'example_string'], 'not_example_string'),
+ (['example_string', 'not_example'], 'not_eXample_string'),
+ (['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)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ 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
+
+ return result is any(map(_test_text.startswith, _test_prefix_list))
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_postfix, test_text", data_sample_1())
+ async def test_endswith(self, test_postfix, test_text, ignore_case):
+ test_filter = Text(endswith=test_postfix, ignore_case=ignore_case)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ if ignore_case:
+ _test_postfix = test_postfix.lower()
+ _test_text = test_text.lower()
+ else:
+ _test_postfix = test_postfix
+ _test_text = test_text
+
+ return result is _test_text.endswith(_test_postfix)
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_postfix_list, test_text", [
+ (['', 'not_example'], ''),
+ (['not_example', ''], 'exAmple_string'),
+
+ (['example_string', 'not_example'], 'example_string'),
+ (['not_example', 'example_string'], 'exAmple_string'),
+ (['exAmple_string', 'not_example'], 'example_string'),
+
+ (['not_example', 'example_string'], 'example_string_dsf'),
+ (['example_string', 'not_example'], 'example_striNG_dsf'),
+ (['not_example', 'example_striNG'], 'example_string_dsf'),
+
+ (['not_example', 'example_string'], 'not_example_string'),
+ (['example_string', 'not_example'], 'not_eXample_string'),
+ (['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)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ 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
+
+ return result is any(map(_test_text.endswith, _test_postfix_list))
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_string, test_text", [
+ ('', ''),
+ ('', '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):
+ test_filter = Text(contains=test_string, ignore_case=ignore_case)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ if ignore_case:
+ _test_string = test_string.lower()
+ _test_text = test_text.lower()
+ else:
+ _test_string = test_string
+ _test_text = test_text
+
+ return result is (_test_string in _test_text)
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_filter_list, test_text", [
+ (['a', 'ab', 'abc'], 'A'),
+ (['a', 'ab', 'abc'], 'ab'),
+ (['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)
+
+ 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
+
+ return result is all(map(_test_text.__contains__, _test_filter_list))
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_filter_text, test_text", [
+ ('', ''),
+ ('', '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):
+ test_filter = Text(equals=test_filter_text, ignore_case=ignore_case)
+
+ async def check(obj):
+ result = await test_filter.check(obj)
+ if ignore_case:
+ _test_filter_text = test_filter_text.lower()
+ _test_text = test_text.lower()
+ else:
+ _test_filter_text = test_filter_text
+ _test_text = test_text
+ return result is (_test_text == _test_filter_text)
+
+ await self._run_check(check, test_text)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize('ignore_case', (True, False))
+ @pytest.mark.parametrize("test_filter_list, test_text", [
+ (['new_string', ''], ''),
+ (['', '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))
diff --git a/tests/states_group.py b/tests/test_states_group.py
similarity index 100%
rename from tests/states_group.py
rename to tests/test_states_group.py
diff --git a/tests/test_token.py b/tests/test_token.py
deleted file mode 100644
index b8a6087f..00000000
--- a/tests/test_token.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import pytest
-
-from aiogram.bot import api
-from aiogram.utils import auth_widget, exceptions
-
-VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
-INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length
-
-VALID_DATA = {
- 'date': 1525385236,
- 'first_name': 'Test',
- 'last_name': 'User',
- 'id': 123456789,
- 'username': 'username',
- 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b'
-}
-INVALID_DATA = {
- 'date': 1525385237,
- 'first_name': 'Test',
- 'last_name': 'User',
- 'id': 123456789,
- 'username': 'username',
- 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b'
-}
-
-
-def test_valid_token():
- assert api.check_token(VALID_TOKEN)
-
-
-def test_invalid_token():
- with pytest.raises(exceptions.ValidationError):
- api.check_token(INVALID_TOKEN)
-
-
-def test_widget():
- assert auth_widget.check_token(VALID_DATA, VALID_TOKEN)
-
-
-def test_invalid_widget_data():
- assert not auth_widget.check_token(INVALID_DATA, VALID_TOKEN)
diff --git a/tests/test_utils/test_auth_widget.py b/tests/test_utils/test_auth_widget.py
new file mode 100644
index 00000000..8c6f5941
--- /dev/null
+++ b/tests/test_utils/test_auth_widget.py
@@ -0,0 +1,46 @@
+import pytest
+
+from aiogram.utils.auth_widget import check_integrity, \
+ generate_hash, check_token
+
+TOKEN = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
+
+
+@pytest.fixture
+def data():
+ return {
+ 'id': '42',
+ 'first_name': 'John',
+ 'last_name': 'Smith',
+ 'username': 'username',
+ 'photo_url': 'https://t.me/i/userpic/320/picname.jpg',
+ 'auth_date': '1565810688',
+ 'hash': 'c303db2b5a06fe41d23a9b14f7c545cfc11dcc7473c07c9c5034ae60062461ce',
+ }
+
+
+def test_generate_hash(data):
+ res = generate_hash(data, TOKEN)
+ assert res == data['hash']
+
+
+class Test_check_token:
+ """
+ This case gonna be deleted
+ """
+ def test_ok(self, data):
+ assert check_token(data, TOKEN) is True
+
+ def test_fail(self, data):
+ data.pop('username')
+ assert check_token(data, TOKEN) is False
+
+
+class Test_check_integrity:
+
+ def test_ok(self, data):
+ assert check_integrity(TOKEN, data) is True
+
+ def test_fail(self, data):
+ data.pop('username')
+ assert check_integrity(TOKEN, data) is False
diff --git a/tests/types/dataset.py b/tests/types/dataset.py
index 4167eae1..18bcbdad 100644
--- a/tests/types/dataset.py
+++ b/tests/types/dataset.py
@@ -8,7 +8,7 @@ USER = {
"first_name": "FirstName",
"last_name": "LastName",
"username": "username",
- "language_code": "ru"
+ "language_code": "ru",
}
CHAT = {
@@ -16,14 +16,14 @@ CHAT = {
"first_name": "FirstName",
"last_name": "LastName",
"username": "username",
- "type": "private"
+ "type": "private",
}
PHOTO = {
"file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg",
"file_size": 1101,
"width": 90,
- "height": 51
+ "height": 51,
}
AUDIO = {
@@ -32,7 +32,7 @@ AUDIO = {
"title": "The Best Song",
"performer": "The Best Singer",
"file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI",
- "file_size": 9507774
+ "file_size": 9507774,
}
CHAT_MEMBER = {
@@ -44,7 +44,7 @@ CHAT_MEMBER = {
"can_invite_users": True,
"can_restrict_members": True,
"can_pin_messages": True,
- "can_promote_members": False
+ "can_promote_members": False,
}
CONTACT = {
@@ -57,7 +57,7 @@ DOCUMENT = {
"file_name": "test.docx",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg",
- "file_size": 21331
+ "file_size": 21331,
}
ANIMATION = {
@@ -65,51 +65,51 @@ ANIMATION = {
"mime_type": "video/mp4",
"thumb": PHOTO,
"file_id": "CgADBAAD4DUAAoceZAe2WiE9y0crrAI",
- "file_size": 65837
+ "file_size": 65837,
}
ENTITY_BOLD = {
"offset": 5,
"length": 2,
- "type": "bold"
+ "type": "bold",
}
ENTITY_ITALIC = {
"offset": 8,
"length": 1,
- "type": "italic"
+ "type": "italic",
}
ENTITY_LINK = {
"offset": 10,
"length": 6,
"type": "text_link",
- "url": "http://google.com/"
+ "url": "http://google.com/",
}
ENTITY_CODE = {
"offset": 17,
"length": 7,
- "type": "code"
+ "type": "code",
}
ENTITY_PRE = {
"offset": 30,
"length": 4,
- "type": "pre"
+ "type": "pre",
}
ENTITY_MENTION = {
"offset": 47,
"length": 9,
- "type": "mention"
+ "type": "mention",
}
GAME = {
"title": "Karate Kido",
"description": "No trees were harmed in the making of this game :)",
"photo": [PHOTO, PHOTO, PHOTO],
- "animation": ANIMATION
+ "animation": ANIMATION,
}
INVOICE = {
@@ -120,19 +120,19 @@ INVOICE = {
"Order our Working Time Machine today!",
"start_parameter": "time-machine-example",
"currency": "USD",
- "total_amount": 6250
+ "total_amount": 6250,
}
LOCATION = {
"latitude": 50.693416,
- "longitude": 30.624605
+ "longitude": 30.624605,
}
VENUE = {
"location": LOCATION,
"title": "Venue Name",
"address": "Venue Address",
- "foursquare_id": "4e6f2cec483bad563d150f98"
+ "foursquare_id": "4e6f2cec483bad563d150f98",
}
SHIPPING_ADDRESS = {
@@ -141,7 +141,7 @@ SHIPPING_ADDRESS = {
"city": "DefaultCity",
"street_line1": "Central",
"street_line2": "Middle",
- "post_code": "424242"
+ "post_code": "424242",
}
STICKER = {
@@ -156,7 +156,7 @@ STICKER = {
"height": 128
},
"file_id": "AAbbCCddEEffGGhh1234567890",
- "file_size": 12345
+ "file_size": 12345,
}
SUCCESSFUL_PAYMENT = {
@@ -164,7 +164,7 @@ SUCCESSFUL_PAYMENT = {
"total_amount": 6250,
"invoice_payload": "HAPPY FRIDAYS COUPON",
"telegram_payment_charge_id": "_",
- "provider_payment_charge_id": "12345678901234_test"
+ "provider_payment_charge_id": "12345678901234_test",
}
VIDEO = {
@@ -174,7 +174,7 @@ VIDEO = {
"mime_type": "video/quicktime",
"thumb": PHOTO,
"file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg",
- "file_size": 10099782
+ "file_size": 10099782,
}
VIDEO_NOTE = {
@@ -182,14 +182,14 @@ VIDEO_NOTE = {
"length": 240,
"thumb": PHOTO,
"file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz",
- "file_size": 186562
+ "file_size": 186562,
}
VOICE = {
"duration": 1,
"mime_type": "audio/ogg",
"file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg",
- "file_size": 4321
+ "file_size": 4321,
}
CALLBACK_QUERY = {}
@@ -206,7 +206,7 @@ EDITED_MESSAGE = {
"chat": CHAT,
"date": 1508825372,
"edit_date": 1508825379,
- "text": "hi there (edited)"
+ "text": "hi there (edited)",
}
FORWARDED_MESSAGE = {
@@ -219,7 +219,7 @@ FORWARDED_MESSAGE = {
"forward_date": 1522749037,
"text": "Forwarded text with entities from public channel ",
"entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK,
- ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE]
+ ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE],
}
INLINE_QUERY = {}
@@ -229,7 +229,7 @@ MESSAGE = {
"from": USER,
"chat": CHAT,
"date": 1508709711,
- "text": "Hi, world!"
+ "text": "Hi, world!",
}
MESSAGE_WITH_AUDIO = {
@@ -238,7 +238,7 @@ MESSAGE_WITH_AUDIO = {
"chat": CHAT,
"date": 1508739776,
"audio": AUDIO,
- "caption": "This is my favourite song"
+ "caption": "This is my favourite song",
}
MESSAGE_WITH_AUTHOR_SIGNATURE = {}
@@ -250,7 +250,7 @@ MESSAGE_WITH_CONTACT = {
"from": USER,
"chat": CHAT,
"date": 1522850298,
- "contact": CONTACT
+ "contact": CONTACT,
}
MESSAGE_WITH_DELETE_CHAT_PHOTO = {}
@@ -261,7 +261,7 @@ MESSAGE_WITH_DOCUMENT = {
"chat": CHAT,
"date": 1508768012,
"document": DOCUMENT,
- "caption": "Read my document"
+ "caption": "Read my document",
}
MESSAGE_WITH_EDIT_DATE = {}
@@ -273,7 +273,7 @@ MESSAGE_WITH_GAME = {
"from": USER,
"chat": CHAT,
"date": 1508824810,
- "game": GAME
+ "game": GAME,
}
MESSAGE_WITH_GROUP_CHAT_CREATED = {}
@@ -283,7 +283,7 @@ MESSAGE_WITH_INVOICE = {
"from": USER,
"chat": CHAT,
"date": 1508761719,
- "invoice": INVOICE
+ "invoice": INVOICE,
}
MESSAGE_WITH_LEFT_CHAT_MEMBER = {}
@@ -293,7 +293,7 @@ MESSAGE_WITH_LOCATION = {
"from": USER,
"chat": CHAT,
"date": 1508755473,
- "location": LOCATION
+ "location": LOCATION,
}
MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {
@@ -301,7 +301,7 @@ MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {
"from": USER,
"chat": CHAT,
"date": 1526943253,
- "migrate_to_chat_id": -1234567890987
+ "migrate_to_chat_id": -1234567890987,
}
MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {
@@ -309,7 +309,7 @@ MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {
"from": USER,
"chat": CHAT,
"date": 1526943253,
- "migrate_from_chat_id": -123456789
+ "migrate_from_chat_id": -123456789,
}
MESSAGE_WITH_NEW_CHAT_MEMBERS = {}
@@ -324,7 +324,7 @@ MESSAGE_WITH_PHOTO = {
"chat": CHAT,
"date": 1508825154,
"photo": [PHOTO, PHOTO, PHOTO, PHOTO],
- "caption": "photo description"
+ "caption": "photo description",
}
MESSAGE_WITH_MEDIA_GROUP = {
@@ -333,7 +333,7 @@ MESSAGE_WITH_MEDIA_GROUP = {
"chat": CHAT,
"date": 1522843665,
"media_group_id": "12182749320567362",
- "photo": [PHOTO, PHOTO, PHOTO, PHOTO]
+ "photo": [PHOTO, PHOTO, PHOTO, PHOTO],
}
MESSAGE_WITH_PINNED_MESSAGE = {}
@@ -345,7 +345,7 @@ MESSAGE_WITH_STICKER = {
"from": USER,
"chat": CHAT,
"date": 1508771450,
- "sticker": STICKER
+ "sticker": STICKER,
}
MESSAGE_WITH_SUCCESSFUL_PAYMENT = {
@@ -353,7 +353,7 @@ MESSAGE_WITH_SUCCESSFUL_PAYMENT = {
"from": USER,
"chat": CHAT,
"date": 1508761169,
- "successful_payment": SUCCESSFUL_PAYMENT
+ "successful_payment": SUCCESSFUL_PAYMENT,
}
MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {}
@@ -364,7 +364,7 @@ MESSAGE_WITH_VENUE = {
"chat": CHAT,
"date": 1522849819,
"location": LOCATION,
- "venue": VENUE
+ "venue": VENUE,
}
MESSAGE_WITH_VIDEO = {
@@ -373,7 +373,7 @@ MESSAGE_WITH_VIDEO = {
"chat": CHAT,
"date": 1508756494,
"video": VIDEO,
- "caption": "description"
+ "caption": "description",
}
MESSAGE_WITH_VIDEO_NOTE = {
@@ -381,7 +381,7 @@ MESSAGE_WITH_VIDEO_NOTE = {
"from": USER,
"chat": CHAT,
"date": 1522835890,
- "video_note": VIDEO_NOTE
+ "video_note": VIDEO_NOTE,
}
MESSAGE_WITH_VOICE = {
@@ -389,7 +389,7 @@ MESSAGE_WITH_VOICE = {
"from": USER,
"chat": CHAT,
"date": 1508768403,
- "voice": VOICE
+ "voice": VOICE,
}
PRE_CHECKOUT_QUERY = {
@@ -397,7 +397,7 @@ PRE_CHECKOUT_QUERY = {
"from": USER,
"currency": "USD",
"total_amount": 6250,
- "invoice_payload": "HAPPY FRIDAYS COUPON"
+ "invoice_payload": "HAPPY FRIDAYS COUPON",
}
REPLY_MESSAGE = {
@@ -406,37 +406,37 @@ REPLY_MESSAGE = {
"chat": CHAT,
"date": 1508751866,
"reply_to_message": MESSAGE,
- "text": "Reply to quoted message"
+ "text": "Reply to quoted message",
}
SHIPPING_QUERY = {
"id": "262181558684397422",
"from": USER,
"invoice_payload": "HAPPY FRIDAYS COUPON",
- "shipping_address": SHIPPING_ADDRESS
+ "shipping_address": SHIPPING_ADDRESS,
}
USER_PROFILE_PHOTOS = {
"total_count": 1, "photos": [
- [PHOTO, PHOTO, PHOTO]
- ]
+ [PHOTO, PHOTO, PHOTO],
+ ],
}
FILE = {
"file_id": "XXXYYYZZZ",
"file_size": 5254,
- "file_path": "voice\/file_8"
+ "file_path": "voice/file_8",
}
INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123'
UPDATE = {
"update_id": 123456789,
- "message": MESSAGE
+ "message": MESSAGE,
}
WEBHOOK_INFO = {
"url": "",
"has_custom_certificate": False,
- "pending_update_count": 0
+ "pending_update_count": 0,
}
diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py
new file mode 100644
index 00000000..2cea44ce
--- /dev/null
+++ b/tests/types/test_chat_member.py
@@ -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()