From c6accd1a53d43e4cd63c105a370a4830bd168b4a Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 19:34:38 +0500 Subject: [PATCH 01/50] Add "expire" argument Add "expire" argument to all set_ and update_ methods in RedisStorage2 --- aiogram/contrib/fsm_storage/redis.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index c3a91f00..6b96869b 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -287,29 +287,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None): + state: typing.Optional[typing.AnyStr] = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state) + await redis.set(key, state, expire=expire) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None): + data: typing.Dict = None, expire: int = 0): 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=expire) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, **kwargs): + data: typing.Dict = None, expire: int = 0, **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) + await self.set_data(chat=chat, user=user, data=temp_data, expire=expire) def has_bucket(self): return True @@ -325,20 +325,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None): + bucket: typing.Dict = None, expire: int = 0): 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=expire) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, **kwargs): + bucket: typing.Dict = None, expire: int = 0, **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, data=temp_bucket) + await self.set_bucket(chat=chat, user=user, data=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From e2842944fa1e051648341b07cb920489f4f55e29 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 20:39:26 +0500 Subject: [PATCH 02/50] Update redis.py Update docstrings --- aiogram/contrib/fsm_storage/redis.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 6b96869b..e75e7f40 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -62,8 +62,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: @@ -241,8 +239,6 @@ class RedisStorage2(BaseStorage): 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: From 8a819eb7ed0758f7342e234c1b10f6c9282e72f1 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 21:00:25 +0500 Subject: [PATCH 03/50] Fix update_bucket --- aiogram/contrib/fsm_storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index e75e7f40..0e89eaea 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -334,7 +334,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, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From 607e3ea13514942bc10192a81604e00404f209c8 Mon Sep 17 00:00:00 2001 From: Gabben Date: Fri, 28 Jun 2019 15:26:07 +0500 Subject: [PATCH 04/50] Update redis.py --- aiogram/contrib/fsm_storage/redis.py | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 0e89eaea..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 @@ -220,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 @@ -233,6 +235,10 @@ 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) @@ -283,29 +289,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None, expire: int = 0): + state: typing.Optional[typing.AnyStr] = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state, expire=expire) + 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, expire: int = 0): + 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), expire=expire) + 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, expire: int = 0, **kwargs): + data: typing.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, expire=expire) + await self.set_data(chat=chat, user=user, data=temp_data) def has_bucket(self): return True @@ -321,20 +327,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0): + bucket: typing.Dict = None): 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), expire=expire) + await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0, **kwargs): + bucket: typing.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, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket) async def reset_all(self, full=True): """ From eae7dc5a2e09b1d12224e0d7112abf1c47c7306b Mon Sep 17 00:00:00 2001 From: SetazeR Date: Mon, 1 Jul 2019 09:19:31 +0700 Subject: [PATCH 05/50] unify finishing state machine interaction --- examples/finite_state_machine_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 58b8053c..90ab8aba 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -112,8 +112,8 @@ async def process_gender(message: types.Message, state: FSMContext): md.text('Gender:', data['gender']), sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN) - # Finish conversation - data.state = None + # Finish conversation + await state.finish() if __name__ == '__main__': From 1b8bcbd1d92eb9ddfd07192bbf1d8cee3e7dd0c4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 2 Jul 2019 02:05:20 +0300 Subject: [PATCH 06/50] added mongo storage --- aiogram/contrib/fsm_storage/mongo.py | 201 +++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 aiogram/contrib/fsm_storage/mongo.py diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py new file mode 100644 index 00000000..e0a6b3cc --- /dev/null +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -0,0 +1,201 @@ +""" +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}' if self._host else 'localhost' + uri += f':{self._port}' if self._port else '/' + + # 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={'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={'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={'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 From a339b7c5712c6dcab332430ef21dd7c137966101 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 2 Jul 2019 10:05:10 +0300 Subject: [PATCH 07/50] Revert "Send data from middlewares to filters " --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 859cb47e..17b715d1 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -105,7 +105,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: From cc6ecefbf730ee54aba7eb60304367db924cc440 Mon Sep 17 00:00:00 2001 From: Jess Date: Sun, 7 Jul 2019 09:59:03 -0700 Subject: [PATCH 08/50] Added financial contributors to the README --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f977023..979b38e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AIOGram -[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) +[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?label=financial+contributors)](https://opencollective.com/aiogram) [![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) [![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) @@ -24,3 +24,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. [[Contribute](CONTRIBUTING.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)] + + + + + + + + + + + From a1531d4e2064c6dd94f5f1d1325f828b07e67e9b Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 8 Jul 2019 23:39:24 +0300 Subject: [PATCH 09/50] logged callback sender id instead of message sender id --- aiogram/contrib/middlewares/logging.py | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 1a3566c6..d0f257d6 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,34 +89,27 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - 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}]") + self.logger.info(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}]") 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: - 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}]") + self.logger.debug(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}]") 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}] " From 4a2b569e5ff0ca278e6d60755153710021b4cbca Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 00:05:46 +0300 Subject: [PATCH 10/50] fixed uri; fixed $set --- aiogram/contrib/fsm_storage/mongo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index e0a6b3cc..9ec18090 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -60,8 +60,7 @@ class MongoStorage(BaseStorage): uri += f'{self._username}:{self._password}@' # set host and port (optional) - uri += f'{self._host}' if self._host else 'localhost' - uri += f':{self._port}' if self._port else '/' + 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) @@ -107,7 +106,7 @@ class MongoStorage(BaseStorage): await db[STATE].delete_one(filter={'chat': chat, 'user': user}) else: await db[STATE].update_one(filter={'chat': chat, 'user': user}, - update={'state': state}, upsert=True) + 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]: @@ -123,7 +122,7 @@ class MongoStorage(BaseStorage): db = await self.get_db() await db[DATA].update_one(filter={'chat': chat, 'user': user}, - update={'data': data}, upsert=True) + 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: @@ -157,7 +156,7 @@ class MongoStorage(BaseStorage): db = await self.get_db() await db[BUCKET].update_one(filter={'chat': chat, 'user': user}, - update={'bucket': bucket}, upsert=True) + update={'$set': {'bucket': bucket}}, upsert=True) async def update_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, From 49e15545fa9035c3e69de2e6fd98a1cf04a6defc Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:29:53 +0300 Subject: [PATCH 11/50] added `originally posted by` to callback --- aiogram/contrib/middlewares/logging.py | 30 ++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index d0f257d6..c5d61aac 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,10 +89,16 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - self.logger.info(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}]") + 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: + text += f" originally posted by [{callback_query.message.from_user.id}]" + + self.logger.info(text) + else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " @@ -100,11 +106,17 @@ class LoggingMiddleware(BaseMiddleware): async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: - self.logger.debug(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}]") + 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: + text += f" originally posted by [{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}] " From 1dd462fcf3572ff56af3c2d694f8cbfe576f6c76 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:38:54 +0300 Subject: [PATCH 12/50] added `user` --- aiogram/contrib/middlewares/logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index c5d61aac..9f389b60 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -95,7 +95,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) @@ -113,7 +113,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) From af889ad6b20a939c2380b76f6059628ff3449975 Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:04:03 +0300 Subject: [PATCH 13/50] Add regular keyboard usage example --- examples/regular_keyboard_example.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 examples/regular_keyboard_example.py 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) From 31b92e1b95fb1aa2461f04f087dcddded3d3d7ea Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:16:00 +0300 Subject: [PATCH 14/50] Add inline keyboard usage example --- examples/inline_keyboard_example.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/inline_keyboard_example.py 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) From 7b9422f627b493b13789683971b52c5fb09c4e53 Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 21:08:29 +0300 Subject: [PATCH 15/50] Add simple example of usage of callback data factory --- examples/callback_data_factory_simple.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 examples/callback_data_factory_simple.py 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) From 1889597aec9b5af083597d68b323067312725583 Mon Sep 17 00:00:00 2001 From: birdi Date: Fri, 12 Jul 2019 20:25:34 +0300 Subject: [PATCH 16/50] Closes #138 --- aiogram/contrib/fsm_storage/files.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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): From 4a4eb5cff90f7ae4c44f5e09c186c90ae0f13798 Mon Sep 17 00:00:00 2001 From: AmirHossein Falahati Date: Sat, 13 Jul 2019 02:33:02 +0430 Subject: [PATCH 17/50] fix BotKicked --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 9d44ed1a460e5fdfc297a4b558a2917146fe3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCller?= Date: Sat, 13 Jul 2019 10:43:53 +0200 Subject: [PATCH 18/50] Typo fix --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 4c06c2af..8e483255 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -77,7 +77,7 @@ class WebhookRequestHandler(web.View): .. code-block:: python3 - app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler') + app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler') But first you need to configure application for getting Dispatcher instance from request handler! It must always be with key 'BOT_DISPATCHER' From 6cd63cb7bc5a51e88280fb8ec364b157f17d8c3d Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Sun, 14 Jul 2019 02:43:15 +0500 Subject: [PATCH 19/50] Add warn in validate_ip Refer to issue #135 --- aiogram/dispatcher/webhook.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 8e483255..c56f20a6 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: """ @@ -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 a unauthorized IP: {ip_address}") raise web.HTTPUnauthorized() + # context.set_value('TELEGRAM_IP', ip_address) From a0ad19888ed39c4f9ec28a07eae7012b06ee9f2f Mon Sep 17 00:00:00 2001 From: Arslan Sakhapov Date: Sun, 14 Jul 2019 03:11:41 +0500 Subject: [PATCH 20/50] Fix typo --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index c56f20a6..bee635ae 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -261,7 +261,7 @@ 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 a unauthorized IP: {ip_address}") + log.warning(f"Blocking request from an unauthorized IP: {ip_address}") raise web.HTTPUnauthorized() # context.set_value('TELEGRAM_IP', ip_address) From dacc8226a7f0fa32ced7d79d272d932e1b7899f8 Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+MrMrRobat@users.noreply.github.com> Date: Sun, 14 Jul 2019 03:26:46 +0300 Subject: [PATCH 21/50] Fixed parse_mode type --- aiogram/types/input_media.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 From 8f570315bc395a44ef374ff58720dc85c7f466bf Mon Sep 17 00:00:00 2001 From: jess Date: Sun, 14 Jul 2019 18:01:40 -0700 Subject: [PATCH 22/50] Update README.md changed badge style to flat square --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 979b38e4..f25d7906 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # AIOGram -[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?label=financial+contributors)](https://opencollective.com/aiogram) [![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) +[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?style=flat-square)](https://opencollective.com/aiogram) +[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) [![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) From 1ea76cd902cfc19986389a52adda70a8bb4555db Mon Sep 17 00:00:00 2001 From: SetazeR Date: Mon, 15 Jul 2019 14:00:38 +0700 Subject: [PATCH 23/50] remove unnecessary loop definitions --- examples/broadcast_example.py | 5 ++--- examples/callback_data_factory.py | 4 ++-- examples/check_user_language.py | 4 ++-- examples/finite_state_machine_example.py | 6 ++---- examples/inline_bot.py | 5 ++--- examples/media_group.py | 5 ++--- examples/middleware_and_antiflood.py | 6 ++---- examples/payments.py | 5 ++--- examples/proxy_and_emojize.py | 5 ++--- examples/throtling_example.py | 5 ++--- examples/webhook_example.py | 3 +-- examples/webhook_example_2.py | 3 +-- 12 files changed, 22 insertions(+), 34 deletions(-) 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/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 90ab8aba..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() @@ -117,4 +115,4 @@ async def process_gender(message: types.Message, state: FSMContext): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) 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/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/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) From 40b9b80ce9f520a74a082a6509c3b54deb8a5a89 Mon Sep 17 00:00:00 2001 From: Gabben Date: Tue, 16 Jul 2019 19:48:59 +0500 Subject: [PATCH 24/50] Fix builtin filters Add inline_query_handlers to Text and Regexp bind Fix type hints --- aiogram/dispatcher/dispatcher.py | 4 ++-- aiogram/dispatcher/filters/builtin.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index e11ff536..39eb7810 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -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 diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 011b9b67..c68bae72 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -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: @@ -359,13 +359,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 From fa1b1aa3ffa85fd7cda437bc91c28e21302c1d0f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 17 Jul 2019 21:45:19 +0300 Subject: [PATCH 25/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f25d7906..c0a5bc26 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). ### Code Contributors -This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. +This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)]. ### Financial Contributors From edfc74069efc55cc70437c24dc93658109171c20 Mon Sep 17 00:00:00 2001 From: WeatherControl Date: Thu, 18 Jul 2019 15:38:00 +0300 Subject: [PATCH 26/50] --- aiogram/bot/bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index b0fc3725..e7fd5d6e 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -337,12 +337,14 @@ 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) From 92334a57bea60def7b1dfd9ee3f64c09be604a26 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 16:19:53 +0300 Subject: [PATCH 27/50] Add enable_cache parameter to lazy_gettext --- aiogram/contrib/middlewares/i18n.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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: From 0c9c83765295c9421adfb191d58dff81e2cffd42 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 20:52:41 +0300 Subject: [PATCH 28/50] Add tests for text filter --- tests/test_filters.py | 177 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/test_filters.py diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 00000000..a2095306 --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,177 @@ +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", + [('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", + [('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", + [('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(endswith=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", + [('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) + + 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)) From 46e5c7a3d000b50cea743af94d7b274be5f29ddf Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 22:57:59 +0300 Subject: [PATCH 29/50] Add ignore_case to equals test --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a2095306..153c31f6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -159,7 +159,7 @@ class TestTextFilter: ('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) + test_filter = Text(equals=test_filter_text, ignore_case=ignore_case) async def check(obj): result = await test_filter.check(obj) From c982fd54ccb4182556b28f9bd6e27a65c9fe348f Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 23:02:03 +0300 Subject: [PATCH 30/50] Fix contains test --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 153c31f6..f68b7c44 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -122,7 +122,7 @@ class TestTextFilter: ('EXample_string', 'not_example_strin', False), ]) async def test_contains(self, test_string, test_text, ignore_case): - test_filter = Text(endswith=test_string, ignore_case=ignore_case) + test_filter = Text(contains=test_string, ignore_case=ignore_case) async def check(obj): result = await test_filter.check(obj) From 6d22f9591a1292a3a3a2ae243d80c9de3ec540c4 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 23:14:00 +0300 Subject: [PATCH 31/50] Add ignore_case support in Text filter. Fixes #169 --- aiogram/dispatcher/filters/builtin.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index c68bae72..7c8caaa8 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -267,13 +267,25 @@ class Text(Filter): text = text.lower() if self.equals: - return text == str(self.equals) + self.equals = str(self.equals) + if self.ignore_case: + self.equals = self.equals.lower() + return text == self.equals elif self.contains: - return str(self.contains) in text + self.contains = str(self.contains) + if self.ignore_case: + self.contains = self.contains.lower() + return self.contains in text elif self.startswith: - return text.startswith(str(self.startswith)) + self.startswith = str(self.startswith) + if self.ignore_case: + self.startswith = self.startswith.lower() + return text.startswith(self.startswith) elif self.endswith: - return text.endswith(str(self.endswith)) + self.endswith = str(self.endswith) + if self.ignore_case: + self.endswith = self.endswith.lower() + return text.endswith(self.endswith) return False From 5c72bb2b589bd3d0727e58ebc75e6a084096a244 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:15:22 +0300 Subject: [PATCH 32/50] Add IdFilter --- aiogram/dispatcher/dispatcher.py | 7 ++- aiogram/dispatcher/filters/__init__.py | 3 +- aiogram/dispatcher/filters/builtin.py | 66 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 39eb7810..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, \ @@ -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 c68bae72..f3bbdba7 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -491,3 +491,69 @@ class ExceptionsFilter(BoundFilter): return True except: return False + + +class IdFilter(Filter): + + def __init__(self, + user_id: Optional[Union[str, int]] = None, + chat_id: Optional[Union[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 = user_id + self.chat_id = chat_id + + # both params should be convertible to int if they aren't None + # here we checks it + # also, by default in Telegram chat_id and user_id are Integer, + # so for convenience we cast them to int + if self.user_id: + self.user_id = int(self.user_id) + if self.chat_id: + self.chat_id = int(self.chat_id) + + @classmethod + def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + result = {} + if 'user' in full_config: + result['user_id'] = full_config.pop('user') + elif 'user_id' in full_config: + result['user_id'] = full_config.pop('user_id') + + if 'chat' in full_config: + result['chat_id'] = full_config.pop('chat') + elif '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 self.user_id == user_id and self.chat_id == chat_id + elif self.user_id: + return self.user_id == user_id + elif self.chat_id: + return self.chat_id == chat_id + + return False From c1fc41bd7e4da3cbd2b070197001d70c4b761245 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:16:30 +0300 Subject: [PATCH 33/50] Add example of usage of IdFilter --- examples/id-filter-example.py/tmp_test.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/id-filter-example.py/tmp_test.py diff --git a/examples/id-filter-example.py/tmp_test.py b/examples/id-filter-example.py/tmp_test.py new file mode 100644 index 00000000..297e23e4 --- /dev/null +++ b/examples/id-filter-example.py/tmp_test.py @@ -0,0 +1,46 @@ +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=user_id_to_test) +async def handler1(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with user=") + raise SkipHandler + + +@dp.message_handler(user_id=user_id_to_test) +async def handler2(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with user_id=") + raise SkipHandler + + +@dp.message_handler(chat=chat_id_to_test) +async def handler3(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with chat=") + raise SkipHandler + + +@dp.message_handler(chat_id=chat_id_to_test) +async def handler4(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with chat_id=") + raise SkipHandler + + +@dp.message_handler(user=user_id_to_test, chat_id=chat_id_to_test) +async def handler5(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello from user= & chat_id=") + +if __name__ == '__main__': + executor.start_polling(dp) From 6a3c13ed50c720305f27958a9233cb00b93c0286 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:21:16 +0300 Subject: [PATCH 34/50] Fix example directory --- .../{id-filter-example.py/tmp_test.py => id-filter-example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{id-filter-example.py/tmp_test.py => id-filter-example.py} (100%) diff --git a/examples/id-filter-example.py/tmp_test.py b/examples/id-filter-example.py similarity index 100% rename from examples/id-filter-example.py/tmp_test.py rename to examples/id-filter-example.py From 6bbc808a3b2f08dd0882fb4e8c7c080f67383cca Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:33:55 +0300 Subject: [PATCH 35/50] Add support for a list of ids and update the example --- aiogram/dispatcher/filters/builtin.py | 33 ++++++++++++++------------- examples/id-filter-example.py | 6 +++++ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index f3bbdba7..d64fb97a 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -496,8 +496,8 @@ class ExceptionsFilter(BoundFilter): class IdFilter(Filter): def __init__(self, - user_id: Optional[Union[str, int]] = None, - chat_id: Optional[Union[str, int]] = None, + 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: @@ -506,17 +506,18 @@ class IdFilter(Filter): 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 = user_id - self.chat_id = chat_id - - # both params should be convertible to int if they aren't None - # here we checks it - # also, by default in Telegram chat_id and user_id are Integer, - # so for convenience we cast them to int - if self.user_id: - self.user_id = int(self.user_id) - if self.chat_id: - self.chat_id = int(self.chat_id) + 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]]: @@ -550,10 +551,10 @@ class IdFilter(Filter): return False if self.user_id and self.chat_id: - return self.user_id == user_id and self.chat_id == chat_id + return user_id in self.user_id and chat_id in self.chat_id elif self.user_id: - return self.user_id == user_id + return user_id in self.user_id elif self.chat_id: - return self.chat_id == chat_id + return chat_id in self.chat_id return False diff --git a/examples/id-filter-example.py b/examples/id-filter-example.py index 297e23e4..b46ab056 100644 --- a/examples/id-filter-example.py +++ b/examples/id-filter-example.py @@ -42,5 +42,11 @@ async def handler5(msg: types.Message): await bot.send_message(msg.chat.id, "Hello from user= & chat_id=") + +@dp.message_handler(user=[user_id_to_test, 123]) # todo: add second id here +async def handler6(msg: types.Message): + print("Checked with list!") + + if __name__ == '__main__': executor.start_polling(dp) From 4f72f5cb0592d33a1d66c731f175d768a78ad546 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:36:13 +0300 Subject: [PATCH 36/50] Update docs --- docs/source/dispatcher/filters.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index d103ac36..e3149b8c 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -111,6 +111,14 @@ ExceptionsFilter :show-inheritance: +ExceptionsFilter +---------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== From 80b11684801446a4e457214738adde4be1018ccd Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 16:56:47 +0300 Subject: [PATCH 37/50] Fix docs and rename id-filter-example.py -> id_filter_example.py --- docs/source/dispatcher/filters.rst | 2 +- examples/{id-filter-example.py => id_filter_example.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/{id-filter-example.py => id_filter_example.py} (100%) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index e3149b8c..af03e163 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -111,7 +111,7 @@ ExceptionsFilter :show-inheritance: -ExceptionsFilter +IdFilter ---------------- .. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter diff --git a/examples/id-filter-example.py b/examples/id_filter_example.py similarity index 100% rename from examples/id-filter-example.py rename to examples/id_filter_example.py From 7d1c4e260cfd48da141958608e2f38afedd72b60 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 22 Jul 2019 17:10:03 +0300 Subject: [PATCH 38/50] Fix typo in pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 8b028693b6dfd932bef4b59e7af8e3dfe55b3e89 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 17:49:30 +0300 Subject: [PATCH 39/50] Remove user and chat arguments, update the example --- aiogram/dispatcher/filters/builtin.py | 8 ++------ examples/id_filter_example.py | 28 +++++++-------------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index d64fb97a..5615964c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -522,14 +522,10 @@ class IdFilter(Filter): @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: result = {} - if 'user' in full_config: - result['user_id'] = full_config.pop('user') - elif 'user_id' in full_config: + if 'user_id' in full_config: result['user_id'] = full_config.pop('user_id') - if 'chat' in full_config: - result['chat_id'] = full_config.pop('chat') - elif 'chat_id' in full_config: + if 'chat_id' in full_config: result['chat_id'] = full_config.pop('chat_id') return result diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py index b46ab056..64dc3b3f 100644 --- a/examples/id_filter_example.py +++ b/examples/id_filter_example.py @@ -9,43 +9,29 @@ user_id_to_test = None # todo: Set id here chat_id_to_test = user_id_to_test -@dp.message_handler(user=user_id_to_test) -async def handler1(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with user=") - raise SkipHandler - - @dp.message_handler(user_id=user_id_to_test) -async def handler2(msg: types.Message): +async def handler1(msg: types.Message): await bot.send_message(msg.chat.id, "Hello, checking with user_id=") raise SkipHandler -@dp.message_handler(chat=chat_id_to_test) -async def handler3(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with chat=") - raise SkipHandler - - @dp.message_handler(chat_id=chat_id_to_test) -async def handler4(msg: types.Message): +async def handler2(msg: types.Message): await bot.send_message(msg.chat.id, "Hello, checking with chat_id=") raise SkipHandler -@dp.message_handler(user=user_id_to_test, chat_id=chat_id_to_test) -async def handler5(msg: types.Message): +@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=[user_id_to_test, 123]) # todo: add second id here -async def handler6(msg: types.Message): - print("Checked with list!") +@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__': From ab9264e2c9033651e1dcd8a12ce2576bd1e5c67e Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 23 Jul 2019 00:43:14 +0300 Subject: [PATCH 40/50] Add tests which fall because empty string isn't a correct match string --- tests/test_filters.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index f68b7c44..da530910 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -7,7 +7,12 @@ from aiogram.types import Message, CallbackQuery, InlineQuery, Poll class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_prefix, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -52,7 +57,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_postfix, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -97,7 +107,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_string, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -142,7 +157,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), From 974f19a614242a1a61a5594813f881e9b82957b8 Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 23 Jul 2019 00:49:26 +0300 Subject: [PATCH 41/50] Fix empty match string in text filter --- aiogram/dispatcher/filters/builtin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9826c63b..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!") @@ -266,22 +266,22 @@ class Text(Filter): if self.ignore_case: text = text.lower() - if self.equals: + 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: + 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: + 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: + elif self.endswith is not None: self.endswith = str(self.endswith) if self.ignore_case: self.endswith = self.endswith.lower() From 8db663c21817c53d731a763c8b7152ca837678f4 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 28 Jul 2019 12:18:55 +0300 Subject: [PATCH 42/50] Fix filter docs The comments were in the wrong place --- aiogram/dispatcher/filters/filters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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]: """ From ddd92acc09067b383ea22bf850326afe0371cf47 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 29 Jul 2019 21:59:32 +0300 Subject: [PATCH 43/50] Wrapped function can be registered as handler --- aiogram/dispatcher/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 17b715d1..2a77d580 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -23,11 +23,11 @@ class CancelHandler(Exception): def _get_spec(func: callable): + wrapped_function = func while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ - spec = inspect.getfullargspec(func) - return spec, func + return spec, wrapped_function def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): From d2d49282f55544886a38a577d6692c0d7349115c Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 30 Jul 2019 12:25:42 +0300 Subject: [PATCH 44/50] make handler._get_spec return only specs --- aiogram/dispatcher/handler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 2a77d580..889dc8d6 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -23,11 +23,10 @@ class CancelHandler(Exception): def _get_spec(func: callable): - wrapped_function = func while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ spec = inspect.getfullargspec(func) - return spec, wrapped_function + 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] From 6ce617cfd2d92c604ba5b46efbe193675964d074 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:38:12 +0300 Subject: [PATCH 45/50] Add support for animated stickers --- aiogram/types/sticker.py | 1 + aiogram/types/sticker_set.py | 1 + 2 files changed, 2 insertions(+) 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) From cbaf826be1b0dd8f958c6e23b0dc928ede859ab6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:43:07 +0300 Subject: [PATCH 46/50] Add support for new chat permissions --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 36 ++++++++++++++++++++++++++++++- aiogram/types/__init__.py | 1 + aiogram/types/chat.py | 2 ++ aiogram/types/chat_member.py | 1 + aiogram/types/chat_permissions.py | 18 ++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 aiogram/types/chat_permissions.py diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 6c51b295..4867e7c9 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -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 e7fd5d6e..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 @@ -345,7 +346,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): 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) @@ -1016,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, @@ -1032,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 @@ -1049,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 @@ -1101,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/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..68746f0e 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() diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 12789462..3548f734 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -28,6 +28,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..0d93256f --- /dev/null +++ b/aiogram/types/chat_permissions.py @@ -0,0 +1,18 @@ +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() From 14f273702b136d3167b953eb83a1e7e0649f0264 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:43:25 +0300 Subject: [PATCH 47/50] Bump version --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c0a5bc26..155b2848 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) 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 4867e7c9..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 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 From 47bc628f2b653e2476d2ca7f52cd4b2e7fc36596 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:46:39 +0300 Subject: [PATCH 48/50] Remove obsolete code --- aiogram/types/chat_member.py | 24 +----- aiogram/types/message.py | 153 ----------------------------------- 2 files changed, 2 insertions(+), 175 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 3548f734..73222a31 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -32,17 +32,11 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() - def is_admin(self): - warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return self.is_chat_admin() - def is_chat_admin(self): - return ChatMemberStatus.is_admin(self.status) + return ChatMemberStatus.is_chat_admin(self.status) def is_chat_member(self): - return ChatMemberStatus.is_member(self.status) + return ChatMemberStatus.is_chat_member(self.status) def __int__(self): return self.user.id @@ -61,20 +55,6 @@ class ChatMemberStatus(helper.Helper): 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.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_admin(role) - - @classmethod - def is_member(cls, role): - warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_member` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_member(role) - @classmethod def is_chat_admin(cls, role): return role in [cls.ADMINISTRATOR, cls.CREATOR] 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, From 870e79d7f20f6935a6a374db5aa8fa886e58a796 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 20:03:58 +0300 Subject: [PATCH 49/50] Add new permissions argument to Chat.restrict method --- aiogram/types/chat.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 68746f0e..f5c521a5 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -204,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, @@ -218,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 @@ -234,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, From 57724c55fff0efb9a330719f06dc2ba23a1ab187 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 20:10:57 +0300 Subject: [PATCH 50/50] Add initializer for ChatPermissions object (for IDE) --- aiogram/types/chat_permissions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py index 0d93256f..9d44653e 100644 --- a/aiogram/types/chat_permissions.py +++ b/aiogram/types/chat_permissions.py @@ -16,3 +16,24 @@ class ChatPermissions(base.TelegramObject): 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, + )