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 a1c2736b..b81ceedb 100644
--- a/aiogram/__init__.py
+++ b/aiogram/__init__.py
@@ -38,5 +38,5 @@ __all__ = [
'utils'
]
-__version__ = '2.2.1.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/bot.py b/aiogram/bot/bot.py
index b0fc3725..53e49997 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)
+ 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..8373f3d6 100644
--- a/aiogram/contrib/middlewares/i18n.py
+++ b/aiogram/contrib/middlewares/i18n.py
@@ -107,7 +107,7 @@ class I18nMiddleware(BaseMiddleware):
else:
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 +115,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..0da5f621 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -9,7 +9,7 @@ import aiohttp
from aiohttp.helpers import sentinel
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
- RegexpCommandsFilter, StateFilter, Text
+ RegexpCommandsFilter, StateFilter, Text, IdFilter
from .handler import Handler
from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@@ -97,7 +97,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(Text, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers,
- self.callback_query_handlers, self.poll_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,
@@ -106,7 +106,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(Regexp, event_handlers=[
self.message_handlers, self.edited_message_handlers,
self.channel_post_handlers, self.edited_channel_post_handlers,
- self.callback_query_handlers, self.poll_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
@@ -114,6 +114,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(ExceptionsFilter, event_handlers=[
self.errors_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):
self.stop_polling()
diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py
index 2ae959cf..eb4a5a52 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
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
@@ -23,6 +23,7 @@ __all__ = [
'Regexp',
'StateFilter',
'Text',
+ 'IdFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py
index 011b9b67..15cd73dd 100644
--- a/aiogram/dispatcher/filters/builtin.py
+++ b/aiogram/dispatcher/filters/builtin.py
@@ -221,13 +221,13 @@ class Text(Filter):
: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!")
@@ -249,7 +249,7 @@ class Text(Filter):
elif 'text_endswith' in full_config:
return {'endswith': full_config.pop('text_endswith')}
- 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:
@@ -266,14 +266,26 @@ class Text(Filter):
if self.ignore_case:
text = text.lower()
- 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))
+ if self.equals is not None:
+ self.equals = str(self.equals)
+ if self.ignore_case:
+ self.equals = self.equals.lower()
+ return text == self.equals
+ elif self.contains is not None:
+ self.contains = str(self.contains)
+ if self.ignore_case:
+ self.contains = self.contains.lower()
+ return self.contains in text
+ elif self.startswith is not None:
+ self.startswith = str(self.startswith)
+ if self.ignore_case:
+ self.startswith = self.startswith.lower()
+ return text.startswith(self.startswith)
+ elif self.endswith is not None:
+ self.endswith = str(self.endswith)
+ if self.ignore_case:
+ self.endswith = self.endswith.lower()
+ return text.endswith(self.endswith)
return False
@@ -359,13 +371,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 +503,66 @@ 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
+ elif self.user_id:
+ return user_id in self.user_id
+ elif self.chat_id:
+ return chat_id in self.chat_id
+
+ return False
diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py
index 46e44fc9..4806c55a 100644
--- a/aiogram/dispatcher/filters/filters.py
+++ b/aiogram/dispatcher/filters/filters.py
@@ -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/handler.py b/aiogram/dispatcher/handler.py
index 859cb47e..889dc8d6 100644
--- a/aiogram/dispatcher/handler.py
+++ b/aiogram/dispatcher/handler.py
@@ -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):
@@ -56,7 +55,7 @@ class Handler:
:param filters: list of filters
:param index: you can reorder handlers
"""
- spec, handler = _get_spec(handler)
+ spec = _get_spec(handler)
if filters and not isinstance(filters, (list, tuple, set)):
filters = [filters]
@@ -105,7 +104,7 @@ class Handler:
try:
for handler_obj in self.handlers:
try:
- data.update(await check_filters(handler_obj.filters, args + (data,)))
+ data.update(await check_filters(handler_obj.filters, args))
except FilterNotPassed:
continue
else:
diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py
index 4c06c2af..bee635ae 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)
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 89506400..7e05a33f 100644
--- a/aiogram/types/chat_member.py
+++ b/aiogram/types/chat_member.py
@@ -29,6 +29,7 @@ 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()
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 7637cf42..67fd07fa 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -959,70 +959,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.3 version.\n'
- 'Use "Message.reply_animation" instead.',
- stacklevel=8)
-
- return await self.bot.send_animation(self.chat.id,
- animation=animation,
- duration=duration,
- width=width,
- height=height,
- thumb=thumb,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
@@ -1323,55 +1259,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
- async def send_venue(self,
- latitude: base.Float, longitude: base.Float,
- title: base.String, address: base.String,
- foursquare_id: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
- """
- Use this method to send information about a venue.
-
- Source: https://core.telegram.org/bots/api#sendvenue
-
- :param latitude: Latitude of the venue
- :type latitude: :obj:`base.Float`
- :param longitude: Longitude of the venue
- :type longitude: :obj:`base.Float`
- :param title: Name of the venue
- :type title: :obj:`base.String`
- :param address: Address of the venue
- :type address: :obj:`base.String`
- :param foursquare_id: Foursquare identifier of the venue
- :type foursquare_id: :obj:`typing.Union[base.String, None]`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
- :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
- :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
- custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
- :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
- types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
- :param reply: fill 'reply_to_message_id'
- :return: On success, the sent Message is returned.
- :rtype: :obj:`types.Message`
- """
- warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n'
- 'Use "Message.reply_venue" instead.',
- stacklevel=8)
-
- return await self.bot.send_venue(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- title=title,
- address=address,
- foursquare_id=foursquare_id,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
-
async def reply_venue(self,
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
@@ -1417,46 +1304,6 @@ class Message(base.TelegramObject):
reply_to_message_id=self.message_id if reply else None,
reply_markup=reply_markup)
- async def send_contact(self, phone_number: base.String,
- first_name: base.String, last_name: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
- """
- Use this method to send phone contacts.
-
- Source: https://core.telegram.org/bots/api#sendcontact
-
- :param phone_number: Contact's phone number
- :type phone_number: :obj:`base.String`
- :param first_name: Contact's first name
- :type first_name: :obj:`base.String`
- :param last_name: Contact's last name
- :type last_name: :obj:`typing.Union[base.String, None]`
- :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
- :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
- :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
- custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
- :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
- types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
- :param reply: fill 'reply_to_message_id'
- :return: On success, the sent Message is returned.
- :rtype: :obj:`types.Message`
- """
- warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n'
- 'Use "Message.reply_contact" instead.',
- stacklevel=8)
-
- return await self.bot.send_contact(chat_id=self.chat.id,
- phone_number=phone_number,
- first_name=first_name, last_name=last_name,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
-
async def reply_contact(self, phone_number: base.String,
first_name: base.String, last_name: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
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/utils/exceptions.py b/aiogram/utils/exceptions.py
index afd623cc..a6612547 100644
--- a/aiogram/utils/exceptions.py
+++ b/aiogram/utils/exceptions.py
@@ -490,7 +490,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
class BotKicked(Unauthorized):
- match = 'Bot was kicked from a chat'
+ match = 'bot was kicked from a chat'
class BotBlocked(Unauthorized):
diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst
index d103ac36..af03e163 100644
--- a/docs/source/dispatcher/filters.rst
+++ b/docs/source/dispatcher/filters.rst
@@ -111,6 +111,14 @@ ExceptionsFilter
:show-inheritance:
+IdFilter
+----------------
+
+.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter
+ :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/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..8fd197df 100644
--- a/examples/callback_data_factory.py
+++ b/examples/callback_data_factory.py
@@ -13,8 +13,8 @@ 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())
diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py
new file mode 100644
index 00000000..2c6a8358
--- /dev/null
+++ b/examples/callback_data_factory_simple.py
@@ -0,0 +1,70 @@
+"""
+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! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard())
+
+
+@dp.callback_query_handler(vote_cb.filter(action='up'))
+async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict):
+ logging.info(callback_data) # callback_data contains all info from callback data
+ likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage
+ amount_of_likes = likes[query.from_user.id]
+
+ await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.',
+ query.from_user.id,
+ query.message.message_id,
+ reply_markup=get_keyboard())
+
+
+@dp.callback_query_handler(vote_cb.filter(action='down'))
+async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict):
+ logging.info(callback_data) # callback_data contains all info from callback data
+ likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage
+ amount_of_likes = likes[query.from_user.id]
+
+ await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.',
+ query.from_user.id,
+ query.message.message_id,
+ reply_markup=get_keyboard())
+
+
+@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
+async def message_not_modified_handler(update, error):
+ # pass
+ 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..f59246cf 100644
--- a/examples/check_user_language.py
+++ b/examples/check_user_language.py
@@ -11,8 +11,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)
diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py
index 58b8053c..66f89fb2 100644
--- a/examples/finite_state_machine_example.py
+++ b/examples/finite_state_machine_example.py
@@ -11,9 +11,7 @@ from aiogram.utils import executor
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()
@@ -112,9 +110,9 @@ async def process_gender(message: types.Message, state: FSMContext):
md.text('Gender:', data['gender']),
sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN)
- # Finish conversation
- data.state = None
+ # Finish conversation
+ await state.finish()
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop, skip_updates=True)
+ 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..64dc3b3f
--- /dev/null
+++ b/examples/id_filter_example.py
@@ -0,0 +1,38 @@
+from aiogram import Bot, Dispatcher, executor, types
+from aiogram.dispatcher.handler import SkipHandler
+
+API_TOKEN = 'API_TOKE_HERE'
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+user_id_to_test = None # todo: Set id here
+chat_id_to_test = user_id_to_test
+
+
+@dp.message_handler(user_id=user_id_to_test)
+async def handler1(msg: types.Message):
+ await bot.send_message(msg.chat.id,
+ "Hello, checking with user_id=")
+ raise SkipHandler
+
+
+@dp.message_handler(chat_id=chat_id_to_test)
+async def handler2(msg: types.Message):
+ await bot.send_message(msg.chat.id,
+ "Hello, checking with chat_id=")
+ raise SkipHandler
+
+
+@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test)
+async def handler3(msg: types.Message):
+ await bot.send_message(msg.chat.id,
+ "Hello from user= & chat_id=")
+
+
+@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here
+async def handler4(msg: types.Message):
+ print("Checked user_id with list!")
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp)
diff --git a/examples/inline_bot.py b/examples/inline_bot.py
index 4a771210..f1a81bb4 100644
--- a/examples/inline_bot.py
+++ b/examples/inline_bot.py
@@ -7,8 +7,7 @@ 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)
@@ -21,4 +20,4 @@ async def inline_echo(inline_query: types.InlineQuery):
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..2478b9e0
--- /dev/null
+++ b/examples/inline_keyboard_example.py
@@ -0,0 +1,56 @@
+"""
+This bot is created for the demonstration of a usage of inline keyboards.
+"""
+
+import logging
+
+from aiogram import Bot, Dispatcher, executor, types
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# Initialize bot and dispatcher
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+
+@dp.message_handler(commands=['start'])
+async def start_cmd_handler(message: types.Message):
+ keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
+ # default row_width is 3, so here we can omit it actually
+ # kept for clearness
+
+ keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'),
+ # in real life for the callback_data the callback data factory should be used
+ # here the raw string is used for the simplicity
+ types.InlineKeyboardButton("No!", callback_data='no'))
+
+ keyboard_markup.add(types.InlineKeyboardButton("aiogram link",
+ url='https://github.com/aiogram/aiogram'))
+ # url buttons has no callback data
+
+ await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
+
+
+@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no'
+# @dp.callback_query_handler(text='yes') # if cb.data == 'yes'
+async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
+ await query.answer() # send answer to close the rounding circle
+
+ answer_data = query.data
+ logger.debug(f"answer_data={answer_data}")
+ # here we can work with query.data
+ if answer_data == 'yes':
+ await bot.send_message(query.from_user.id, "That's great!")
+ elif answer_data == 'no':
+ await bot.send_message(query.from_user.id, "Oh no...Why so?")
+ else:
+ await bot.send_message(query.from_user.id, "Invalid callback data!")
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/media_group.py b/examples/media_group.py
index b1f5246a..eafbac6a 100644
--- a/examples/media_group.py
+++ b/examples/media_group.py
@@ -4,8 +4,7 @@ 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)
+bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@@ -40,4 +39,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..4a0cc491 100644
--- a/examples/middleware_and_antiflood.py
+++ b/examples/middleware_and_antiflood.py
@@ -9,12 +9,10 @@ from aiogram.utils.exceptions import Throttled
TOKEN = 'BOT TOKEN HERE'
-loop = asyncio.get_event_loop()
-
# 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..a01fbaf3 100644
--- a/examples/payments.py
+++ b/examples/payments.py
@@ -9,9 +9,8 @@ from aiogram.utils import executor
BOT_TOKEN = 'BOT TOKEN HERE'
PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef'
-loop = asyncio.get_event_loop()
bot = Bot(BOT_TOKEN)
-dp = Dispatcher(bot, loop=loop)
+dp = Dispatcher(bot)
# Setup prices
prices = [
@@ -96,4 +95,4 @@ async def got_payment(message: types.Message):
if __name__ == '__main__':
- executor.start_polling(dp, loop=loop)
+ executor.start_polling(dp)
diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py
index 7e4452ee..17e33872 100644
--- a/examples/proxy_and_emojize.py
+++ b/examples/proxy_and_emojize.py
@@ -25,8 +25,7 @@ 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)
dp = Dispatcher(bot)
@@ -62,4 +61,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/regular_keyboard_example.py b/examples/regular_keyboard_example.py
new file mode 100644
index 00000000..350e007e
--- /dev/null
+++ b/examples/regular_keyboard_example.py
@@ -0,0 +1,61 @@
+"""
+This bot is created for the demonstration of a usage of regular keyboards.
+"""
+
+import logging
+
+from aiogram import Bot, Dispatcher, executor, types
+
+API_TOKEN = 'BOT_TOKEN_HERE'
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# Initialize bot and dispatcher
+bot = Bot(token=API_TOKEN)
+dp = Dispatcher(bot)
+
+
+@dp.message_handler(commands=['start'])
+async def start_cmd_handler(message: types.Message):
+ keyboard_markup = types.ReplyKeyboardMarkup(row_width=3)
+ # default row_width is 3, so here we can omit it actually
+ # kept for clearness
+
+ keyboard_markup.row(types.KeyboardButton("Yes!"),
+ types.KeyboardButton("No!"))
+ # adds buttons as a new row to the existing keyboard
+ # the behaviour doesn't depend on row_width attribute
+
+ keyboard_markup.add(types.KeyboardButton("I don't know"),
+ types.KeyboardButton("Who am i?"),
+ types.KeyboardButton("Where am i?"),
+ types.KeyboardButton("Who is there?"))
+ # adds buttons. New rows is formed according to row_width parameter
+
+ await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
+
+
+@dp.message_handler()
+async def all_msg_handler(message: types.Message):
+ # pressing of a KeyboardButton is the same as sending the regular message with the same text
+ # so, to handle the responses from the keyboard, we need to use a message_handler
+ # in real bot, it's better to define message_handler(text="...") for each button
+ # but here for the simplicity only one handler is defined
+
+ text_of_button = message.text
+ logger.debug(text_of_button) # print the text we got
+
+ if text_of_button == 'Yes!':
+ await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove())
+ elif text_of_button == 'No!':
+ await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove())
+ else:
+ await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove())
+ # with message, we send types.ReplyKeyboardRemove() to hide the keyboard
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/throtling_example.py b/examples/throtling_example.py
index b979a979..2641b44b 100644
--- a/examples/throtling_example.py
+++ b/examples/throtling_example.py
@@ -17,8 +17,7 @@ API_TOKEN = 'BOT TOKEN HERE'
logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
+bot = Bot(token=API_TOKEN)
# Throttling manager does not work without Leaky Bucket.
# Then need to use storages. For example use simple in-memory storage.
@@ -40,4 +39,4 @@ async def send_welcome(message: types.Message):
if __name__ == '__main__':
- start_polling(dp, loop=loop, skip_updates=True)
+ start_polling(dp, skip_updates=True)
diff --git a/examples/webhook_example.py b/examples/webhook_example.py
index 86520988..0f6ae3cd 100644
--- a/examples/webhook_example.py
+++ b/examples/webhook_example.py
@@ -37,8 +37,7 @@ WEBAPP_PORT = 3001
BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO
-loop = asyncio.get_event_loop()
-bot = Bot(TOKEN, loop=loop)
+bot = Bot(TOKEN)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
diff --git a/examples/webhook_example_2.py b/examples/webhook_example_2.py
index 75b29c75..e2d9225c 100644
--- a/examples/webhook_example_2.py
+++ b/examples/webhook_example_2.py
@@ -18,8 +18,7 @@ WEBAPP_PORT = 3001
logging.basicConfig(level=logging.INFO)
-loop = asyncio.get_event_loop()
-bot = Bot(token=API_TOKEN, loop=loop)
+bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
diff --git a/tests/test_filters.py b/tests/test_filters.py
new file mode 100644
index 00000000..da530910
--- /dev/null
+++ b/tests/test_filters.py
@@ -0,0 +1,197 @@
+import pytest
+
+from aiogram.dispatcher.filters import Text
+from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
+
+
+class TestTextFilter:
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize("test_prefix, test_text, ignore_case",
+ [('', '', True),
+ ('', 'exAmple_string', True),
+ ('', '', False),
+ ('', 'exAmple_string', False),
+
+ ('example_string', 'example_string', True),
+ ('example_string', 'exAmple_string', True),
+ ('exAmple_string', 'example_string', True),
+
+ ('example_string', 'example_string', False),
+ ('example_string', 'exAmple_string', False),
+ ('exAmple_string', 'example_string', False),
+
+ ('example_string', 'example_string_dsf', True),
+ ('example_string', 'example_striNG_dsf', True),
+ ('example_striNG', 'example_string_dsf', True),
+
+ ('example_string', 'example_string_dsf', False),
+ ('example_string', 'example_striNG_dsf', False),
+ ('example_striNG', 'example_string_dsf', False),
+
+ ('example_string', 'not_example_string', True),
+ ('example_string', 'not_eXample_string', True),
+ ('EXample_string', 'not_example_string', True),
+
+ ('example_string', 'not_example_string', False),
+ ('example_string', 'not_eXample_string', False),
+ ('EXample_string', 'not_example_string', False),
+ ])
+ async def test_startswith(self, test_prefix, test_text, ignore_case):
+ 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)
+
+ 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("test_postfix, test_text, ignore_case",
+ [('', '', True),
+ ('', 'exAmple_string', True),
+ ('', '', False),
+ ('', 'exAmple_string', False),
+
+ ('example_string', 'example_string', True),
+ ('example_string', 'exAmple_string', True),
+ ('exAmple_string', 'example_string', True),
+
+ ('example_string', 'example_string', False),
+ ('example_string', 'exAmple_string', False),
+ ('exAmple_string', 'example_string', False),
+
+ ('example_string', 'example_string_dsf', True),
+ ('example_string', 'example_striNG_dsf', True),
+ ('example_striNG', 'example_string_dsf', True),
+
+ ('example_string', 'example_string_dsf', False),
+ ('example_string', 'example_striNG_dsf', False),
+ ('example_striNG', 'example_string_dsf', False),
+
+ ('example_string', 'not_example_string', True),
+ ('example_string', 'not_eXample_string', True),
+ ('EXample_string', 'not_eXample_string', True),
+
+ ('example_string', 'not_example_string', False),
+ ('example_string', 'not_eXample_string', False),
+ ('EXample_string', 'not_example_string', False),
+ ])
+ async def test_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)
+
+ 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("test_string, test_text, ignore_case",
+ [('', '', True),
+ ('', 'exAmple_string', True),
+ ('', '', False),
+ ('', 'exAmple_string', False),
+
+ ('example_string', 'example_string', True),
+ ('example_string', 'exAmple_string', True),
+ ('exAmple_string', 'example_string', True),
+
+ ('example_string', 'example_string', False),
+ ('example_string', 'exAmple_string', False),
+ ('exAmple_string', 'example_string', False),
+
+ ('example_string', 'example_string_dsf', True),
+ ('example_string', 'example_striNG_dsf', True),
+ ('example_striNG', 'example_string_dsf', True),
+
+ ('example_string', 'example_string_dsf', False),
+ ('example_string', 'example_striNG_dsf', False),
+ ('example_striNG', 'example_string_dsf', False),
+
+ ('example_string', 'not_example_strin', True),
+ ('example_string', 'not_eXample_strin', True),
+ ('EXample_string', 'not_eXample_strin', True),
+
+ ('example_string', 'not_example_strin', False),
+ ('example_string', 'not_eXample_strin', False),
+ ('EXample_string', 'not_example_strin', False),
+ ])
+ 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)
+
+ 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("test_filter_text, test_text, ignore_case",
+ [('', '', True),
+ ('', 'exAmple_string', True),
+ ('', '', False),
+ ('', 'exAmple_string', False),
+
+ ('example_string', 'example_string', True),
+ ('example_string', 'exAmple_string', True),
+ ('exAmple_string', 'example_string', True),
+
+ ('example_string', 'example_string', False),
+ ('example_string', 'exAmple_string', False),
+ ('exAmple_string', 'example_string', False),
+
+ ('example_string', 'not_example_string', True),
+ ('example_string', 'not_eXample_string', True),
+ ('EXample_string', 'not_eXample_string', True),
+
+ ('example_string', 'not_example_string', False),
+ ('example_string', 'not_eXample_string', False),
+ ('EXample_string', 'not_example_string', False),
+ ])
+ async def test_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)
+
+ 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))