Merge remote-tracking branch 'origin/dev-3.x' into dev-3.x

# Conflicts:
#	README.md
#	README.rst
#	aiogram/__init__.py
#	aiogram/bot/bot.py
#	aiogram/contrib/fsm_storage/redis.py
#	aiogram/contrib/middlewares/logging.py
#	aiogram/dispatcher/dispatcher.py
#	aiogram/dispatcher/filters/__init__.py
#	aiogram/dispatcher/filters/builtin.py
#	aiogram/dispatcher/filters/filters.py
#	aiogram/dispatcher/filters/state.py
#	aiogram/dispatcher/handler.py
#	aiogram/dispatcher/webhook.py
#	aiogram/types/base.py
#	aiogram/types/chat.py
#	aiogram/types/chat_member.py
#	aiogram/types/input_media.py
#	aiogram/types/message.py
#	aiogram/utils/callback_data.py
#	aiogram/utils/deprecated.py
#	aiogram/utils/exceptions.py
#	aiogram/utils/executor.py
#	aiogram/utils/helper.py
#	aiogram/utils/json.py
#	aiogram/utils/mixins.py
#	aiogram/utils/payload.py
#	dev_requirements.txt
#	docs/source/index.rst
#	examples/callback_data_factory.py
#	examples/check_user_language.py
#	examples/echo_bot.py
#	examples/finite_state_machine_example.py
#	examples/i18n_example.py
#	examples/inline_bot.py
#	examples/media_group.py
#	examples/middleware_and_antiflood.py
#	examples/payments.py
#	examples/proxy_and_emojize.py
#	examples/regexp_commands_filter_example.py
#	examples/throtling_example.py
#	examples/webhook_example.py
#	examples/webhook_example_2.py
#	setup.py
#	tests/test_bot.py
#	tests/test_token.py
#	tests/types/dataset.py
This commit is contained in:
Alex Root Junior 2019-11-03 22:19:44 +02:00
commit 87393f2475
98 changed files with 5048 additions and 4854 deletions

10
.github/FUNDING.yml vendored
View file

@ -1,8 +1,2 @@
# These are supported funding model platforms
github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: aiogram # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL
github: [JRootJunior]
open_collective: aiogram

View file

@ -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)

View file

@ -1,17 +1,17 @@
# AIOGram
[![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)
[![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)
[![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/python/black)
[![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/aiogram?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)
**aiogram** is a pretty simple and fully asynchronous library for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
**aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
@ -25,3 +25,33 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
- Source: [Github repo](https://github.com/aiogram/aiogram)
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)].
<a href="https://github.com/aiogram/aiogram/graphs/contributors"><img src="https://opencollective.com/aiogram/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)]
#### Individuals
<a href="https://opencollective.com/aiogram"><img src="https://opencollective.com/aiogram/individuals.svg?width=890"></a>
#### 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)]
<a href="https://opencollective.com/aiogram/organization/0/website"><img src="https://opencollective.com/aiogram/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/1/website"><img src="https://opencollective.com/aiogram/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/2/website"><img src="https://opencollective.com/aiogram/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/3/website"><img src="https://opencollective.com/aiogram/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/4/website"><img src="https://opencollective.com/aiogram/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/5/website"><img src="https://opencollective.com/aiogram/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/6/website"><img src="https://opencollective.com/aiogram/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/7/website"><img src="https://opencollective.com/aiogram/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/8/website"><img src="https://opencollective.com/aiogram/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/9/website"><img src="https://opencollective.com/aiogram/organization/9/avatar.svg"></a>

View file

@ -21,18 +21,14 @@ 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
.. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest?style=flat-square
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square
:target: https://github.com/python/black
:alt: Code style: Black
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
:target: https://github.com/aiogram/aiogram/issues
:alt: Github issues
@ -42,7 +38,7 @@ AIOGramBot
:alt: MIT License
**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.7 with `asyncio <https://docs.python.org/3/library/asyncio.html>`_ and `aiohttp <https://github.com/aio-libs/aiohttp>`_. It helps you to make your bots faster and simpler.
**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.7 with `asyncio <https://docs.python.org/3/library/asyncio.html>`_ and `aiohttp <https://github.com/aio-libs/aiohttp>`_. It helps you to make your bots faster and simpler.
You can `read the docs here <http://aiogram.readthedocs.io/en/latest/>`_.

View file

@ -17,26 +17,26 @@ try:
except ImportError:
uvloop = None
else:
if "DISABLE_UVLOOP" not in os.environ:
if 'DISABLE_UVLOOP' not in os.environ:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
__all__ = [
"Bot",
"Dispatcher",
"__api_version__",
"__version__",
"bot",
"contrib",
"dispatcher",
"exceptions",
"executor",
"filters",
"helper",
"md",
"middlewares",
"types",
"utils",
'Bot',
'Dispatcher',
'__api_version__',
'__version__',
'bot',
'contrib',
'dispatcher',
'exceptions',
'executor',
'filters',
'helper',
'md',
'middlewares',
'types',
'utils'
]
__version__ = "3.0.dev1"
__api_version__ = "4.3"
__version__ = '2.4'
__api_version__ = '4.4'

View file

@ -155,7 +155,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
@ -191,6 +191,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

View file

@ -13,7 +13,7 @@ from aiohttp.helpers import sentinel
from . import api
from ..types import ParseMode, base
from ..utils import json
from ..utils.auth_widget import check_token
from ..utils.auth_widget import check_integrity
class BaseBot:
@ -115,6 +115,15 @@ class BaseBot:
self.parse_mode = parse_mode
def __del__(self):
if not hasattr(self, 'loop'):
return
if self.loop.is_running():
self.loop.create_task(self.close())
return
loop = asyncio.new_event_loop()
loop.run_until_complete(self.close())
@staticmethod
def _prepare_timeout(
value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]
@ -302,4 +311,4 @@ class BaseBot:
self.parse_mode = None
def check_auth_widget(self, data):
return check_token(data, self.__token)
return check_integrity(self.__token, data)

File diff suppressed because it is too large Load diff

View file

@ -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):

View file

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

View file

@ -11,9 +11,9 @@ import aioredis
from ...dispatcher.storage import BaseStorage
from ...utils import json
STATE_KEY = "state"
STATE_DATA_KEY = "data"
STATE_BUCKET_KEY = "bucket"
STATE_KEY = 'state'
STATE_DATA_KEY = 'data'
STATE_BUCKET_KEY = 'bucket'
class RedisStorage(BaseStorage):
@ -35,10 +35,7 @@ class RedisStorage(BaseStorage):
await dp.storage.wait_closed()
"""
def __init__(
self, host="localhost", port=6379, db=None, password=None, ssl=None, loop=None, **kwargs
):
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
self._host = host
self._port = port
self._db = db
@ -64,28 +61,19 @@ 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:
if self._redis is None:
self._redis = await aioredis.create_connection(
(self._host, self._port),
db=self._db,
password=self._password,
ssl=self._ssl,
loop=self._loop,
**self._kwargs,
)
self._redis = await aioredis.create_connection((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
loop=self._loop,
**self._kwargs)
return self._redis
async def get_record(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
) -> typing.Dict:
async def get_record(self, *,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None) -> typing.Dict:
"""
Get record from storage
@ -97,20 +85,13 @@ class RedisStorage(BaseStorage):
addr = f"fsm:{chat}:{user}"
conn = await self.redis()
data = await conn.execute("GET", addr)
data = await conn.execute('GET', addr)
if data is None:
return {"state": None, "data": {}}
return {'state': None, 'data': {}}
return json.loads(data)
async def set_record(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
state=None,
data=None,
bucket=None,
):
async def set_record(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
state=None, data=None, bucket=None):
"""
Write record to storage
@ -129,65 +110,39 @@ class RedisStorage(BaseStorage):
chat, user = self.check_address(chat=chat, user=user)
addr = f"fsm:{chat}:{user}"
record = {"state": state, "data": data, "bucket": bucket}
record = {'state': state, 'data': data, 'bucket': bucket}
conn = await self.redis()
await conn.execute("SET", addr, json.dumps(record))
await conn.execute('SET', addr, json.dumps(record))
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Optional[str]:
record = await self.get_record(chat=chat, user=user)
return record["state"]
return record['state']
async def get_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Dict:
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Dict:
record = await self.get_record(chat=chat, user=user)
return record["data"]
return record['data']
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,
):
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):
record = await self.get_record(chat=chat, user=user)
await self.set_record(chat=chat, user=user, state=state, data=record["data"])
await self.set_record(chat=chat, user=user, state=state, data=record['data'])
async def set_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
):
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):
record = await self.get_record(chat=chat, user=user)
await self.set_record(chat=chat, user=user, state=record["state"], data=data)
await self.set_record(chat=chat, user=user, state=record['state'], data=data)
async def update_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
**kwargs,
):
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
if data is None:
data = {}
record = await self.get_record(chat=chat, user=user)
record_data = record.get("data", {})
record_data = record.get('data', {})
record_data.update(data, **kwargs)
await self.set_record(chat=chat, user=user, state=record["state"], data=record_data)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_data)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
"""
@ -198,9 +153,9 @@ class RedisStorage(BaseStorage):
conn = await self.redis()
result = []
keys = await conn.execute("KEYS", "fsm:*")
keys = await conn.execute('KEYS', 'fsm:*')
for item in keys:
*_, chat, user = item.decode("utf-8").split(":")
*_, chat, user = item.decode('utf-8').split(':')
result.append((chat, user))
return result
@ -215,52 +170,33 @@ class RedisStorage(BaseStorage):
conn = await self.redis()
if full:
await conn.execute("FLUSHDB")
await conn.execute('FLUSHDB')
else:
keys = await conn.execute("KEYS", "fsm:*")
await conn.execute("DEL", *keys)
keys = await conn.execute('KEYS', 'fsm:*')
await conn.execute('DEL', *keys)
def has_bucket(self):
return True
async def get_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Dict:
async def get_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Dict:
record = await self.get_record(chat=chat, user=user)
return record.get("bucket", {})
return record.get('bucket', {})
async def set_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
):
async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None):
record = await self.get_record(chat=chat, user=user)
await self.set_record(
chat=chat, user=user, state=record["state"], data=record["data"], bucket=bucket
)
await self.set_record(chat=chat, user=user, state=record['state'], data=record['data'], bucket=bucket)
async def update_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
**kwargs,
):
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None, **kwargs):
record = await self.get_record(chat=chat, user=user)
record_bucket = record.get("bucket", {})
record_bucket = record.get('bucket', {})
if bucket is None:
bucket = {}
record_bucket.update(bucket, **kwargs)
await self.set_record(
chat=chat, user=user, state=record["state"], data=record_bucket, bucket=bucket
)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_bucket, bucket=bucket)
class RedisStorage2(BaseStorage):
@ -283,19 +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
@ -306,32 +235,28 @@ class RedisStorage2(BaseStorage):
self._kwargs = kwargs
self._prefix = (prefix,)
self._state_ttl = state_ttl
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
self._redis: aioredis.RedisConnection = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def redis(self) -> aioredis.Redis:
"""
Get Redis connection
This property is awaitable.
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
if self._redis is None:
self._redis = await aioredis.create_redis_pool(
(self._host, self._port),
db=self._db,
password=self._password,
ssl=self._ssl,
minsize=1,
maxsize=self._pool_size,
loop=self._loop,
**self._kwargs,
)
self._redis = await aioredis.create_redis_pool((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
minsize=1, maxsize=self._pool_size,
loop=self._loop, **self._kwargs)
return self._redis
def generate_key(self, *parts):
return ":".join(self._prefix + tuple(map(str, parts)))
return ':'.join(self._prefix + tuple(map(str, parts)))
async def close(self):
async with self._connection_lock:
@ -346,68 +271,42 @@ class RedisStorage2(BaseStorage):
return await self._redis.wait_closed()
return True
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Optional[str]:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_KEY)
redis = await self.redis()
return await redis.get(key, encoding="utf8") or None
return await redis.get(key, encoding='utf8') or None
async def get_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None,
) -> typing.Dict:
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None) -> typing.Dict:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
raw_result = await redis.get(key, encoding="utf8")
raw_result = await redis.get(key, encoding='utf8')
if raw_result:
return json.loads(raw_result)
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,
):
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):
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=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,
):
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
await redis.set(key, json.dumps(data))
await redis.set(key, json.dumps(data), expire=self._data_ttl)
async def update_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
**kwargs,
):
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
if data is None:
data = {}
temp_data = await self.get_data(chat=chat, user=user, default={})
@ -417,46 +316,31 @@ class RedisStorage2(BaseStorage):
def has_bucket(self):
return True
async def get_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None,
) -> typing.Dict:
async def get_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None) -> typing.Dict:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
redis = await self.redis()
raw_result = await redis.get(key, encoding="utf8")
raw_result = await redis.get(key, encoding='utf8')
if raw_result:
return json.loads(raw_result)
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,
):
async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
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))
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
async def update_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
**kwargs,
):
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None, **kwargs):
if bucket is None:
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
await self.set_bucket(chat=chat, user=user, data=temp_bucket)
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
@ -470,7 +354,7 @@ class RedisStorage2(BaseStorage):
if full:
await conn.flushdb()
else:
keys = await conn.keys(self.generate_key("*"))
keys = await conn.keys(self.generate_key('*'))
await conn.delete(*keys)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
@ -482,9 +366,9 @@ class RedisStorage2(BaseStorage):
conn = await self.redis()
result = []
keys = await conn.keys(self.generate_key("*", "*", STATE_KEY), encoding="utf8")
keys = await conn.keys(self.generate_key('*', '*', STATE_KEY), encoding='utf8')
for item in keys:
*_, chat, user, _ = item.split(":")
*_, chat, user, _ = item.split(':')
result.append((chat, user))
return result
@ -506,7 +390,7 @@ async def migrate_redis1_to_redis2(storage1: RedisStorage, storage2: RedisStorag
if not isinstance(storage2, RedisStorage):
raise TypeError(f"{type(storage2)} is not RedisStorage instance.")
log = logging.getLogger("aiogram.RedisStorage")
log = logging.getLogger('aiogram.RedisStorage')
for chat, user in await storage1.get_states_list():
state = await storage1.get_state(chat=chat, user=user)

View file

@ -97,17 +97,15 @@ class I18nMiddleware(BaseMiddleware):
if locale not in self.locales:
if n is 1:
return singular
else:
return plural
return plural
translator = self.locales[locale]
if plural is None:
return translator.gettext(singular)
else:
return translator.ngettext(singular, plural, n)
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=False) -> LazyProxy:
"""
Lazy get text
@ -115,9 +113,10 @@ class I18nMiddleware(BaseMiddleware):
:param plural:
:param n:
:param locale:
:param enable_cache:
:return:
"""
return LazyProxy(self.gettext, singular, plural, n, locale)
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:

View file

@ -5,7 +5,7 @@ import logging
from aiogram import types
from aiogram.dispatcher.middlewares import BaseMiddleware
HANDLED_STR = ["Unhandled", "Handled"]
HANDLED_STR = ['Unhandled', 'Handled']
class LoggingMiddleware(BaseMiddleware):
@ -18,181 +18,128 @@ class LoggingMiddleware(BaseMiddleware):
super(LoggingMiddleware, self).__init__()
def check_timeout(self, obj):
start = obj.conf.get("_start", None)
start = obj.conf.get('_start', None)
if start:
del obj.conf["_start"]
del obj.conf['_start']
return round((time.time() - start) * 1000)
return -1
async def on_pre_process_update(self, update: types.Update, data: dict):
update.conf["_start"] = time.time()
update.conf['_start'] = time.time()
self.logger.debug(f"Received update [ID:{update.update_id}]")
async def on_post_process_update(self, update: types.Update, result, data: dict):
timeout = self.check_timeout(update)
if timeout > 0:
self.logger.info(
f"Process update [ID:{update.update_id}]: [success] (in {timeout} ms)"
)
self.logger.info(f"Process update [ID:{update.update_id}]: [success] (in {timeout} ms)")
async def on_pre_process_message(self, message: types.Message, data: dict):
self.logger.info(
f"Received message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]"
)
self.logger.info(f"Received message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]")
async def on_post_process_message(self, message: types.Message, results, data: dict):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]"
)
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]")
async def on_pre_process_edited_message(self, edited_message, data: dict):
self.logger.info(
f"Received edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]"
)
self.logger.info(f"Received edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]")
async def on_post_process_edited_message(self, edited_message, results, data: dict):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]"
)
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]")
async def on_pre_process_channel_post(self, channel_post: types.Message, data: dict):
self.logger.info(
f"Received channel post [ID:{channel_post.message_id}] "
f"in channel [ID:{channel_post.chat.id}]"
)
self.logger.info(f"Received channel post [ID:{channel_post.message_id}] "
f"in channel [ID:{channel_post.chat.id}]")
async def on_post_process_channel_post(self, channel_post: types.Message, results, data: dict):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"channel post [ID:{channel_post.message_id}] "
f"in chat [{channel_post.chat.type}:{channel_post.chat.id}]"
)
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"channel post [ID:{channel_post.message_id}] "
f"in chat [{channel_post.chat.type}:{channel_post.chat.id}]")
async def on_pre_process_edited_channel_post(
self, edited_channel_post: types.Message, data: dict
):
self.logger.info(
f"Received edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]"
)
async def on_pre_process_edited_channel_post(self, edited_channel_post: types.Message, data: dict):
self.logger.info(f"Received edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]")
async def on_post_process_edited_channel_post(
self, edited_channel_post: types.Message, results, data: dict
):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]"
)
async def on_post_process_edited_channel_post(self, edited_channel_post: types.Message, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]")
async def on_pre_process_inline_query(self, inline_query: types.InlineQuery, data: dict):
self.logger.info(
f"Received inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]"
)
self.logger.info(f"Received inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]")
async def on_post_process_inline_query(
self, inline_query: types.InlineQuery, results, data: dict
):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]"
)
async def on_post_process_inline_query(self, inline_query: types.InlineQuery, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]")
async def on_pre_process_chosen_inline_result(
self, chosen_inline_result: types.ChosenInlineResult, data: dict
):
self.logger.info(
f"Received chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]"
)
async def on_pre_process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult, data: dict):
self.logger.info(f"Received chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]")
async def on_post_process_chosen_inline_result(
self, chosen_inline_result, results, data: dict
):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]"
)
async def on_post_process_chosen_inline_result(self, chosen_inline_result, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]")
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
if callback_query.message:
text = (f"Received callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
self.logger.info(
f"Received callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
f"from user [ID:{callback_query.message.from_user.id}]"
)
else:
self.logger.info(
f"Received callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]"
)
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.info(
f"Received callback query [ID:{callback_query.id}] "
f"from inline message [ID:{callback_query.inline_message_id}] "
f"from user [ID:{callback_query.from_user.id}]"
)
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for inline message [ID:{callback_query.inline_message_id}] ")
async def on_post_process_callback_query(self, callback_query, results, data: dict):
if callback_query.message:
text = (f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
f"from user [ID:{callback_query.message.from_user.id}]"
)
else:
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]"
)
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from inline message [ID:{callback_query.inline_message_id}] "
f"from user [ID:{callback_query.from_user.id}]"
)
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"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}] "
f"from user [ID:{shipping_query.from_user.id}]"
)
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
f"from user [ID:{shipping_query.from_user.id}]")
async def on_post_process_shipping_query(self, shipping_query, results, data: dict):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"shipping query [ID:{shipping_query.id}] "
f"from user [ID:{shipping_query.from_user.id}]"
)
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"shipping query [ID:{shipping_query.id}] "
f"from user [ID:{shipping_query.from_user.id}]")
async def on_pre_process_pre_checkout_query(
self, pre_checkout_query: types.PreCheckoutQuery, data: dict
):
self.logger.info(
f"Received pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]"
)
async def on_pre_process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery, data: dict):
self.logger.info(f"Received pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]")
async def on_post_process_pre_checkout_query(self, pre_checkout_query, results, data: dict):
self.logger.debug(
f"{HANDLED_STR[bool(len(results))]} "
f"pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]"
)
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]")
async def on_pre_process_error(self, update, error, data: dict):
timeout = self.check_timeout(update)
@ -226,7 +173,7 @@ class LoggingFilter(logging.Filter):
"""
def __init__(self, name="", prefix="tg", include_content=False):
def __init__(self, name='', prefix='tg', include_content=False):
"""
:param name:
:param prefix: prefix for all records
@ -258,34 +205,34 @@ class LoggingFilter(logging.Filter):
:param update:
:return:
"""
yield "update_id", update.update_id
yield 'update_id', update.update_id
if update.message:
yield "update_type", "message"
yield 'update_type', 'message'
yield from self.process_message(update.message)
if update.edited_message:
yield "update_type", "edited_message"
yield 'update_type', 'edited_message'
yield from self.process_message(update.edited_message)
if update.channel_post:
yield "update_type", "channel_post"
yield 'update_type', 'channel_post'
yield from self.process_message(update.channel_post)
if update.edited_channel_post:
yield "update_type", "edited_channel_post"
yield 'update_type', 'edited_channel_post'
yield from self.process_message(update.edited_channel_post)
if update.inline_query:
yield "update_type", "inline_query"
yield 'update_type', 'inline_query'
yield from self.process_inline_query(update.inline_query)
if update.chosen_inline_result:
yield "update_type", "chosen_inline_result"
yield 'update_type', 'chosen_inline_result'
yield from self.process_chosen_inline_result(update.chosen_inline_result)
if update.callback_query:
yield "update_type", "callback_query"
yield 'update_type', 'callback_query'
yield from self.process_callback_query(update.callback_query)
if update.shipping_query:
yield "update_type", "shipping_query"
yield 'update_type', 'shipping_query'
yield from self.process_shipping_query(update.shipping_query)
if update.pre_checkout_query:
yield "update_type", "pre_checkout_query"
yield 'update_type', 'pre_checkout_query'
yield from self.process_pre_checkout_query(update.pre_checkout_query)
def make_prefix(self, prefix, iterable):
@ -312,11 +259,11 @@ class LoggingFilter(logging.Filter):
if not user:
return
yield "user_id", user.id
yield 'user_id', user.id
if self.include_content:
yield "user_full_name", user.full_name
yield 'user_full_name', user.full_name
if user.username:
yield "user_name", f"@{user.username}"
yield 'user_name', f"@{user.username}"
def process_chat(self, chat: types.Chat):
"""
@ -328,15 +275,15 @@ class LoggingFilter(logging.Filter):
if not chat:
return
yield "chat_id", chat.id
yield "chat_type", chat.type
yield 'chat_id', chat.id
yield 'chat_type', chat.type
if self.include_content:
yield "chat_title", chat.full_name
yield 'chat_title', chat.full_name
if chat.username:
yield "chat_name", f"@{chat.username}"
yield 'chat_name', f"@{chat.username}"
def process_message(self, message: types.Message):
yield "message_content_type", message.content_type
yield 'message_content_type', message.content_type
yield from self.process_user(message.from_user)
yield from self.process_chat(message.chat)
@ -344,84 +291,82 @@ class LoggingFilter(logging.Filter):
return
if message.reply_to_message:
yield from self.make_prefix("reply_to", self.process_message(message.reply_to_message))
yield from self.make_prefix('reply_to', self.process_message(message.reply_to_message))
if message.forward_from:
yield from self.make_prefix("forward_from", self.process_user(message.forward_from))
yield from self.make_prefix('forward_from', self.process_user(message.forward_from))
if message.forward_from_chat:
yield from self.make_prefix(
"forward_from_chat", self.process_chat(message.forward_from_chat)
)
yield from self.make_prefix('forward_from_chat', self.process_chat(message.forward_from_chat))
if message.forward_from_message_id:
yield "message_forward_from_message_id", message.forward_from_message_id
yield 'message_forward_from_message_id', message.forward_from_message_id
if message.forward_date:
yield "message_forward_date", message.forward_date
yield 'message_forward_date', message.forward_date
if message.edit_date:
yield "message_edit_date", message.edit_date
yield 'message_edit_date', message.edit_date
if message.media_group_id:
yield "message_media_group_id", message.media_group_id
yield 'message_media_group_id', message.media_group_id
if message.author_signature:
yield "message_author_signature", message.author_signature
yield 'message_author_signature', message.author_signature
if message.text:
yield "text", message.text or message.caption
yield "html_text", message.html_text
yield 'text', message.text or message.caption
yield 'html_text', message.html_text
elif message.audio:
yield "audio", message.audio.file_id
yield 'audio', message.audio.file_id
elif message.animation:
yield "animation", message.animation.file_id
yield 'animation', message.animation.file_id
elif message.document:
yield "document", message.document.file_id
yield 'document', message.document.file_id
elif message.game:
yield "game", message.game.title
yield 'game', message.game.title
elif message.photo:
yield "photo", message.photo[-1].file_id
yield 'photo', message.photo[-1].file_id
elif message.sticker:
yield "sticker", message.sticker.file_id
yield 'sticker', message.sticker.file_id
elif message.video:
yield "video", message.video.file_id
yield 'video', message.video.file_id
elif message.video_note:
yield "video_note", message.video_note.file_id
yield 'video_note', message.video_note.file_id
elif message.voice:
yield "voice", message.voice.file_id
yield 'voice', message.voice.file_id
elif message.contact:
yield "contact_full_name", message.contact.full_name
yield "contact_phone_number", message.contact.phone_number
yield 'contact_full_name', message.contact.full_name
yield 'contact_phone_number', message.contact.phone_number
elif message.venue:
yield "venue_address", message.venue.address
yield "location_latitude", message.venue.location.latitude
yield "location_longitude", message.venue.location.longitude
yield 'venue_address', message.venue.address
yield 'location_latitude', message.venue.location.latitude
yield 'location_longitude', message.venue.location.longitude
elif message.location:
yield "location_latitude", message.location.latitude
yield "location_longitude", message.location.longitude
yield 'location_latitude', message.location.latitude
yield 'location_longitude', message.location.longitude
elif message.new_chat_members:
yield "new_chat_members", [user.id for user in message.new_chat_members]
yield 'new_chat_members', [user.id for user in message.new_chat_members]
elif message.left_chat_member:
yield "left_chat_member", [user.id for user in message.new_chat_members]
yield 'left_chat_member', [user.id for user in message.new_chat_members]
elif message.invoice:
yield "invoice_title", message.invoice.title
yield "invoice_description", message.invoice.description
yield "invoice_start_parameter", message.invoice.start_parameter
yield "invoice_currency", message.invoice.currency
yield "invoice_total_amount", message.invoice.total_amount
yield 'invoice_title', message.invoice.title
yield 'invoice_description', message.invoice.description
yield 'invoice_start_parameter', message.invoice.start_parameter
yield 'invoice_currency', message.invoice.currency
yield 'invoice_total_amount', message.invoice.total_amount
elif message.successful_payment:
yield "successful_payment_currency", message.successful_payment.currency
yield "successful_payment_total_amount", message.successful_payment.total_amount
yield "successful_payment_invoice_payload", message.successful_payment.invoice_payload
yield "successful_payment_shipping_option_id", message.successful_payment.shipping_option_id
yield "successful_payment_telegram_payment_charge_id", message.successful_payment.telegram_payment_charge_id
yield "successful_payment_provider_payment_charge_id", message.successful_payment.provider_payment_charge_id
yield 'successful_payment_currency', message.successful_payment.currency
yield 'successful_payment_total_amount', message.successful_payment.total_amount
yield 'successful_payment_invoice_payload', message.successful_payment.invoice_payload
yield 'successful_payment_shipping_option_id', message.successful_payment.shipping_option_id
yield 'successful_payment_telegram_payment_charge_id', message.successful_payment.telegram_payment_charge_id
yield 'successful_payment_provider_payment_charge_id', message.successful_payment.provider_payment_charge_id
elif message.connected_website:
yield "connected_website", message.connected_website
yield 'connected_website', message.connected_website
elif message.migrate_from_chat_id:
yield "migrate_from_chat_id", message.migrate_from_chat_id
yield 'migrate_from_chat_id', message.migrate_from_chat_id
elif message.migrate_to_chat_id:
yield "migrate_to_chat_id", message.migrate_to_chat_id
yield 'migrate_to_chat_id', message.migrate_to_chat_id
elif message.pinned_message:
yield from self.make_prefix("pinned_message", message.pinned_message)
yield from self.make_prefix('pinned_message', message.pinned_message)
elif message.new_chat_title:
yield "new_chat_title", message.new_chat_title
yield 'new_chat_title', message.new_chat_title
elif message.new_chat_photo:
yield "new_chat_photo", message.new_chat_photo[-1].file_id
yield 'new_chat_photo', message.new_chat_photo[-1].file_id
# elif message.delete_chat_photo:
# yield 'delete_chat_photo', message.delete_chat_photo
# elif message.group_chat_created:
@ -430,55 +375,53 @@ class LoggingFilter(logging.Filter):
# yield 'passport_data', message.passport_data
def process_inline_query(self, inline_query: types.InlineQuery):
yield "inline_query_id", inline_query.id
yield 'inline_query_id', inline_query.id
yield from self.process_user(inline_query.from_user)
if self.include_content:
yield "inline_query_text", inline_query.query
yield 'inline_query_text', inline_query.query
if inline_query.location:
yield "location_latitude", inline_query.location.latitude
yield "location_longitude", inline_query.location.longitude
yield 'location_latitude', inline_query.location.latitude
yield 'location_longitude', inline_query.location.longitude
if inline_query.offset:
yield "inline_query_offset", inline_query.offset
yield 'inline_query_offset', inline_query.offset
def process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult):
yield "chosen_inline_result_id", chosen_inline_result.result_id
yield 'chosen_inline_result_id', chosen_inline_result.result_id
yield from self.process_user(chosen_inline_result.from_user)
if self.include_content:
yield "inline_query_text", chosen_inline_result.query
yield 'inline_query_text', chosen_inline_result.query
if chosen_inline_result.location:
yield "location_latitude", chosen_inline_result.location.latitude
yield "location_longitude", chosen_inline_result.location.longitude
yield 'location_latitude', chosen_inline_result.location.latitude
yield 'location_longitude', chosen_inline_result.location.longitude
def process_callback_query(self, callback_query: types.CallbackQuery):
yield from self.process_user(callback_query.from_user)
yield "callback_query_data", callback_query.data
yield 'callback_query_data', callback_query.data
if callback_query.message:
yield from self.make_prefix(
"callback_query_message", self.process_message(callback_query.message)
)
yield from self.make_prefix('callback_query_message', self.process_message(callback_query.message))
if callback_query.inline_message_id:
yield "callback_query_inline_message_id", callback_query.inline_message_id
yield 'callback_query_inline_message_id', callback_query.inline_message_id
if callback_query.chat_instance:
yield "callback_query_chat_instance", callback_query.chat_instance
yield 'callback_query_chat_instance', callback_query.chat_instance
if callback_query.game_short_name:
yield "callback_query_game_short_name", callback_query.game_short_name
yield 'callback_query_game_short_name', callback_query.game_short_name
def process_shipping_query(self, shipping_query: types.ShippingQuery):
yield "shipping_query_id", shipping_query.id
yield 'shipping_query_id', shipping_query.id
yield from self.process_user(shipping_query.from_user)
if self.include_content:
yield "shipping_query_invoice_payload", shipping_query.invoice_payload
yield 'shipping_query_invoice_payload', shipping_query.invoice_payload
def process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery):
yield "pre_checkout_query_id", pre_checkout_query.id
yield 'pre_checkout_query_id', pre_checkout_query.id
yield from self.process_user(pre_checkout_query.from_user)
if self.include_content:
yield "pre_checkout_query_currency", pre_checkout_query.currency
yield "pre_checkout_query_total_amount", pre_checkout_query.total_amount
yield "pre_checkout_query_invoice_payload", pre_checkout_query.invoice_payload
yield "pre_checkout_query_shipping_option_id", pre_checkout_query.shipping_option_id
yield 'pre_checkout_query_currency', pre_checkout_query.currency
yield 'pre_checkout_query_total_amount', pre_checkout_query.total_amount
yield 'pre_checkout_query_invoice_payload', pre_checkout_query.invoice_payload
yield 'pre_checkout_query_shipping_option_id', pre_checkout_query.shipping_option_id

File diff suppressed because it is too large Load diff

View file

@ -1,51 +1,34 @@
from .builtin import (
Command,
CommandHelp,
CommandPrivacy,
CommandSettings,
CommandStart,
ContentTypeFilter,
ExceptionsFilter,
HashTag,
Regexp,
RegexpCommandsFilter,
StateFilter,
Text,
)
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
Text, IDFilter, AdminFilter, IsReplyFilter
from .factory import FiltersFactory
from .filters import (
AbstractFilter,
BoundFilter,
Filter,
FilterNotPassed,
FilterRecord,
execute_filter,
check_filters,
get_filter_spec,
get_filters_spec,
)
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
__all__ = [
"AbstractFilter",
"BoundFilter",
"Command",
"CommandStart",
"CommandHelp",
"CommandPrivacy",
"CommandSettings",
"ContentTypeFilter",
"ExceptionsFilter",
"HashTag",
"Filter",
"FilterNotPassed",
"FilterRecord",
"FiltersFactory",
"RegexpCommandsFilter",
"Regexp",
"StateFilter",
"Text",
"get_filter_spec",
"get_filters_spec",
"execute_filter",
"check_filters",
'AbstractFilter',
'BoundFilter',
'Command',
'CommandStart',
'CommandHelp',
'CommandPrivacy',
'CommandSettings',
'ContentTypeFilter',
'ExceptionsFilter',
'HashTag',
'Filter',
'FilterNotPassed',
'FilterRecord',
'FiltersFactory',
'RegexpCommandsFilter',
'Regexp',
'StateFilter',
'Text',
'IDFilter',
'IsReplyFilter',
'AdminFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
'check_filters',
]

View file

@ -9,7 +9,7 @@ from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
class Command(Filter):
@ -21,13 +21,10 @@ class Command(Filter):
By default this filter is registered for messages and edited messages handlers.
"""
def __init__(
self,
commands: Union[Iterable, str],
prefixes: Union[Iterable, str] = "/",
ignore_case: bool = True,
ignore_mention: bool = False,
):
def __init__(self, commands: Union[Iterable, str],
prefixes: Union[Iterable, str] = '/',
ignore_case: bool = True,
ignore_mention: bool = False):
"""
Filter can be initialized from filters factory or by simply creating instance of this class.
@ -69,38 +66,33 @@ class Command(Filter):
:return: config or empty dict
"""
config = {}
if "commands" in full_config:
config["commands"] = full_config.pop("commands")
if config and "commands_prefix" in full_config:
config["prefixes"] = full_config.pop("commands_prefix")
if config and "commands_ignore_mention" in full_config:
config["ignore_mention"] = full_config.pop("commands_ignore_mention")
if 'commands' in full_config:
config['commands'] = full_config.pop('commands')
if config and 'commands_prefix' in full_config:
config['prefixes'] = full_config.pop('commands_prefix')
if config and 'commands_ignore_mention' in full_config:
config['ignore_mention'] = full_config.pop('commands_ignore_mention')
return config
async def check(self, message: types.Message):
return await self.check_command(
message, self.commands, self.prefixes, self.ignore_case, self.ignore_mention
)
return await self.check_command(message, self.commands, self.prefixes, self.ignore_case, self.ignore_mention)
@staticmethod
async def check_command(
message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False
):
async def check_command(message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False):
if not message.text: # Prevent to use with non-text content types
return False
full_command = message.text.split()[0]
prefix, (command, _, mention) = (full_command[0], full_command[1:].partition("@"))
prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@')
if (
not ignore_mention
and mention
and (await message.bot.me).username.lower() != mention.lower()
):
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
return False
elif prefix not in prefixes:
if prefix not in prefixes:
return False
elif (command.lower() if ignore_case else command) not in commands:
if (command.lower() if ignore_case else command) not in commands:
return False
return {"command": Command.CommandObj(command=command, prefix=prefix, mention=mention)}
return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)}
@dataclass
class CommandObj:
@ -111,9 +103,9 @@ class Command(Filter):
"""
"""Command prefix"""
prefix: str = "/"
prefix: str = '/'
"""Command without prefix and mention"""
command: str = ""
command: str = ''
"""Mention (if available)"""
mention: str = None
"""Command argument"""
@ -137,9 +129,9 @@ class Command(Filter):
"""
line = self.prefix + self.command
if self.mentioned:
line += "@" + self.mention
line += '@' + self.mention
if self.args:
line += " " + self.args
line += ' ' + self.args
return line
@ -160,7 +152,7 @@ class CommandStart(Command):
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
"""
super(CommandStart, self).__init__(["start"])
super().__init__(['start'])
self.deep_link = deep_link
async def check(self, message: types.Message):
@ -170,7 +162,7 @@ class CommandStart(Command):
:param message:
:return:
"""
check = await super(CommandStart, self).check(message)
check = await super().check(message)
if check and self.deep_link is not None:
if not isinstance(self.deep_link, re.Pattern):
@ -178,7 +170,7 @@ class CommandStart(Command):
match = self.deep_link.match(message.get_args())
if match:
return {"deep_link": match}
return {'deep_link': match}
return False
return check
@ -190,7 +182,7 @@ class CommandHelp(Command):
"""
def __init__(self):
super(CommandHelp, self).__init__(["help"])
super().__init__(['help'])
class CommandSettings(Command):
@ -199,7 +191,7 @@ class CommandSettings(Command):
"""
def __init__(self):
super(CommandSettings, self).__init__(["settings"])
super().__init__(['settings'])
class CommandPrivacy(Command):
@ -208,7 +200,7 @@ class CommandPrivacy(Command):
"""
def __init__(self):
super(CommandPrivacy, self).__init__(["privacy"])
super().__init__(['privacy'])
class Text(Filter):
@ -216,42 +208,44 @@ class Text(Filter):
Simple text filter
"""
def __init__(
self,
equals: Optional[Union[str, LazyProxy]] = None,
contains: Optional[Union[str, LazyProxy]] = None,
startswith: Optional[Union[str, LazyProxy]] = None,
endswith: Optional[Union[str, LazyProxy]] = None,
ignore_case=False,
):
_default_params = (
('text', 'equals'),
('text_contains', 'contains'),
('text_startswith', 'startswith'),
('text_endswith', 'endswith'),
)
def __init__(self,
equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
ignore_case=False):
"""
Check text for one of pattern. Only one mode can be used in one filter.
In every pattern, a single string is treated as a list with 1 element.
:param equals:
:param contains:
:param startswith:
:param endswith:
:param equals: True if object's text in the list
:param contains: True if object's text contains all strings from the list
:param startswith: True if object's text starts with any of strings from the list
:param endswith: True if object's text ends with any of strings from the list
:param ignore_case: case insensitive
"""
# Only one mode can be used. check it.
check = sum(map(bool, (equals, contains, startswith, endswith)))
check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
if check > 1:
args = "' and '".join(
[
arg[0]
for arg in [
("equals", equals),
("contains", contains),
("startswith", startswith),
("endswith", endswith),
]
if arg[1]
]
)
args = "' and '".join([arg[0] for arg in [('equals', equals),
('contains', contains),
('startswith', startswith),
('endswith', endswith)
] if arg[1] is not None])
raise ValueError(f"Arguments '{args}' cannot be used together.")
elif check == 0:
raise ValueError(f"No one mode is specified!")
equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
else e,
(equals, contains, endswith, startswith))
self.equals = equals
self.contains = contains
self.endswith = endswith
@ -260,18 +254,13 @@ class Text(Filter):
@classmethod
def validate(cls, full_config: Dict[str, Any]):
if "text" in full_config:
return {"equals": full_config.pop("text")}
elif "text_contains" in full_config:
return {"contains": full_config.pop("text_contains")}
elif "text_startswith" in full_config:
return {"startswith": full_config.pop("text_startswith")}
elif "text_endswith" in full_config:
return {"endswith": full_config.pop("text_endswith")}
for param, key in cls._default_params:
if param in full_config:
return {key: full_config.pop(param)}
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
text = obj.text or obj.caption or ""
text = obj.text or obj.caption or ''
if not text and obj.poll:
text = obj.poll.question
elif isinstance(obj, CallbackQuery):
@ -285,15 +274,26 @@ class Text(Filter):
if self.ignore_case:
text = text.lower()
_pre_process_func = lambda s: str(s).lower()
else:
_pre_process_func = str
if self.equals:
return text == str(self.equals)
elif self.contains:
return str(self.contains) in text
elif self.startswith:
return text.startswith(str(self.startswith))
elif self.endswith:
return text.endswith(str(self.endswith))
# now check
if self.equals is not None:
equals = list(map(_pre_process_func, self.equals))
return text in equals
if self.contains is not None:
contains = list(map(_pre_process_func, self.contains))
return all(map(text.__contains__, contains))
if self.startswith is not None:
startswith = list(map(_pre_process_func, self.startswith))
return any(map(text.startswith, startswith))
if self.endswith is not None:
endswith = list(map(_pre_process_func, self.endswith))
return any(map(text.endswith, endswith))
return False
@ -307,7 +307,7 @@ class HashTag(Filter):
def __init__(self, hashtags=None, cashtags=None):
if not hashtags and not cashtags:
raise ValueError("No one hashtag or cashtag is specified!")
raise ValueError('No one hashtag or cashtag is specified!')
if hashtags is None:
hashtags = []
@ -327,10 +327,10 @@ class HashTag(Filter):
@classmethod
def validate(cls, full_config: Dict[str, Any]):
config = {}
if "hashtags" in full_config:
config["hashtags"] = full_config.pop("hashtags")
if "cashtags" in full_config:
config["cashtags"] = full_config.pop("cashtags")
if 'hashtags' in full_config:
config['hashtags'] = full_config.pop('hashtags')
if 'cashtags' in full_config:
config['cashtags'] = full_config.pop('cashtags')
return config
async def check(self, message: types.Message):
@ -344,13 +344,9 @@ class HashTag(Filter):
return False
hashtags, cashtags = self._get_tags(text, entities)
if (
self.hashtags
and set(hashtags) & set(self.hashtags)
or self.cashtags
and set(cashtags) & set(self.cashtags)
):
return {"hashtags": hashtags, "cashtags": cashtags}
if self.hashtags and set(hashtags) & set(self.hashtags) \
or self.cashtags and set(cashtags) & set(self.cashtags):
return {'hashtags': hashtags, 'cashtags': cashtags}
def _get_tags(self, text, entities):
hashtags = []
@ -358,11 +354,11 @@ class HashTag(Filter):
for entity in entities:
if entity.type == types.MessageEntityType.HASHTAG:
value = entity.get_text(text).lstrip("#")
value = entity.get_text(text).lstrip('#')
hashtags.append(value)
elif entity.type == types.MessageEntityType.CASHTAG:
value = entity.get_text(text).lstrip("$")
value = entity.get_text(text).lstrip('$')
cashtags.append(value)
return hashtags, cashtags
@ -380,23 +376,27 @@ class Regexp(Filter):
@classmethod
def validate(cls, full_config: Dict[str, Any]):
if "regexp" in full_config:
return {"regexp": full_config.pop("regexp")}
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 ""
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
match = self.regexp.search(content)
if match:
return {"regexp": match}
return {'regexp': match}
return False
@ -405,19 +405,17 @@ class RegexpCommandsFilter(BoundFilter):
Check commands by regexp in message
"""
key = "regexp_commands"
key = 'regexp_commands'
def __init__(self, regexp_commands):
self.regexp_commands = [
re.compile(command, flags=re.IGNORECASE | re.MULTILINE) for command in regexp_commands
]
self.regexp_commands = [re.compile(command, flags=re.IGNORECASE | re.MULTILINE) for command in regexp_commands]
async def check(self, message):
if not message.is_command():
return False
command = message.text.split()[0][1:]
command, _, mention = command.partition("@")
command, _, mention = command.partition('@')
if mention and mention != (await message.bot.me).username:
return False
@ -425,7 +423,7 @@ class RegexpCommandsFilter(BoundFilter):
for command in self.regexp_commands:
search = command.search(message.text)
if search:
return {"regexp_command": search}
return {'regexp_command': search}
return False
@ -434,7 +432,7 @@ class ContentTypeFilter(BoundFilter):
Check message content type
"""
key = "content_types"
key = 'content_types'
required = True
default = types.ContentTypes.TEXT
@ -442,21 +440,18 @@ class ContentTypeFilter(BoundFilter):
self.content_types = content_types
async def check(self, message):
return (
types.ContentType.ANY in self.content_types
or message.content_type in self.content_types
)
return types.ContentType.ANY in self.content_types or \
message.content_type in self.content_types
class StateFilter(BoundFilter):
"""
Check user state
"""
key = "state"
key = 'state'
required = True
ctx_state = ContextVar("user_state")
ctx_state = ContextVar('user_state')
def __init__(self, dispatcher, state):
from aiogram.dispatcher.filters.state import State, StatesGroup
@ -464,7 +459,7 @@ class StateFilter(BoundFilter):
self.dispatcher = dispatcher
states = []
if not isinstance(state, (list, set, tuple, frozenset)) or state is None:
state = [state]
state = [state, ]
for item in state:
if isinstance(item, State):
states.append(item.state)
@ -475,14 +470,11 @@ class StateFilter(BoundFilter):
self.states = states
def get_target(self, obj):
return (
getattr(getattr(obj, "chat", None), "id", None),
getattr(getattr(obj, "from_user", None), "id", None),
)
return getattr(getattr(obj, 'chat', None), 'id', None), getattr(getattr(obj, 'from_user', None), 'id', None)
async def check(self, obj):
if "*" in self.states:
return {"state": self.dispatcher.current_state()}
if '*' in self.states:
return {'state': self.dispatcher.current_state()}
try:
state = self.ctx_state.get()
@ -493,11 +485,11 @@ class StateFilter(BoundFilter):
state = await self.dispatcher.storage.get_state(chat=chat, user=user)
self.ctx_state.set(state)
if state in self.states:
return {"state": self.dispatcher.current_state(), "raw_state": state}
return {'state': self.dispatcher.current_state(), 'raw_state': state}
else:
if state in self.states:
return {"state": self.dispatcher.current_state(), "raw_state": state}
return {'state': self.dispatcher.current_state(), 'raw_state': state}
return False
@ -507,7 +499,7 @@ class ExceptionsFilter(BoundFilter):
Filter for exceptions
"""
key = "exception"
key = 'exception'
def __init__(self, exception):
self.exception = exception
@ -519,3 +511,136 @@ class ExceptionsFilter(BoundFilter):
return True
except:
return False
class IDFilter(Filter):
def __init__(self,
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
):
"""
:param user_id:
:param chat_id:
"""
if user_id is None and chat_id is None:
raise ValueError("Both user_id and chat_id can't be None")
self.user_id = None
self.chat_id = None
if user_id:
if isinstance(user_id, Iterable):
self.user_id = list(map(int, user_id))
else:
self.user_id = [int(user_id), ]
if chat_id:
if isinstance(chat_id, Iterable):
self.chat_id = list(map(int, chat_id))
else:
self.chat_id = [int(chat_id), ]
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if 'user_id' in full_config:
result['user_id'] = full_config.pop('user_id')
if 'chat_id' in full_config:
result['chat_id'] = full_config.pop('chat_id')
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
if isinstance(obj, Message):
user_id = obj.from_user.id
chat_id = obj.chat.id
elif isinstance(obj, CallbackQuery):
user_id = obj.from_user.id
chat_id = None
if obj.message is not None:
# if the button was sent with message
chat_id = obj.message.chat.id
elif isinstance(obj, InlineQuery):
user_id = obj.from_user.id
chat_id = None
else:
return False
if self.user_id and self.chat_id:
return user_id in self.user_id and chat_id in self.chat_id
if self.user_id:
return user_id in self.user_id
if self.chat_id:
return chat_id in self.chat_id
return False
class AdminFilter(Filter):
"""
Checks if user is admin in a chat.
If is_chat_admin is not set, the filter will check in the current chat (correct only for messages).
is_chat_admin is required for InlineQuery.
"""
def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
if is_chat_admin:
if isinstance(is_chat_admin, bool):
self._check_current = is_chat_admin
if isinstance(is_chat_admin, Iterable):
self._chat_ids = list(is_chat_admin)
else:
self._chat_ids = [is_chat_admin]
else:
self._check_current = True
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if "is_chat_admin" in full_config:
result["is_chat_admin"] = full_config.pop("is_chat_admin")
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool:
user_id = obj.from_user.id
if self._check_current:
if isinstance(obj, Message):
message = obj
elif isinstance(obj, CallbackQuery) and obj.message:
message = obj.message
else:
return False
if ChatType.is_private(message): # there is no admin in private chats
return False
chat_ids = [message.chat.id]
else:
chat_ids = self._chat_ids
admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)]
return user_id in admins
class IsReplyFilter(BoundFilter):
"""
Check if message is replied and send reply message to handler
"""
key = 'is_reply'
def __init__(self, is_reply):
self.is_reply = is_reply
async def check(self, msg: Message):
if msg.reply_to_message and self.is_reply:
return {'reply': msg.reply_to_message}
elif not msg.reply_to_message and not self.is_reply:
return True

View file

@ -13,11 +13,9 @@ def wrap_async(func):
async def async_wrapper(*args, **kwargs):
return func(*args, **kwargs)
if (
inspect.isawaitable(func)
or inspect.iscoroutinefunction(func)
or isinstance(func, AbstractFilter)
):
if inspect.isawaitable(func) \
or inspect.iscoroutinefunction(func) \
or isinstance(func, AbstractFilter):
return func
return async_wrapper
@ -25,16 +23,14 @@ def wrap_async(func):
def get_filter_spec(dispatcher, filter_: callable):
kwargs = {}
if not callable(filter_):
raise TypeError("Filter must be callable and/or awaitable!")
raise TypeError('Filter must be callable and/or awaitable!')
spec = inspect.getfullargspec(filter_)
if "dispatcher" in spec:
kwargs["dispatcher"] = dispatcher
if (
inspect.isawaitable(filter_)
or inspect.iscoroutinefunction(filter_)
or isinstance(filter_, AbstractFilter)
):
if 'dispatcher' in spec:
kwargs['dispatcher'] = dispatcher
if inspect.isawaitable(filter_) \
or inspect.iscoroutinefunction(filter_) \
or isinstance(filter_, AbstractFilter):
return FilterObj(filter=filter_, kwargs=kwargs, is_async=True)
else:
return FilterObj(filter=filter_, kwargs=kwargs, is_async=False)
@ -86,17 +82,12 @@ class FilterRecord:
Filters record for factory
"""
def __init__(
self,
callback: typing.Callable,
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
):
def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'],
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None):
if event_handlers and exclude_event_handlers:
raise ValueError(
"'event_handlers' and 'exclude_event_handlers' arguments cannot be used together."
)
raise ValueError("'event_handlers' and 'exclude_event_handlers' arguments cannot be used together.")
self.callback = callback
self.event_handlers = event_handlers
@ -109,17 +100,17 @@ class FilterRecord:
elif issubclass(callback, AbstractFilter):
self.resolver = callback.validate
else:
raise RuntimeError("validator is required!")
raise RuntimeError('validator is required!')
def resolve(self, dispatcher, event_handler, full_config):
if not self._check_event_handler(event_handler):
return
config = self.resolver(full_config)
if config:
if "dispatcher" not in config:
if 'dispatcher' not in config:
spec = inspect.getfullargspec(self.callback)
if "dispatcher" in spec.args:
config["dispatcher"] = dispatcher
if 'dispatcher' in spec.args:
config['dispatcher'] = dispatcher
for key in config:
if key in full_config:
@ -142,9 +133,7 @@ class AbstractFilter(abc.ABC):
@classmethod
@abc.abstractmethod
def validate(
cls, full_config: typing.Dict[str, typing.Any]
) -> typing.Optional[typing.Dict[str, typing.Any]]:
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
"""
Validate and parse config.
@ -195,9 +184,7 @@ class Filter(AbstractFilter):
"""
@classmethod
def validate(
cls, full_config: typing.Dict[str, typing.Any]
) -> typing.Optional[typing.Dict[str, typing.Any]]:
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
"""
Here method ``validate`` is optional.
If you need to use filter from filters factory you need to override this method.
@ -215,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]:
"""
@ -241,7 +228,7 @@ class BoundFilter(Filter):
class _LogicFilter(Filter):
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]):
raise ValueError("That filter can't be used in filters factory!")
raise ValueError('That filter can\'t be used in filters factory!')
class NotFilter(_LogicFilter):
@ -253,6 +240,7 @@ class NotFilter(_LogicFilter):
class AndFilter(_LogicFilter):
def __init__(self, *targets):
self.targets = list(wrap_async(target) for target in targets)

View file

@ -17,7 +17,7 @@ class State:
@property
def group(self):
if not self._group:
raise RuntimeError("This state is not in any group.")
raise RuntimeError('This state is not in any group.')
return self._group
def get_root(self):
@ -25,21 +25,21 @@ class State:
@property
def state(self):
if self._state is None:
return None
elif self._state == "*":
if self._state is None or self._state == '*':
return self._state
elif self._group_name is None and self._group:
if self._group_name is None and self._group:
group = self._group.__full_group_name__
elif self._group_name:
group = self._group_name
else:
group = "@"
return f"{group}:{self._state}"
group = '@'
return f'{group}:{self._state}'
def set_parent(self, group):
if not issubclass(group, StatesGroup):
raise ValueError("Group must be subclass of StatesGroup")
raise ValueError('Group must be subclass of StatesGroup')
self._group = group
def __set_name__(self, owner, name):
@ -73,7 +73,6 @@ class StatesGroupMeta(type):
elif inspect.isclass(prop) and issubclass(prop, StatesGroup):
childs.append(prop)
prop._parent = cls
# continue
cls._parent = None
cls._childs = tuple(childs)
@ -83,13 +82,13 @@ class StatesGroupMeta(type):
return cls
@property
def __group_name__(cls):
def __group_name__(cls) -> str:
return cls._group_name
@property
def __full_group_name__(cls):
def __full_group_name__(cls) -> str:
if cls._parent:
return cls._parent.__full_group_name__ + "." + cls._group_name
return '.'.join((cls._parent.__full_group_name__, cls._group_name))
return cls._group_name
@property
@ -97,7 +96,7 @@ class StatesGroupMeta(type):
return cls._states
@property
def childs(cls):
def childs(cls) -> tuple:
return cls._childs
@property
@ -130,9 +129,9 @@ class StatesGroupMeta(type):
def __contains__(cls, item):
if isinstance(item, str):
return item in cls.all_states_names
elif isinstance(item, State):
if isinstance(item, State):
return item in cls.all_states
elif isinstance(item, StatesGroup):
if isinstance(item, StatesGroup):
return item in cls.all_childs
return False
@ -195,4 +194,4 @@ class StatesGroup(metaclass=StatesGroupMeta):
default_state = State()
any_state = State(state="*")
any_state = State(state='*')

View file

@ -1,10 +1,10 @@
import inspect
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Optional, Iterable
from typing import Optional, Iterable, List
ctx_data = ContextVar("ctx_handler_data")
current_handler = ContextVar("current_handler")
ctx_data = ContextVar('ctx_handler_data')
current_handler = ContextVar('current_handler')
@dataclass
@ -23,11 +23,10 @@ class CancelHandler(Exception):
def _get_spec(func: callable):
while hasattr(func, "__wrapped__"): # Try to resolve decorated callbacks
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
func = func.__wrapped__
spec = inspect.getfullargspec(func)
return spec, func
return spec
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
@ -42,12 +41,10 @@ class Handler:
self.dispatcher = dispatcher
self.once = once
self.handlers = []
self.handlers: List[Handler.HandlerObj] = []
self.middleware_key = middleware_key
def register(self, handler, filters=None, index=None):
from .filters import get_filters_spec
"""
Register callback
@ -57,7 +54,9 @@ class Handler:
:param filters: list of filters
:param index: you can reorder handlers
"""
spec, handler = _get_spec(handler)
from .filters import get_filters_spec
spec = _get_spec(handler)
if filters and not isinstance(filters, (list, tuple, set)):
filters = [filters]
@ -81,7 +80,7 @@ class Handler:
if handler is registered:
self.handlers.remove(handler_obj)
return True
raise ValueError("This handler is not registered!")
raise ValueError('This handler is not registered!')
async def notify(self, *args):
"""
@ -99,9 +98,7 @@ class Handler:
if self.middleware_key:
try:
await self.dispatcher.middleware.trigger(
f"pre_process_{self.middleware_key}", args + (data,)
)
await self.dispatcher.middleware.trigger(f"pre_process_{self.middleware_key}", args + (data,))
except CancelHandler: # Allow to cancel current event
return results
@ -115,9 +112,7 @@ class Handler:
ctx_token = current_handler.set(handler_obj.handler)
try:
if self.middleware_key:
await self.dispatcher.middleware.trigger(
f"process_{self.middleware_key}", args + (data,)
)
await self.dispatcher.middleware.trigger(f"process_{self.middleware_key}", args + (data,))
partial_data = _check_spec(handler_obj.spec, data)
response = await handler_obj.handler(*args, **partial_data)
if response is not None:
@ -132,9 +127,8 @@ class Handler:
current_handler.reset(ctx_token)
finally:
if self.middleware_key:
await self.dispatcher.middleware.trigger(
f"post_process_{self.middleware_key}", args + (results, data)
)
await self.dispatcher.middleware.trigger(f"post_process_{self.middleware_key}",
args + (results, data,))
return results

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -9,37 +9,31 @@ from babel.support import LazyProxy
from .fields import BaseField
from ..utils import json
from ..utils.mixins import ContextInstanceMixin
if typing.TYPE_CHECKING:
from ..bot.bot import Bot
__all__ = (
"MetaTelegramObject",
"TelegramObject",
"InputFile",
"String",
"Integer",
"Float",
"Boolean",
)
__all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean')
PROPS_ATTR_NAME = "_props"
VALUES_ATTR_NAME = "_values"
ALIASES_ATTR_NAME = "_aliases"
PROPS_ATTR_NAME = '_props'
VALUES_ATTR_NAME = '_values'
ALIASES_ATTR_NAME = '_aliases'
# Binding of builtin types
InputFile = TypeVar("InputFile", "InputFile", io.BytesIO, io.FileIO, str)
String = TypeVar("String", bound=str)
Integer = TypeVar("Integer", bound=int)
Float = TypeVar("Float", bound=float)
Boolean = TypeVar("Boolean", bound=bool)
InputFile = TypeVar('InputFile', 'InputFile', io.BytesIO, io.FileIO, str)
String = TypeVar('String', bound=str)
Integer = TypeVar('Integer', bound=int)
Float = TypeVar('Float', bound=float)
Boolean = TypeVar('Boolean', bound=bool)
T = TypeVar('T')
class MetaTelegramObject(type):
"""
Metaclass for telegram objects
"""
_objects = {}
def __new__(mcs, name, bases, namespace, **kwargs):
def __new__(mcs: typing.Type[T], name: str, bases: typing.Tuple[typing.Type], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any) -> T:
cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace)
props = {}
@ -55,9 +49,7 @@ class MetaTelegramObject(type):
aliases.update(getattr(base, ALIASES_ATTR_NAME))
# Scan current object for props
for name, prop in (
(name, prop) for name, prop in namespace.items() if isinstance(prop, BaseField)
):
for name, prop in ((name, prop) for name, prop in namespace.items() if isinstance(prop, BaseField)):
props[prop.alias] = prop
if prop.default is not None:
values[prop.alias] = prop.default
@ -82,7 +74,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
Abstract class for telegram objects
"""
def __init__(self, conf=None, **kwargs):
def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None:
"""
Deserialize object
@ -128,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, ALIASES_ATTR_NAME, {})
@property
def values(self):
def values(self) -> typing.Tuple[str]:
"""
Get values
@ -139,11 +131,11 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, VALUES_ATTR_NAME)
@property
def telegram_types(self):
def telegram_types(self) -> typing.List[TelegramObject]:
return type(self).telegram_types
@classmethod
def to_object(cls, data):
def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T:
"""
Deserialize object
@ -153,19 +145,17 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return cls(**data)
@property
def bot(self):
def bot(self) -> Bot:
from ..bot.bot import Bot
bot = Bot.get_current()
if bot is None:
raise RuntimeError(
"Can't get bot instance from context. "
"You can fix it with setting current instance: "
"'Bot.set_current(bot_instance)'"
)
raise RuntimeError("Can't get bot instance from context. "
"You can fix it with setting current instance: "
"'Bot.set_current(bot_instance)'")
return bot
def to_python(self) -> typing.Dict:
def to_python(self) -> typing.Dict[str, typing.Any]:
"""
Get object as JSON serializable
@ -183,7 +173,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
result[self.props_aliases.get(name, name)] = value
return result
def clean(self):
def clean(self) -> None:
"""
Remove empty values
"""
@ -201,7 +191,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return json.dumps(self.to_python())
@classmethod
def create(cls, *args, **kwargs):
def create(cls: Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
raise NotImplemented
def __str__(self) -> str:
@ -212,7 +202,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
return self.as_json()
def __getitem__(self, item):
def __getitem__(self, item: typing.Union[str, int]) -> typing.Any:
"""
Item getter (by key)
@ -223,7 +213,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return self.props[item].get_value(self)
raise KeyError(item)
def __setitem__(self, key, value):
def __setitem__(self, key: str, value: typing.Any) -> None:
"""
Item setter (by key)
@ -232,10 +222,10 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
:return:
"""
if key in self.props:
return self.props[key].set_value(self, value, self.conf.get("parent", None))
return self.props[key].set_value(self, value, self.conf.get('parent', None))
raise KeyError(key)
def __contains__(self, item):
def __contains__(self, item: typing.Dict[str, typing.Any]) -> bool:
"""
Check key contains in that object
@ -245,7 +235,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
self.clean()
return item in self.values
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
"""
Iterate over items
@ -254,7 +244,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for item in self.to_python().items():
yield item
def iter_keys(self):
def iter_keys(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over keys
@ -263,7 +253,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for key, _ in self:
yield key
def iter_values(self):
def iter_values(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over values
@ -272,9 +262,9 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for _, value in self:
yield value
def __hash__(self):
def _hash(obj):
buf = 0
def __hash__(self) -> int:
def _hash(obj)-> int:
buf: int = 0
if isinstance(obj, list):
for item in obj:
buf += _hash(item)
@ -294,5 +284,5 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return result
def __eq__(self, other):
def __eq__(self, other: TelegramObject) -> bool:
return isinstance(other, self.__class__) and hash(other) == hash(self)

View file

@ -1,10 +1,12 @@
from __future__ import annotations
import asyncio
import datetime
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
@ -16,7 +18,6 @@ class Chat(base.TelegramObject):
https://core.telegram.org/bots/api#chat
"""
id: base.Integer = fields.Field()
type: base.String = fields.Field()
title: base.String = fields.Field()
@ -27,7 +28,8 @@ class Chat(base.TelegramObject):
photo: ChatPhoto = fields.Field(base=ChatPhoto)
description: base.String = fields.Field()
invite_link: base.String = fields.Field()
pinned_message: "Message" = fields.Field(base="Message")
pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field()
@ -39,7 +41,7 @@ class Chat(base.TelegramObject):
if self.type == ChatType.PRIVATE:
full_name = self.first_name
if self.last_name:
full_name += " " + self.last_name
full_name += ' ' + self.last_name
return full_name
return self.title
@ -49,7 +51,7 @@ class Chat(base.TelegramObject):
Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned
"""
if self.username:
return "@" + self.username
return '@' + self.username
if self.type == ChatType.PRIVATE:
return self.full_name
return None
@ -57,7 +59,7 @@ class Chat(base.TelegramObject):
@property
def user_url(self):
if self.type != ChatType.PRIVATE:
raise TypeError("`user_url` property is only available in private chats!")
raise TypeError('`user_url` property is only available in private chats!')
return f"tg://user?id={self.id}"
@ -80,7 +82,7 @@ class Chat(base.TelegramObject):
return f"tg://user?id={self.id}"
if self.username:
return f"https://t.me/{self.username}"
return f'https://t.me/{self.username}'
if self.invite_link:
return self.invite_link
@ -162,9 +164,8 @@ class Chat(base.TelegramObject):
"""
return await self.bot.delete_chat_description(self.id, description)
async def kick(
self, user_id: base.Integer, until_date: typing.Union[base.Integer, None] = None
):
async def kick(self, user_id: base.Integer,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None):
"""
Use this method to kick a user from a group, a supergroup or a channel.
In the case of supergroups and channels, the user will not be able to return to the group
@ -203,15 +204,13 @@ 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,
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,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
can_add_web_page_previews: typing.Union[base.Boolean, None] = None,
) -> base.Boolean:
async def restrict(self, user_id: base.Integer,
permissions: typing.Optional[ChatPermissions] = None,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
can_add_web_page_previews: typing.Union[base.Boolean, None] = None) -> base.Boolean:
"""
Use this method to restrict a user in a supergroup.
The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
@ -221,6 +220,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
@ -237,28 +238,23 @@ 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,
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
)
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,
can_add_web_page_previews=can_add_web_page_previews)
async def promote(
self,
user_id: base.Integer,
can_change_info: typing.Union[base.Boolean, None] = None,
can_post_messages: typing.Union[base.Boolean, None] = None,
can_edit_messages: typing.Union[base.Boolean, None] = None,
can_delete_messages: typing.Union[base.Boolean, None] = None,
can_invite_users: typing.Union[base.Boolean, None] = None,
can_restrict_members: typing.Union[base.Boolean, None] = None,
can_pin_messages: typing.Union[base.Boolean, None] = None,
can_promote_members: typing.Union[base.Boolean, None] = None,
) -> base.Boolean:
async def promote(self, user_id: base.Integer,
can_change_info: typing.Union[base.Boolean, None] = None,
can_post_messages: typing.Union[base.Boolean, None] = None,
can_edit_messages: typing.Union[base.Boolean, None] = None,
can_delete_messages: typing.Union[base.Boolean, None] = None,
can_invite_users: typing.Union[base.Boolean, None] = None,
can_restrict_members: typing.Union[base.Boolean, None] = None,
can_pin_messages: typing.Union[base.Boolean, None] = None,
can_promote_members: typing.Union[base.Boolean, None] = None) -> base.Boolean:
"""
Use this method to promote or demote a user in a supergroup or a channel.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@ -289,18 +285,16 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.promote_chat_member(
self.id,
user_id=user_id,
can_change_info=can_change_info,
can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages,
can_delete_messages=can_delete_messages,
can_invite_users=can_invite_users,
can_restrict_members=can_restrict_members,
can_pin_messages=can_pin_messages,
can_promote_members=can_promote_members,
)
return await self.bot.promote_chat_member(self.id,
user_id=user_id,
can_change_info=can_change_info,
can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages,
can_delete_messages=can_delete_messages,
can_invite_users=can_invite_users,
can_restrict_members=can_restrict_members,
can_pin_messages=can_pin_messages,
can_promote_members=can_promote_members)
async def pin_message(self, message_id: int, disable_notification: bool = False):
"""
@ -436,9 +430,9 @@ class ChatType(helper.Helper):
@staticmethod
def _check(obj, chat_types) -> bool:
if hasattr(obj, "chat"):
if hasattr(obj, 'chat'):
obj = obj.chat
if not hasattr(obj, "type"):
if not hasattr(obj, 'type'):
return False
return obj.type in chat_types
@ -525,13 +519,12 @@ class ChatActions(helper.Helper):
@classmethod
async def _do(cls, action: str, sleep=None):
from aiogram import Bot
await Bot.get_current().send_chat_action(Chat.get_current().id, action)
if sleep:
await asyncio.sleep(sleep)
@classmethod
def calc_timeout(cls, text, timeout=0.8):
def calc_timeout(cls, text, timeout=.8):
"""
Calculate timeout for text

View file

@ -1,5 +1,6 @@
import datetime
import warnings
from typing import Optional
from . import base
from . import fields
@ -13,7 +14,6 @@ class ChatMember(base.TelegramObject):
https://core.telegram.org/bots/api#chatmember
"""
user: User = fields.Field(base=User)
status: base.String = fields.Field()
until_date: datetime.datetime = fields.DateTimeField()
@ -29,25 +29,17 @@ class ChatMember(base.TelegramObject):
is_member: base.Boolean = fields.Field()
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
def is_admin(self):
warnings.warn(
"`is_admin` method deprecated due to updates in Bot API 4.2. "
"This method renamed to `is_chat_admin` and will be available until aiogram 2.3",
DeprecationWarning,
stacklevel=2,
)
return self.is_chat_admin()
def is_chat_admin(self) -> bool:
return ChatMemberStatus.is_chat_admin(self.status)
def is_chat_admin(self):
return ChatMemberStatus.is_admin(self.status)
def is_chat_member(self) -> bool:
return ChatMemberStatus.is_chat_member(self.status)
def is_chat_member(self):
return ChatMemberStatus.is_member(self.status)
def __int__(self):
def __int__(self) -> int:
return self.user.id
@ -55,39 +47,19 @@ class ChatMemberStatus(helper.Helper):
"""
Chat member status
"""
mode = helper.HelperMode.lowercase
CREATOR = helper.Item() # creator
ADMINISTRATOR = helper.Item() # administrator
MEMBER = helper.Item() # member
RESTRICTED = helper.Item() # restricted
LEFT = helper.Item() # left
KICKED = helper.Item() # kicked
@classmethod
def is_admin(cls, role):
warnings.warn(
"`is_admin` method deprecated due to updates in Bot API 4.2. "
"This method renamed to `is_chat_admin` and will be available until aiogram 2.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):
def is_chat_admin(cls, role: str) -> bool:
return role in [cls.ADMINISTRATOR, cls.CREATOR]
@classmethod
def is_chat_member(cls, role):
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR]
def is_chat_member(cls, role: str) -> bool:
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED]

View file

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

View file

@ -6,7 +6,7 @@ from . import base
from . import fields
from .input_file import InputFile
ATTACHMENT_PREFIX = "attach://"
ATTACHMENT_PREFIX = 'attach://'
class InputMedia(base.TelegramObject):
@ -22,26 +22,23 @@ class InputMedia(base.TelegramObject):
https://core.telegram.org/bots/api#inputmedia
"""
type: base.String = fields.Field(default="photo")
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"
)
type: base.String = fields.Field(default='photo')
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
self._media_file = None
media = kwargs.pop("media", None)
media = kwargs.pop('media', None)
if isinstance(media, (io.IOBase, InputFile)):
self.file = media
elif media is not None:
self.media = media
thumb = kwargs.pop("thumb", None)
thumb = kwargs.pop('thumb', None)
if isinstance(thumb, (io.IOBase, InputFile)):
self.thumb_file = thumb
elif thumb is not None:
@ -61,7 +58,7 @@ class InputMedia(base.TelegramObject):
@file.setter
def file(self, file: io.IOBase):
self.media = "attach://" + secrets.token_urlsafe(16)
self.media = 'attach://' + secrets.token_urlsafe(16)
self._media_file = file
@file.deleter
@ -70,7 +67,7 @@ class InputMedia(base.TelegramObject):
self._media_file = None
def _media_changed(self, value):
if value is None or isinstance(value, str) and not value.startswith("attach://"):
if value is None or isinstance(value, str) and not value.startswith('attach://'):
self._media_file = None
@property
@ -79,7 +76,7 @@ class InputMedia(base.TelegramObject):
@thumb_file.setter
def thumb_file(self, file: io.IOBase):
self.thumb = "attach://" + secrets.token_urlsafe(16)
self.thumb = 'attach://' + secrets.token_urlsafe(16)
self._thumb_file = file
@thumb_file.deleter
@ -88,7 +85,7 @@ class InputMedia(base.TelegramObject):
self._thumb_file = None
def _thumb_changed(self, value):
if value is None or isinstance(value, str) and not value.startswith("attach://"):
if value is None or isinstance(value, str) and not value.startswith('attach://'):
self._thumb_file = None
def get_files(self):
@ -109,28 +106,14 @@ class InputMediaAnimation(InputMedia):
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
def __init__(
self,
media: base.InputFile,
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,
):
super(InputMediaAnimation, self).__init__(
type="animation",
media=media,
thumb=thumb,
caption=caption,
width=width,
height=height,
duration=duration,
parse_mode=parse_mode,
conf=kwargs,
)
def __init__(self, media: base.InputFile,
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.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)
class InputMediaDocument(InputMedia):
@ -140,22 +123,11 @@ class InputMediaDocument(InputMedia):
https://core.telegram.org/bots/api#inputmediadocument
"""
def __init__(
self,
media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
parse_mode: base.Boolean = None,
**kwargs,
):
super(InputMediaDocument, self).__init__(
type="document",
media=media,
thumb=thumb,
caption=caption,
parse_mode=parse_mode,
conf=kwargs,
)
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
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)
class InputMediaAudio(InputMedia):
@ -171,32 +143,18 @@ class InputMediaAudio(InputMedia):
performer: base.String = fields.Field()
title: base.String = fields.Field()
def __init__(
self,
media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None,
height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.Boolean = None,
**kwargs,
):
super(InputMediaAudio, self).__init__(
type="audio",
media=media,
thumb=thumb,
caption=caption,
width=width,
height=height,
duration=duration,
performer=performer,
title=title,
parse_mode=parse_mode,
conf=kwargs,
)
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
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,
parse_mode=parse_mode, conf=kwargs)
class InputMediaPhoto(InputMedia):
@ -206,22 +164,11 @@ class InputMediaPhoto(InputMedia):
https://core.telegram.org/bots/api#inputmediaphoto
"""
def __init__(
self,
media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
parse_mode: base.Boolean = None,
**kwargs,
):
super(InputMediaPhoto, self).__init__(
type="photo",
media=media,
thumb=thumb,
caption=caption,
parse_mode=parse_mode,
conf=kwargs,
)
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
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)
class InputMediaVideo(InputMedia):
@ -230,36 +177,21 @@ class InputMediaVideo(InputMedia):
https://core.telegram.org/bots/api#inputmediavideo
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
supports_streaming: base.Boolean = fields.Field()
def __init__(
self,
media: base.InputFile,
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,
supports_streaming: base.Boolean = None,
**kwargs,
):
super(InputMediaVideo, self).__init__(
type="video",
media=media,
thumb=thumb,
caption=caption,
width=width,
height=height,
duration=duration,
parse_mode=parse_mode,
supports_streaming=supports_streaming,
conf=kwargs,
)
def __init__(self, media: base.InputFile,
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.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,
parse_mode=parse_mode,
supports_streaming=supports_streaming, conf=kwargs)
class MediaGroup(base.TelegramObject):
@ -267,9 +199,7 @@ class MediaGroup(base.TelegramObject):
Helper for sending media group
"""
def __init__(
self, medias: typing.Optional[typing.List[typing.Union[InputMedia, typing.Dict]]] = None
):
def __init__(self, medias: typing.Optional[typing.List[typing.Union[InputMedia, typing.Dict]]] = None):
super(MediaGroup, self).__init__()
self.media = []
@ -292,13 +222,13 @@ class MediaGroup(base.TelegramObject):
:param media:
"""
if isinstance(media, dict):
if "type" not in media:
if 'type' not in media:
raise ValueError(f"Invalid media!")
media_type = media["type"]
if media_type == "photo":
media_type = media['type']
if media_type == 'photo':
media = InputMediaPhoto(**media)
elif media_type == "video":
elif media_type == 'video':
media = InputMediaVideo(**media)
# elif media_type == 'document':
# media = InputMediaDocument(**media)
@ -310,11 +240,9 @@ class MediaGroup(base.TelegramObject):
raise TypeError(f"Invalid media type '{media_type}'!")
elif not isinstance(media, InputMedia):
raise TypeError(
f"Media must be an instance of InputMedia or dict, not {type(media).__name__}"
)
raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}")
elif media.type in ["document", "audio", "animation"]:
elif media.type in ['document', 'audio', 'animation']:
raise ValueError(f"This type of media is not supported by media groups!")
self.media.append(media)
@ -349,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
@ -371,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
@ -385,9 +313,8 @@ class MediaGroup(base.TelegramObject):
self.attach(document)
'''
def attach_photo(
self, photo: typing.Union[InputMediaPhoto, base.InputFile], caption: base.String = None
):
def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile],
caption: base.String = None):
"""
Attach photo
@ -398,15 +325,10 @@ class MediaGroup(base.TelegramObject):
photo = InputMediaPhoto(media=photo, caption=caption)
self.attach(photo)
def attach_video(
self,
video: typing.Union[InputMediaVideo, base.InputFile],
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None,
height: base.Integer = None,
duration: base.Integer = None,
):
def attach_video(self, video: typing.Union[InputMediaVideo, base.InputFile],
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None):
"""
Attach video
@ -417,14 +339,8 @@ class MediaGroup(base.TelegramObject):
:param duration:
"""
if not isinstance(video, InputMedia):
video = InputMediaVideo(
media=video,
thumb=thumb,
caption=caption,
width=width,
height=height,
duration=duration,
)
video = InputMediaVideo(media=video, thumb=thumb, caption=caption,
width=width, height=height, duration=duration)
self.attach(video)
def to_python(self) -> typing.List:

File diff suppressed because it is too large Load diff

View file

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

View file

@ -26,7 +26,7 @@ class Downloadable:
if destination is None:
destination = file.file_path
elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination):
os.path.join(destination, file.file_path)
destination = os.path.join(destination, file.file_path)
else:
is_path = False

View file

@ -15,6 +15,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()

View file

@ -14,5 +14,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)

View file

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

View file

@ -8,7 +8,10 @@ import collections
import hashlib
import hmac
from aiogram.utils.deprecated import deprecated
@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`', stacklevel=3)
def generate_hash(data: dict, token: str) -> str:
"""
Generate secret hash
@ -24,6 +27,7 @@ def generate_hash(data: dict, token: str) -> str:
return hmac.new(secret.digest(), msg.encode("utf-8"), digestmod=hashlib.sha256).hexdigest()
@deprecated('`check_token` helper was renamed to `check_integrity`', stacklevel=3)
def check_token(data: dict, token: str) -> bool:
"""
Validate auth token
@ -34,3 +38,32 @@ def check_token(data: dict, token: str) -> bool:
"""
param_hash = data.get("hash", "") or ""
return param_hash == generate_hash(data, token)
def check_signature(token: str, hash: str, **kwargs) -> bool:
"""
Generate hexadecimal representation
of the HMAC-SHA-256 signature of the data-check-string
with the SHA256 hash of the bot's token used as a secret key
:param token:
:param hash:
:param kwargs: all params received on auth
:return:
"""
secret = hashlib.sha256(token.encode('utf-8'))
check_string = '\n'.join(map(lambda k: f'{k}={kwargs[k]}', sorted(kwargs)))
hmac_string = hmac.new(secret.digest(), check_string.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
return hmac_string == hash
def check_integrity(token: str, data: dict) -> bool:
"""
Verify the authentication and the integrity
of the data received on user's auth
:param token: Bot's token
:param data: all data that came on auth
:return:
"""
return check_signature(token, **data)

View file

@ -26,15 +26,15 @@ class CallbackData:
Callback data factory
"""
def __init__(self, prefix, *parts, sep=":"):
def __init__(self, prefix, *parts, sep=':'):
if not isinstance(prefix, str):
raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}")
elif not prefix:
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
if not prefix:
raise ValueError("Prefix can't be empty")
elif sep in prefix:
raise ValueError(f"Separator '{sep}' can't be used in prefix")
elif not parts:
raise TypeError("Parts is not passed!")
if sep in prefix:
raise ValueError(f"Separator {sep!r} can't be used in prefix")
if not parts:
raise TypeError('Parts were not passed!')
self.prefix = prefix
self.sep = sep
@ -59,24 +59,24 @@ class CallbackData:
if args:
value = args.pop(0)
else:
raise ValueError(f"Value for '{part}' is not passed!")
raise ValueError(f'Value for {part!r} was not passed!')
if value is not None and not isinstance(value, str):
value = str(value)
if not value:
raise ValueError(f"Value for part {part} can't be empty!'")
elif self.sep in value:
raise ValueError(f"Symbol defined as separator can't be used in values of parts")
raise ValueError(f"Value for part {part!r} can't be empty!'")
if self.sep in value:
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
data.append(value)
if args or kwargs:
raise TypeError("Too many arguments is passed!")
raise TypeError('Too many arguments were passed!')
callback_data = self.sep.join(data)
if len(callback_data) > 64:
raise ValueError("Resulted callback data is too long!")
raise ValueError('Resulted callback data is too long!')
return callback_data
@ -91,9 +91,9 @@ class CallbackData:
if prefix != self.prefix:
raise ValueError("Passed callback data can't be parsed with that prefix.")
elif len(parts) != len(self._part_names):
raise ValueError("Invalid parts count!")
raise ValueError('Invalid parts count!')
result = {"@": prefix}
result = {'@': prefix}
result.update(zip(self._part_names, parts))
return result
@ -106,11 +106,12 @@ class CallbackData:
"""
for key in config.keys():
if key not in self._part_names:
raise ValueError(f"Invalid field name '{key}'")
raise ValueError(f'Invalid field name {key!r}')
return CallbackDataFilter(self, config)
class CallbackDataFilter(Filter):
def __init__(self, factory: CallbackData, config: typing.Dict[str, str]):
self.config = config
self.factory = factory
@ -124,12 +125,12 @@ class CallbackDataFilter(Filter):
data = self.factory.parse(query.data)
except ValueError:
return False
else:
for key, value in self.config.items():
if isinstance(value, (list, tuple, set)):
if data.get(key) not in value:
return False
else:
if value != data.get(key):
return False
return {"callback_data": data}
for key, value in self.config.items():
if isinstance(value, (list, tuple, set, frozenset)):
if data.get(key) not in value:
return False
else:
if data.get(key) != value:
return False
return {'callback_data': data}

View file

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

View file

@ -1,102 +1,100 @@
"""
TelegramAPIError
ValidationError
Throttled
BadRequest
MessageError
MessageNotModified
MessageToForwardNotFound
MessageToDeleteNotFound
MessageIdentifierNotSpecified
MessageTextIsEmpty
MessageCantBeEdited
MessageCantBeDeleted
MessageToEditNotFound
MessageToReplyNotFound
ToMuchMessages
PollError
PollCantBeStopped
PollHasAlreadyClosed
PollsCantBeSentToPrivateChats
PollSizeError
PollMustHaveMoreOptions
PollCantHaveMoreOptions
PollsOptionsLengthTooLong
PollOptionsMustBeNonEmpty
PollQuestionMustBeNonEmpty
MessageWithPollNotFound (with MessageError)
MessageIsNotAPoll (with MessageError)
ObjectExpectedAsReplyMarkup
InlineKeyboardExpected
ChatNotFound
ChatDescriptionIsNotModified
InvalidQueryID
InvalidPeerID
InvalidHTTPUrlContent
ButtonURLInvalid
URLHostIsEmpty
StartParamInvalid
ButtonDataInvalid
WrongFileIdentifier
GroupDeactivated
BadWebhook
WebhookRequireHTTPS
BadWebhookPort
BadWebhookAddrInfo
BadWebhookNoAddressAssociatedWithHostname
NotFound
MethodNotKnown
PhotoAsInputFileRequired
InvalidStickersSet
NoStickerInRequest
ChatAdminRequired
NeedAdministratorRightsInTheChannel
MethodNotAvailableInPrivateChats
CantDemoteChatCreator
CantRestrictSelf
NotEnoughRightsToRestrict
PhotoDimensions
UnavailableMembers
TypeOfFileMismatch
WrongRemoteFileIdSpecified
PaymentProviderInvalid
CurrencyTotalAmountInvalid
CantParseUrl
UnsupportedUrlProtocol
CantParseEntities
ResultIdDuplicate
ConflictError
TerminatedByOtherGetUpdates
CantGetUpdates
Unauthorized
BotKicked
BotBlocked
UserDeactivated
CantInitiateConversation
CantTalkWithBots
NetworkError
RetryAfter
MigrateToChat
RestartingTelegram
- TelegramAPIError
- ValidationError
- Throttled
- BadRequest
- MessageError
- MessageNotModified
- MessageToForwardNotFound
- MessageToDeleteNotFound
- MessageIdentifierNotSpecified
- MessageTextIsEmpty
- MessageCantBeEdited
- MessageCantBeDeleted
- MessageToEditNotFound
- MessageToReplyNotFound
- ToMuchMessages
- PollError
- PollCantBeStopped
- PollHasAlreadyClosed
- PollsCantBeSentToPrivateChats
- PollSizeError
- PollMustHaveMoreOptions
- PollCantHaveMoreOptions
- PollsOptionsLengthTooLong
- PollOptionsMustBeNonEmpty
- PollQuestionMustBeNonEmpty
- MessageWithPollNotFound (with MessageError)
- MessageIsNotAPoll (with MessageError)
- ObjectExpectedAsReplyMarkup
- InlineKeyboardExpected
- ChatNotFound
- ChatDescriptionIsNotModified
- InvalidQueryID
- InvalidPeerID
- InvalidHTTPUrlContent
- ButtonURLInvalid
- URLHostIsEmpty
- StartParamInvalid
- ButtonDataInvalid
- WrongFileIdentifier
- GroupDeactivated
- BadWebhook
- WebhookRequireHTTPS
- BadWebhookPort
- BadWebhookAddrInfo
- BadWebhookNoAddressAssociatedWithHostname
- NotFound
- MethodNotKnown
- PhotoAsInputFileRequired
- InvalidStickersSet
- NoStickerInRequest
- ChatAdminRequired
- NeedAdministratorRightsInTheChannel
- MethodNotAvailableInPrivateChats
- CantDemoteChatCreator
- CantRestrictSelf
- NotEnoughRightsToRestrict
- PhotoDimensions
- UnavailableMembers
- TypeOfFileMismatch
- WrongRemoteFileIdSpecified
- PaymentProviderInvalid
- CurrencyTotalAmountInvalid
- CantParseUrl
- UnsupportedUrlProtocol
- CantParseEntities
- ResultIdDuplicate
- ConflictError
- TerminatedByOtherGetUpdates
- CantGetUpdates
- Unauthorized
- BotKicked
- BotBlocked
- UserDeactivated
- CantInitiateConversation
- CantTalkWithBots
- NetworkError
- RetryAfter
- MigrateToChat
- RestartingTelegram
TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
AIOGramWarning
TimeoutWarning
- AIOGramWarning
- TimeoutWarning
"""
import time
# TODO: Use exceptions detector from `aiograph`.
# TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
# TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
_PREFIXES = ["error: ", "[error]: ", "bad request: ", "conflict: ", "not found: "]
_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: ']
def _clean_message(text):
for prefix in _PREFIXES:
if text.startswith(prefix):
text = text[len(prefix) :]
text = text[len(prefix):]
return (text[0].upper() + text[1:]).strip()
@ -106,7 +104,7 @@ class TelegramAPIError(Exception):
class _MatchErrorMixin:
match = ""
match = ''
text = None
__subclasses = []
@ -166,72 +164,67 @@ class MessageNotModified(MessageError):
"""
Will be raised when you try to set new text is equals to current text.
"""
match = "message is not modified"
match = 'message is not modified'
class MessageToForwardNotFound(MessageError):
"""
Will be raised when you try to forward very old or deleted or unknown message.
"""
match = "message to forward not found"
match = 'message to forward not found'
class MessageToDeleteNotFound(MessageError):
"""
Will be raised when you try to delete very old or deleted or unknown message.
"""
match = "message to delete not found"
match = 'message to delete not found'
class MessageToReplyNotFound(MessageError):
"""
Will be raised when you try to reply to very old or deleted or unknown message.
"""
match = "message to reply not found"
match = 'message to reply not found'
class MessageIdentifierNotSpecified(MessageError):
match = "message identifier is not specified"
match = 'message identifier is not specified'
class MessageTextIsEmpty(MessageError):
match = "Message text is empty"
match = 'Message text is empty'
class MessageCantBeEdited(MessageError):
match = "message can't be edited"
match = 'message can\'t be edited'
class MessageCantBeDeleted(MessageError):
match = "message can't be deleted"
match = 'message can\'t be deleted'
class MessageToEditNotFound(MessageError):
match = "message to edit not found"
match = 'message to edit not found'
class MessageIsTooLong(MessageError):
match = "message is too long"
match = 'message is too long'
class ToMuchMessages(MessageError):
"""
Will be raised when you try to send media group with more than 10 items.
"""
match = "Too much messages to send as an album"
match = 'Too much messages to send as an album'
class ObjectExpectedAsReplyMarkup(BadRequest):
match = "object expected as reply markup"
match = 'object expected as reply markup'
class InlineKeyboardExpected(BadRequest):
match = "inline keyboard expected"
match = 'inline keyboard expected'
class PollError(BadRequest):
@ -243,7 +236,7 @@ class PollCantBeStopped(PollError):
class PollHasAlreadyBeenClosed(PollError):
match = "poll has already been closed"
match = 'poll has already been closed'
class PollsCantBeSentToPrivateChats(PollError):
@ -282,112 +275,109 @@ class MessageWithPollNotFound(PollError, MessageError):
"""
Will be raised when you try to stop poll with message without poll
"""
match = "message with poll to stop not found"
match = 'message with poll to stop not found'
class MessageIsNotAPoll(PollError, MessageError):
"""
Will be raised when you try to stop poll with message without poll
"""
match = "message is not a poll"
match = 'message is not a poll'
class ChatNotFound(BadRequest):
match = "chat not found"
match = 'chat not found'
class ChatIdIsEmpty(BadRequest):
match = "chat_id is empty"
match = 'chat_id is empty'
class InvalidUserId(BadRequest):
match = "user_id_invalid"
text = "Invalid user id"
match = 'user_id_invalid'
text = 'Invalid user id'
class ChatDescriptionIsNotModified(BadRequest):
match = "chat description is not modified"
match = 'chat description is not modified'
class InvalidQueryID(BadRequest):
match = "query is too old and response timeout expired or query id is invalid"
match = 'query is too old and response timeout expired or query id is invalid'
class InvalidPeerID(BadRequest):
match = "PEER_ID_INVALID"
text = "Invalid peer ID"
match = 'PEER_ID_INVALID'
text = 'Invalid peer ID'
class InvalidHTTPUrlContent(BadRequest):
match = "Failed to get HTTP URL content"
match = 'Failed to get HTTP URL content'
class ButtonURLInvalid(BadRequest):
match = "BUTTON_URL_INVALID"
text = "Button URL invalid"
match = 'BUTTON_URL_INVALID'
text = 'Button URL invalid'
class URLHostIsEmpty(BadRequest):
match = "URL host is empty"
match = 'URL host is empty'
class StartParamInvalid(BadRequest):
match = "START_PARAM_INVALID"
text = "Start param invalid"
match = 'START_PARAM_INVALID'
text = 'Start param invalid'
class ButtonDataInvalid(BadRequest):
match = "BUTTON_DATA_INVALID"
text = "Button data invalid"
match = 'BUTTON_DATA_INVALID'
text = 'Button data invalid'
class WrongFileIdentifier(BadRequest):
match = "wrong file identifier/HTTP URL specified"
match = 'wrong file identifier/HTTP URL specified'
class GroupDeactivated(BadRequest):
match = "group is deactivated"
match = 'group is deactivated'
class PhotoAsInputFileRequired(BadRequest):
"""
Will be raised when you try to set chat photo from file ID.
"""
match = "Photo should be uploaded as an InputFile"
match = 'Photo should be uploaded as an InputFile'
class InvalidStickersSet(BadRequest):
match = "STICKERSET_INVALID"
text = "Stickers set is invalid"
match = 'STICKERSET_INVALID'
text = 'Stickers set is invalid'
class NoStickerInRequest(BadRequest):
match = "there is no sticker in the request"
match = 'there is no sticker in the request'
class ChatAdminRequired(BadRequest):
match = "CHAT_ADMIN_REQUIRED"
text = "Admin permissions is required!"
match = 'CHAT_ADMIN_REQUIRED'
text = 'Admin permissions is required!'
class NeedAdministratorRightsInTheChannel(BadRequest):
match = "need administrator rights in the channel chat"
text = "Admin permissions is required!"
match = 'need administrator rights in the channel chat'
text = 'Admin permissions is required!'
class NotEnoughRightsToPinMessage(BadRequest):
match = "not enough rights to pin a message"
match = 'not enough rights to pin a message'
class MethodNotAvailableInPrivateChats(BadRequest):
match = "method is available only for supergroups and channel"
match = 'method is available only for supergroups and channel'
class CantDemoteChatCreator(BadRequest):
match = "can't demote chat creator"
match = 'can\'t demote chat creator'
class CantRestrictSelf(BadRequest):
@ -396,34 +386,34 @@ class CantRestrictSelf(BadRequest):
class NotEnoughRightsToRestrict(BadRequest):
match = "not enough rights to restrict/unrestrict chat member"
match = 'not enough rights to restrict/unrestrict chat member'
class PhotoDimensions(BadRequest):
match = "PHOTO_INVALID_DIMENSIONS"
text = "Invalid photo dimensions"
match = 'PHOTO_INVALID_DIMENSIONS'
text = 'Invalid photo dimensions'
class UnavailableMembers(BadRequest):
match = "supergroup members are unavailable"
match = 'supergroup members are unavailable'
class TypeOfFileMismatch(BadRequest):
match = "type of file mismatch"
match = 'type of file mismatch'
class WrongRemoteFileIdSpecified(BadRequest):
match = "wrong remote file id specified"
match = 'wrong remote file id specified'
class PaymentProviderInvalid(BadRequest):
match = "PAYMENT_PROVIDER_INVALID"
text = "payment provider invalid"
match = 'PAYMENT_PROVIDER_INVALID'
text = 'payment provider invalid'
class CurrencyTotalAmountInvalid(BadRequest):
match = "currency_total_amount_invalid"
text = "currency total amount invalid"
match = 'currency_total_amount_invalid'
text = 'currency total amount invalid'
class BadWebhook(BadRequest):
@ -431,44 +421,44 @@ class BadWebhook(BadRequest):
class WebhookRequireHTTPS(BadWebhook):
match = "HTTPS url must be provided for webhook"
text = "bad webhook: " + match
match = 'HTTPS url must be provided for webhook'
text = 'bad webhook: ' + match
class BadWebhookPort(BadWebhook):
match = "Webhook can be set up only on ports 80, 88, 443 or 8443"
text = "bad webhook: " + match
match = 'Webhook can be set up only on ports 80, 88, 443 or 8443'
text = 'bad webhook: ' + match
class BadWebhookAddrInfo(BadWebhook):
match = "getaddrinfo: Temporary failure in name resolution"
text = "bad webhook: " + match
match = 'getaddrinfo: Temporary failure in name resolution'
text = 'bad webhook: ' + match
class BadWebhookNoAddressAssociatedWithHostname(BadWebhook):
match = "failed to resolve host: no address associated with hostname"
match = 'failed to resolve host: no address associated with hostname'
class CantParseUrl(BadRequest):
match = "can't parse URL"
match = 'can\'t parse URL'
class UnsupportedUrlProtocol(BadRequest):
match = "unsupported URL protocol"
match = 'unsupported URL protocol'
class CantParseEntities(BadRequest):
match = "can't parse entities"
match = 'can\'t parse entities'
class ResultIdDuplicate(BadRequest):
match = "result_id_duplicate"
text = "Result ID duplicate"
match = 'result_id_duplicate'
text = 'Result ID duplicate'
class BotDomainInvalid(BadRequest):
match = "bot_domain_invalid"
text = "Invalid bot domain"
match = 'bot_domain_invalid'
text = 'Invalid bot domain'
class NotFound(TelegramAPIError, _MatchErrorMixin):
@ -476,7 +466,7 @@ class NotFound(TelegramAPIError, _MatchErrorMixin):
class MethodNotKnown(NotFound):
match = "method not found"
match = 'method not found'
class ConflictError(TelegramAPIError, _MatchErrorMixin):
@ -484,15 +474,13 @@ class ConflictError(TelegramAPIError, _MatchErrorMixin):
class TerminatedByOtherGetUpdates(ConflictError):
match = "terminated by other getUpdates request"
text = (
"Terminated by other getUpdates request; "
"Make sure that only one bot instance is running"
)
match = 'terminated by other getUpdates request'
text = 'Terminated by other getUpdates request; ' \
'Make sure that only one bot instance is running'
class CantGetUpdates(ConflictError):
match = "can't use getUpdates method while webhook is active"
match = 'can\'t use getUpdates method while webhook is active'
class Unauthorized(TelegramAPIError, _MatchErrorMixin):
@ -500,23 +488,23 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
class BotKicked(Unauthorized):
match = "Bot was kicked from a chat"
match = 'bot was kicked from a chat'
class BotBlocked(Unauthorized):
match = "bot was blocked by the user"
match = 'bot was blocked by the user'
class UserDeactivated(Unauthorized):
match = "user is deactivated"
match = 'user is deactivated'
class CantInitiateConversation(Unauthorized):
match = "bot can't initiate conversation with a user"
match = 'bot can\'t initiate conversation with a user'
class CantTalkWithBots(Unauthorized):
match = "bot can't send messages to bots"
match = 'bot can\'t send messages to bots'
class NetworkError(TelegramAPIError):
@ -525,43 +513,34 @@ class NetworkError(TelegramAPIError):
class RestartingTelegram(TelegramAPIError):
def __init__(self):
super(RestartingTelegram, self).__init__(
"The Telegram Bot API service is restarting. Wait few second."
)
super(RestartingTelegram, self).__init__('The Telegram Bot API service is restarting. Wait few second.')
class RetryAfter(TelegramAPIError):
def __init__(self, retry_after):
super(RetryAfter, self).__init__(
f"Flood control exceeded. Retry in {retry_after} seconds."
)
super(RetryAfter, self).__init__(f"Flood control exceeded. Retry in {retry_after} seconds.")
self.timeout = retry_after
class MigrateToChat(TelegramAPIError):
def __init__(self, chat_id):
super(MigrateToChat, self).__init__(
f"The group has been migrated to a supergroup. New id: {chat_id}."
)
super(MigrateToChat, self).__init__(f"The group has been migrated to a supergroup. New id: {chat_id}.")
self.migrate_to_chat_id = chat_id
class Throttled(TelegramAPIError):
def __init__(self, **kwargs):
from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT
self.key = kwargs.pop(KEY, "<None>")
self.key = kwargs.pop(KEY, '<None>')
self.called_at = kwargs.pop(LAST_CALL, time.time())
self.rate = kwargs.pop(RATE_LIMIT, None)
self.result = kwargs.pop(RESULT, False)
self.exceeded_count = kwargs.pop(EXCEEDED_COUNT, 0)
self.delta = kwargs.pop(DELTA, 0)
self.user = kwargs.pop("user", None)
self.chat = kwargs.pop("chat", None)
self.user = kwargs.pop('user', None)
self.chat = kwargs.pop('chat', None)
def __str__(self):
return (
f"Rate limit exceeded! (Limit: {self.rate} s, "
f"exceeded: {self.exceeded_count}, "
return f"Rate limit exceeded! (Limit: {self.rate} s, " \
f"exceeded: {self.exceeded_count}, " \
f"time delta: {round(self.delta, 3)} s)"
)

View file

@ -12,27 +12,18 @@ from ..bot.api import log
from ..dispatcher.dispatcher import Dispatcher
from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, WebhookRequestHandler
APP_EXECUTOR_KEY = "APP_EXECUTOR"
APP_EXECUTOR_KEY = 'APP_EXECUTOR'
def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
if on_startup is not None:
executor.on_startup(on_startup)
if on_shutdown is not None:
executor.on_shutdown(on_shutdown)
def start_polling(
dispatcher,
*,
loop=None,
skip_updates=False,
reset_webhook=True,
on_startup=None,
on_shutdown=None,
timeout=20,
fast=True,
):
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@ -47,22 +38,14 @@ def start_polling(
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
def set_webhook(
dispatcher: Dispatcher,
webhook_path: str,
*,
loop: Optional[asyncio.AbstractEventLoop] = None,
skip_updates: bool = None,
on_startup: Optional[Callable] = None,
on_shutdown: Optional[Callable] = None,
check_ip: bool = False,
retry_after: Optional[Union[str, int]] = None,
route_name: str = DEFAULT_ROUTE_NAME,
web_app: Optional[Application] = None,
):
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
skip_updates: bool = None, on_startup: Optional[Callable] = None,
on_shutdown: Optional[Callable] = None, check_ip: bool = False,
retry_after: Optional[Union[str, int]] = None, route_name: str = DEFAULT_ROUTE_NAME,
web_app: Optional[Application] = None):
"""
Set webhook for bot
@ -78,32 +61,17 @@ def set_webhook(
:param web_app: Optional[Application] (default: None)
:return:
"""
executor = Executor(
dispatcher,
skip_updates=skip_updates,
check_ip=check_ip,
retry_after=retry_after,
loop=loop,
)
executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, retry_after=retry_after,
loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
executor.set_webhook(webhook_path, route_name=route_name, web_app=web_app)
return executor
def start_webhook(
dispatcher,
webhook_path,
*,
loop=None,
skip_updates=None,
on_startup=None,
on_shutdown=None,
check_ip=False,
retry_after=None,
route_name=DEFAULT_ROUTE_NAME,
**kwargs,
):
def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None,
on_startup=None, on_shutdown=None, check_ip=False, retry_after=None, route_name=DEFAULT_ROUTE_NAME,
**kwargs):
"""
Start bot in webhook mode
@ -118,21 +86,20 @@ def start_webhook(
:param kwargs:
:return:
"""
executor = set_webhook(
dispatcher=dispatcher,
webhook_path=webhook_path,
loop=loop,
skip_updates=skip_updates,
on_startup=on_startup,
on_shutdown=on_shutdown,
check_ip=check_ip,
retry_after=retry_after,
route_name=route_name,
)
executor = set_webhook(dispatcher=dispatcher,
webhook_path=webhook_path,
loop=loop,
skip_updates=skip_updates,
on_startup=on_startup,
on_shutdown=on_shutdown,
check_ip=check_ip,
retry_after=retry_after,
route_name=route_name)
executor.run_app(**kwargs)
def start(dispatcher, future, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None):
def start(dispatcher, future, *, loop=None, skip_updates=None,
on_startup=None, on_shutdown=None):
"""
Execute Future.
@ -175,7 +142,6 @@ class Executor:
self._freeze = False
from aiogram import Bot, Dispatcher
Bot.set_current(dispatcher.bot)
Dispatcher.set_current(dispatcher)
@ -194,7 +160,7 @@ class Executor:
@property
def web_app(self) -> web.Application:
if self._web_app is None:
raise RuntimeError("web.Application() is not configured!")
raise RuntimeError('web.Application() is not configured!')
return self._web_app
def on_startup(self, callback: callable, polling=True, webhook=True):
@ -207,7 +173,7 @@ class Executor:
"""
self._check_frozen()
if not webhook and not polling:
warn("This action has no effect!", UserWarning)
warn('This action has no effect!', UserWarning)
return
if isinstance(callback, (list, tuple, set)):
@ -230,7 +196,7 @@ class Executor:
"""
self._check_frozen()
if not webhook and not polling:
warn("This action has no effect!", UserWarning)
warn('This action has no effect!', UserWarning)
return
if isinstance(callback, (list, tuple, set)):
@ -245,7 +211,7 @@ class Executor:
def _check_frozen(self):
if self.frozen:
raise RuntimeError("Executor is frozen!")
raise RuntimeError('Executor is frozen!')
def _prepare_polling(self):
self._check_frozen()
@ -253,9 +219,7 @@ class Executor:
# self.loop.set_task_factory(context.task_factory)
def _prepare_webhook(
self, path=None, handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME, app=None
):
def _prepare_webhook(self, path=None, handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME, app=None):
self._check_frozen()
self._freeze = True
@ -269,14 +233,14 @@ class Executor:
raise RuntimeError("web.Application() is already configured!")
if self.retry_after:
app["RETRY_AFTER"] = self.retry_after
app['RETRY_AFTER'] = self.retry_after
if self._identity == app.get(self._identity):
# App is already configured
return
if path is not None:
app.router.add_route("*", path, handler, name=route_name)
app.router.add_route('*', path, handler, name=route_name)
async def _wrap_callback(cb, _):
return await cb(self.dispatcher)
@ -294,15 +258,10 @@ class Executor:
app[APP_EXECUTOR_KEY] = self
app[BOT_DISPATCHER_KEY] = self.dispatcher
app[self._identity] = datetime.datetime.now()
app["_check_ip"] = self.check_ip
app['_check_ip'] = self.check_ip
def set_webhook(
self,
webhook_path: Optional[str] = None,
request_handler: Any = WebhookRequestHandler,
route_name: str = DEFAULT_ROUTE_NAME,
web_app: Optional[Application] = None,
):
def set_webhook(self, webhook_path: Optional[str] = None, request_handler: Any = WebhookRequestHandler,
route_name: str = DEFAULT_ROUTE_NAME, web_app: Optional[Application] = None):
"""
Set webhook for bot
@ -318,13 +277,8 @@ class Executor:
def run_app(self, **kwargs):
web.run_app(self._web_app, **kwargs)
def start_webhook(
self,
webhook_path=None,
request_handler=WebhookRequestHandler,
route_name=DEFAULT_ROUTE_NAME,
**kwargs,
):
def start_webhook(self, webhook_path=None, request_handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME,
**kwargs):
"""
Start bot in webhook mode
@ -334,12 +288,10 @@ class Executor:
:param kwargs:
:return:
"""
self.set_webhook(
webhook_path=webhook_path, request_handler=request_handler, route_name=route_name
)
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
self.run_app(**kwargs)
def start_polling(self, reset_webhook=None, timeout=20, fast=True):
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
"""
Start bot in long-polling mode
@ -351,11 +303,8 @@ class Executor:
try:
loop.run_until_complete(self._startup_polling())
loop.create_task(
self.dispatcher.start_polling(
reset_webhook=reset_webhook, timeout=timeout, fast=fast
)
)
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
relax=relax, fast=fast))
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
# loop.stop()
@ -391,7 +340,7 @@ class Executor:
async def _skip_updates(self):
await self.dispatcher.reset_webhook(True)
await self.dispatcher.skip_updates()
log.warning(f"Updates are skipped successfully.")
log.warning(f'Updates were skipped successfully.')
async def _welcome(self):
user = await self.dispatcher.bot.me
@ -412,11 +361,11 @@ class Executor:
await callback(self.dispatcher)
async def _shutdown_polling(self, wait_closed=False):
await self._shutdown()
for callback in self._on_shutdown_polling:
await callback(self.dispatcher)
await self._shutdown()
if wait_closed:
await self.dispatcher.wait_closed()

View file

@ -13,10 +13,13 @@ Example:
>>> print(MyHelper.all())
<<< ['barItem', 'bazItem', 'fooItem', 'lorem']
"""
from typing import List
PROPS_KEYS_ATTR_NAME = '_props_keys'
class Helper:
mode = ""
mode = ''
@classmethod
def all(cls):
@ -37,13 +40,13 @@ class Helper:
class HelperMode(Helper):
mode = "original"
mode = 'original'
SCREAMING_SNAKE_CASE = "SCREAMING_SNAKE_CASE"
lowerCamelCase = "lowerCamelCase"
CamelCase = "CamelCase"
snake_case = "snake_case"
lowercase = "lowercase"
SCREAMING_SNAKE_CASE = 'SCREAMING_SNAKE_CASE'
lowerCamelCase = 'lowerCamelCase'
CamelCase = 'CamelCase'
snake_case = 'snake_case'
lowercase = 'lowercase'
@classmethod
def all(cls):
@ -65,10 +68,10 @@ class HelperMode(Helper):
"""
if text.isupper():
return text
result = ""
result = ''
for pos, symbol in enumerate(text):
if symbol.isupper() and pos > 0:
result += "_" + symbol
result += '_' + symbol
else:
result += symbol.upper()
return result
@ -94,10 +97,10 @@ class HelperMode(Helper):
:param first_upper: first symbol must be upper?
:return:
"""
result = ""
result = ''
need_upper = False
for pos, symbol in enumerate(text):
if symbol == "_" and pos > 0:
if symbol == '_' and pos > 0:
need_upper = True
else:
if need_upper:
@ -120,15 +123,15 @@ class HelperMode(Helper):
"""
if mode == cls.SCREAMING_SNAKE_CASE:
return cls._screaming_snake_case(text)
elif mode == cls.snake_case:
if mode == cls.snake_case:
return cls._snake_case(text)
elif mode == cls.lowercase:
return cls._snake_case(text).replace("_", "")
elif mode == cls.lowerCamelCase:
if mode == cls.lowercase:
return cls._snake_case(text).replace('_', '')
if mode == cls.lowerCamelCase:
return cls._camel_case(text)
elif mode == cls.CamelCase:
if mode == cls.CamelCase:
return cls._camel_case(text, True)
elif callable(mode):
if callable(mode):
return mode(text)
return text
@ -149,10 +152,10 @@ class Item:
def __set_name__(self, owner, name):
if not name.isupper():
raise NameError("Name for helper item must be in uppercase!")
raise NameError('Name for helper item must be in uppercase!')
if not self._value:
if hasattr(owner, "mode"):
self._value = HelperMode.apply(name, getattr(owner, "mode"))
if hasattr(owner, 'mode'):
self._value = HelperMode.apply(name, getattr(owner, 'mode'))
class ListItem(Item):
@ -191,3 +194,36 @@ class ItemsList(list):
return self
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
class OrderedHelperMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super().__new__(mcs, name, bases, namespace)
props_keys = []
for prop_name in (name for name, prop in namespace.items() if isinstance(prop, (Item, ListItem))):
props_keys.append(prop_name)
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
return cls
class OrderedHelper(metaclass=OrderedHelperMeta):
mode = ''
@classmethod
def all(cls) -> List[str]:
"""
Get all Items values
"""
result = []
for name in getattr(cls, PROPS_KEYS_ATTR_NAME, []):
value = getattr(cls, name)
if isinstance(value, ItemsList):
result.append(value[0])
else:
result.append(value)
return result

View file

@ -1,14 +1,14 @@
import importlib
import os
JSON = "json"
RAPIDJSON = "rapidjson"
UJSON = "ujson"
JSON = 'json'
RAPIDJSON = 'rapidjson'
UJSON = 'ujson'
# Detect mode
mode = JSON
for json_lib in (RAPIDJSON, UJSON):
if "DISABLE_" + json_lib.upper() in os.environ:
if 'DISABLE_' + json_lib.upper() in os.environ:
continue
try:
@ -20,35 +20,28 @@ for json_lib in (RAPIDJSON, UJSON):
break
if mode == RAPIDJSON:
def dumps(data):
return json.dumps(
data,
ensure_ascii=False,
number_mode=json.NM_NATIVE,
datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC,
)
def loads(data):
return json.loads(
data, number_mode=json.NM_NATIVE, datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC
)
elif mode == UJSON:
def loads(data):
return json.loads(data)
def dumps(data):
return json.dumps(data, ensure_ascii=False)
def loads(data):
return json.loads(data, number_mode=json.NM_NATIVE)
elif mode == UJSON:
def loads(data):
return json.loads(data)
def dumps(data):
return json.dumps(data, ensure_ascii=False)
else:
import json
def dumps(data):
return json.dumps(data, ensure_ascii=False)
def loads(data):
return json.loads(data)

View file

@ -1,16 +1,16 @@
import contextvars
from typing import TypeVar, Type
__all__ = ("DataMixin", "ContextInstanceMixin")
__all__ = ('DataMixin', 'ContextInstanceMixin')
class DataMixin:
@property
def data(self):
data = getattr(self, "_data", None)
data = getattr(self, '_data', None)
if data is None:
data = {}
setattr(self, "_data", data)
setattr(self, '_data', data)
return data
def __getitem__(self, item):
@ -26,12 +26,12 @@ class DataMixin:
return self.data.get(key, default)
T = TypeVar("T")
T = TypeVar('T')
class ContextInstanceMixin:
def __init_subclass__(cls, **kwargs):
cls.__context_instance = contextvars.ContextVar("instance_" + cls.__name__)
cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}')
return cls
@classmethod
@ -43,7 +43,5 @@ class ContextInstanceMixin:
@classmethod
def set_current(cls: Type[T], value: T):
if not isinstance(value, cls):
raise TypeError(
f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'"
)
raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}')
cls.__context_instance.set(value)

View file

@ -6,7 +6,7 @@ from babel.support import LazyProxy
from aiogram import types
from . import json
DEFAULT_FILTER = ["self", "cls"]
DEFAULT_FILTER = ['self', 'cls']
def generate_payload(exclude=None, **kwargs):
@ -21,11 +21,10 @@ def generate_payload(exclude=None, **kwargs):
"""
if exclude is None:
exclude = []
return {
key: value
for key, value in kwargs.items()
if key not in exclude + DEFAULT_FILTER and value is not None and not key.startswith("_")
}
return {key: value for key, value in kwargs.items() if
key not in exclude + DEFAULT_FILTER
and value is not None
and not key.startswith('_')}
def _normalize(obj):
@ -39,7 +38,7 @@ def _normalize(obj):
return [_normalize(item) for item in obj]
elif isinstance(obj, dict):
return {k: _normalize(v) for k, v in obj.items() if v is not None}
elif hasattr(obj, "to_python"):
elif hasattr(obj, 'to_python'):
return obj.to_python()
return obj
@ -53,14 +52,14 @@ def prepare_arg(value):
"""
if value is None:
return value
elif isinstance(value, (list, dict)) or hasattr(value, "to_python"):
if isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
return json.dumps(_normalize(value))
elif isinstance(value, datetime.timedelta):
if isinstance(value, datetime.timedelta):
now = datetime.datetime.now()
return int((now + value).timestamp())
elif isinstance(value, datetime.datetime):
if isinstance(value, datetime.datetime):
return round(value.timestamp())
elif isinstance(value, LazyProxy):
if isinstance(value, LazyProxy):
return str(value)
return value

View file

@ -15,5 +15,4 @@ sphinx-rtd-theme>=0.4.3
sphinxcontrib-programoutput>=0.14
aiohttp-socks>=0.2.2
rethinkdb>=2.4.1
lxml==4.3.4
requests==2.22.0
coverage==4.5.3

View file

@ -111,6 +111,30 @@ ExceptionsFilter
:show-inheritance:
IDFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter
:members:
:show-inheritance:
AdminFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter
:members:
:show-inheritance:
IsReplyFilter
-------------
.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter
:members:
:show-inheritance:
Making own filters (Custom filters)
===================================
@ -156,3 +180,4 @@ BoundFilter
dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers])

View file

@ -1,28 +1,28 @@
.. Autogenerated file at 2018-10-28 19:31:48.335963
=========================
Adwanced executor example
Advanced executor example
=========================
!/usr/bin/env python3
**This example is outdated**
In this example used ArgumentParser for configuring Your bot.
Provided to start bot with webhook:
python adwanced_executor_example.py \
python advanced_executor_example.py \
--token TOKEN_HERE \
--host 0.0.0.0 \
--port 8084 \
--host-name example.com \
--webhook-port 443
Or long polling:
python adwanced_executor_example.py --token TOKEN_HERE
python advanced_executor_example.py --token TOKEN_HERE
So... In this example found small trouble:
can't get bot instance in handlers.
If you want to automatic change getting updates method use executor utils (from aiogram.utils.executor)
TODO: Move token to environment variables.
.. literalinclude:: ../../../examples/adwanced_executor_example.py
:caption: adwanced_executor_example.py
.. literalinclude:: ../../../examples/advanced_executor_example.py
:caption: advanced_executor_example.py
:language: python
:linenos:
:lines: 25-

View file

@ -6,7 +6,7 @@ Examples
echo_bot
inline_bot
adwanced_executor_example
advanced_executor_example
proxy_and_emojize
finite_state_machine_example
throtling_example

View file

@ -22,18 +22,14 @@ 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
.. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest?style=flat-square
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
:target: http://aiogram.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square
:target: https://github.com/python/black
:alt: Code style: Black
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
:target: https://github.com/aiogram/aiogram/issues
:alt: Github issues
@ -43,7 +39,7 @@ Welcome to aiogram's documentation!
:alt: MIT License
**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.7 with `asyncio <https://docs.python.org/3/library/asyncio.html>`_ and `aiohttp <https://github.com/aio-libs/aiohttp>`_. It helps you to make your bots faster and simpler.
**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.7 with `asyncio <https://docs.python.org/3/library/asyncio.html>`_ and `aiohttp <https://github.com/aio-libs/aiohttp>`_. It helps you to make your bots faster and simpler.
Official aiogram resources

View file

@ -7,21 +7,34 @@ Using PIP
$ pip install -U aiogram
Using Pipenv
------------
.. code-block:: bash
$ pipenv install aiogram
Using AUR
---------
*aiogram* is also available in Arch User Repository, so you can install this library on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram <https://aur.archlinux.org/packages/python-aiogram/>`_ package.
From sources
------------
Development versions:
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
$ python setup.py install
or if you want to install development version (maybe unstable):
Or if you want to install stable version (The same with version form PyPi):
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
$ git checkout dev-2.x
$ git checkout master
$ python setup.py install
@ -49,4 +62,36 @@ You can speedup your bots by following next instructions:
$ pip install ujson
- Use aiohttp speedups
- Use `cchardet <https://github.com/PyYoshi/cChardet>`_ instead of chardet module.
*cChardet* is high speed universal character encoding detector.
**Installation:**
.. code-block:: bash
$ pip install cchardet
- Use `aiodns <https://github.com/saghul/aiodns>`_ for speeding up DNS resolving.
*aiodns* provides a simple way for doing asynchronous DNS resolutions.
**Installation:**
.. code-block:: bash
$ pip install aiodns
- Installing speedups altogether.
The following will get you ``aiohttp`` along with ``cchardet``, ``aiodns`` and ``brotlipy`` in one bundle.
**Installation:**
.. code-block:: bash
$ pip install aiohttp[speedups]
In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment.

View file

@ -1,4 +1,6 @@
===========
Auth Widget
===========
Coming soon...
.. automodule:: aiogram.utils.auth_widget
:members:

View file

@ -1,4 +0,0 @@
=======
Context
=======
Coming soon...

View file

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

View file

@ -1,4 +1,6 @@
=====
Emoji
=====
Coming soon...
.. automodule:: aiogram.utils.emoji
:members:

View file

@ -1,4 +1,6 @@
==========
Exceptions
==========
Coming soon...
.. automodule:: aiogram.utils.exceptions
:members:

View file

@ -1,4 +1,7 @@
========
Executor
========
Coming soon...
.. automodule:: aiogram.utils.executor
:members:

View file

@ -1,4 +1,6 @@
======
Helper
======
Coming soon...
.. automodule:: aiogram.utils.helper
:members:

View file

@ -3,14 +3,13 @@ Utils
.. toctree::
auth_widget
executor
exceptions
context
markdown
helper
auth_widget
deprecated
payload
parts
json
emoji
deprecated

View file

@ -1,4 +1,6 @@
====
JSON
====
Coming soon...
.. automodule:: aiogram.utils.json
:members:

View file

@ -1,4 +1,6 @@
========
Markdown
========
Coming soon...
.. automodule:: aiogram.utils.markdown
:members:

View file

@ -1,4 +1,6 @@
=====
Parts
=====
Coming soon...
.. automodule:: aiogram.utils.parts
:members:

View file

@ -1,4 +1,6 @@
=======
Payload
=======
Coming soon...
.. automodule:: aiogram.utils.payload
:members:

View file

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

View file

@ -4,7 +4,7 @@
In this example used ArgumentParser for configuring Your bot.
Provided to start bot with webhook:
python adwanced_executor_example.py \
python advanced_executor_example.py \
--token TOKEN_HERE \
--host 0.0.0.0 \
--port 8084 \
@ -12,7 +12,7 @@ Provided to start bot with webhook:
--webhook-port 443
Or long polling:
python adwanced_executor_example.py --token TOKEN_HERE
python advanced_executor_example.py --token TOKEN_HERE
So... In this example found small trouble:
can't get bot instance in handlers.

View file

@ -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():

View file

@ -1,4 +1,3 @@
import asyncio
import logging
import random
import uuid
@ -11,27 +10,26 @@ from aiogram.utils.exceptions import MessageNotModified, Throttled
logging.basicConfig(level=logging.INFO)
API_TOKEN = "BOT TOKEN HERE"
API_TOKEN = 'BOT TOKEN HERE'
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
dp.middleware.setup(LoggingMiddleware())
POSTS = {
str(uuid.uuid4()): {
"title": f"Post {index}",
"body": "Lorem ipsum dolor sit amet, "
"consectetur adipiscing elit, "
"sed do eiusmod tempor incididunt ut "
"labore et dolore magna aliqua",
"votes": random.randint(-2, 5),
}
for index in range(1, 6)
'title': f'Post {index}',
'body': 'Lorem ipsum dolor sit amet, '
'consectetur adipiscing elit, '
'sed do eiusmod tempor incididunt ut '
'labore et dolore magna aliqua',
'votes': random.randint(-2, 5),
} for index in range(1, 6)
}
posts_cb = CallbackData("post", "id", "action") # post:<id>:<action>
posts_cb = CallbackData('post', 'id', 'action') # post:<id>:<action>
def get_keyboard() -> types.InlineKeyboardMarkup:
@ -42,73 +40,72 @@ def get_keyboard() -> types.InlineKeyboardMarkup:
for post_id, post in POSTS.items():
markup.add(
types.InlineKeyboardButton(
post["title"], callback_data=posts_cb.new(id=post_id, action="view")
)
post['title'],
callback_data=posts_cb.new(id=post_id, action='view')),
)
return markup
def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup):
text = (
f"{md.hbold(post['title'])}\n"
f"{md.quote_html(post['body'])}\n"
f"\n"
f"Votes: {post['votes']}"
text = md.text(
md.hbold(post['title']),
md.quote_html(post['body']),
'', # just new empty line
f"Votes: {post['votes']}",
sep = '\n',
)
markup = types.InlineKeyboardMarkup()
markup.row(
types.InlineKeyboardButton("👍", callback_data=posts_cb.new(id=post_id, action="like")),
types.InlineKeyboardButton("👎", callback_data=posts_cb.new(id=post_id, action="unlike")),
)
markup.add(
types.InlineKeyboardButton("<< Back", callback_data=posts_cb.new(id="-", action="list"))
types.InlineKeyboardButton('👍', callback_data=posts_cb.new(id=post_id, action='like')),
types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='dislike')),
)
markup.add(types.InlineKeyboardButton('<< Back', callback_data=posts_cb.new(id='-', action='list')))
return text, markup
@dp.message_handler(commands="start")
@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
await message.reply("Posts", reply_markup=get_keyboard())
await message.reply('Posts', reply_markup=get_keyboard())
@dp.callback_query_handler(posts_cb.filter(action="list"))
@dp.callback_query_handler(posts_cb.filter(action='list'))
async def query_show_list(query: types.CallbackQuery):
await query.message.edit_text("Posts", reply_markup=get_keyboard())
await query.message.edit_text('Posts', reply_markup=get_keyboard())
@dp.callback_query_handler(posts_cb.filter(action="view"))
@dp.callback_query_handler(posts_cb.filter(action='view'))
async def query_view(query: types.CallbackQuery, callback_data: dict):
post_id = callback_data["id"]
post_id = callback_data['id']
post = POSTS.get(post_id, None)
if not post:
return await query.answer("Unknown post!")
return await query.answer('Unknown post!')
text, markup = format_post(post_id, post)
await query.message.edit_text(text, reply_markup=markup)
@dp.callback_query_handler(posts_cb.filter(action=["like", "unlike"]))
@dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike']))
async def query_post_vote(query: types.CallbackQuery, callback_data: dict):
try:
await dp.throttle("vote", rate=1)
await dp.throttle('vote', rate=1)
except Throttled:
return await query.answer("Too many requests.")
return await query.answer('Too many requests.')
post_id = callback_data["id"]
action = callback_data["action"]
post_id = callback_data['id']
action = callback_data['action']
post = POSTS.get(post_id, None)
if not post:
return await query.answer("Unknown post!")
return await query.answer('Unknown post!')
if action == "like":
post["votes"] += 1
elif action == "unlike":
post["votes"] -= 1
if action == 'like':
post['votes'] += 1
elif action == 'dislike':
post['votes'] -= 1
await query.answer("Voted.")
await query.answer('Vote accepted')
text, markup = format_post(post_id, post)
await query.message.edit_text(text, reply_markup=markup)
@ -118,5 +115,5 @@ async def message_not_modified_handler(update, error):
return True
if __name__ == "__main__":
executor.start_polling(dp, loop=loop, skip_updates=True)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -0,0 +1,68 @@
"""
This is a simple example of usage of CallbackData factory
For more comprehensive example see callback_data_factory.py
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.utils.callback_data import CallbackData
from aiogram.utils.exceptions import MessageNotModified
logging.basicConfig(level=logging.INFO)
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
dp.middleware.setup(LoggingMiddleware())
vote_cb = CallbackData('vote', 'action') # vote:<action>
likes = {} # user_id: amount_of_likes
def get_keyboard():
return types.InlineKeyboardMarkup().row(
types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')),
types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')),
)
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0
await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard())
@dp.callback_query_handler(vote_cb.filter(action=['up', 'down']))
async def callback_vote_action(query: types.CallbackQuery, callback_data: dict):
logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data
await query.answer() # don't forget to answer callback query as soon as possible
callback_data_action = callback_data['action']
likes_count = likes.get(query.from_user.id, 0)
if callback_data_action == 'up':
likes_count += 1
else:
likes_count -= 1
likes[query.from_user.id] = likes_count # update amount of likes in storage
await bot.edit_message_text(
f'You voted {callback_data_action}! Now you have {likes_count} vote[s].',
query.from_user.id,
query.message.message_id,
reply_markup=get_keyboard(),
)
@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
async def message_not_modified_handler(update, error):
return True
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -2,17 +2,16 @@
Babel is required.
"""
import asyncio
import logging
from aiogram import Bot, Dispatcher, executor, md, types
API_TOKEN = "BOT TOKEN HERE"
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)
@ -20,17 +19,15 @@ dp = Dispatcher(bot)
async def check_language(message: types.Message):
locale = message.from_user.locale
await message.reply(
md.text(
md.bold("Info about your language:"),
md.text(" 🔸", md.bold("Code:"), md.italic(locale.locale)),
md.text(" 🔸", md.bold("Territory:"), md.italic(locale.territory or "Unknown")),
md.text(" 🔸", md.bold("Language name:"), md.italic(locale.language_name)),
md.text(" 🔸", md.bold("English language name:"), md.italic(locale.english_name)),
sep="\n",
)
)
await message.reply(md.text(
md.bold('Info about your language:'),
md.text('🔸', md.bold('Code:'), md.code(locale.language)),
md.text('🔸', md.bold('Territory:'), md.code(locale.territory or 'Unknown')),
md.text('🔸', md.bold('Language name:'), md.code(locale.language_name)),
md.text('🔸', md.bold('English language name:'), md.code(locale.english_name)),
sep='\n',
))
if __name__ == "__main__":
executor.start_polling(dp, loop=loop, skip_updates=True)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -7,7 +7,7 @@ import logging
from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = "BOT TOKEN HERE"
API_TOKEN = 'BOT TOKEN HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
@ -17,29 +17,37 @@ bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(commands=["start", "help"])
@dp.message_handler(commands=['start', 'help'])
async def send_welcome(message: types.Message):
"""
This handler will be called when client send `/start` or `/help` commands.
This handler will be called when user sends `/start` or `/help` command
"""
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
@dp.message_handler(regexp="(^cat[s]?$|puss)")
@dp.message_handler(regexp='(^cat[s]?$|puss)')
async def cats(message: types.Message):
with open("data/cats.jpg", "rb") as photo:
with open('data/cats.jpg', 'rb') as photo:
'''
# Old fashioned way:
await bot.send_photo(
message.chat.id,
photo,
caption="Cats is here 😺",
caption='Cats are here 😺',
reply_to_message_id=message.message_id,
)
'''
await message.reply_photo(photo, caption='Cats are here 😺')
@dp.message_handler()
async def echo(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# old style:
# await bot.send_message(message.chat.id, message.text)
await message.reply(message.text, reply=False)
if __name__ == "__main__":
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

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

View file

@ -3,6 +3,19 @@ Internalize your bot
Step 1: extract texts
# pybabel extract i18n_example.py -o locales/mybot.pot
Some useful options:
- Extract texts with pluralization support
# -k __:1,2
- Add comments for translators, you can use another tag if you want (TR)
# --add-comments=NOTE
- Disable comments with string location in code
# --no-location
- Set project name
# --project=MySuperBot
- Set version
# --version=2.2
Step 2: create *.po files. For e.g. create en, ru, uk locales.
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
Step 3: translate texts
@ -24,11 +37,11 @@ from pathlib import Path
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.middlewares.i18n import I18nMiddleware
TOKEN = "BOT TOKEN HERE"
I18N_DOMAIN = "mybot"
TOKEN = 'BOT_TOKEN_HERE'
I18N_DOMAIN = 'mybot'
BASE_DIR = Path(__file__).parent
LOCALES_DIR = BASE_DIR / "locales"
LOCALES_DIR = BASE_DIR / 'locales'
bot = Bot(TOKEN, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot)
@ -41,16 +54,45 @@ dp.middleware.setup(i18n)
_ = i18n.gettext
@dp.message_handler(commands=["start"])
@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
# Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts.
await message.reply(_("Hello, <b>{user}</b>!").format(user=message.from_user.full_name))
await message.reply(_('Hello, <b>{user}</b>!').format(user=message.from_user.full_name))
@dp.message_handler(commands=["lang"])
@dp.message_handler(commands='lang')
async def cmd_lang(message: types.Message, locale):
await message.reply(_("Your current language: <i>{language}</i>").format(language=locale))
# For setting custom lang you have to modify i18n middleware, like this:
# https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py
await message.reply(_('Your current language: <i>{language}</i>').format(language=locale))
# If you care about pluralization, here's small handler
# And also, there's and example of comments for translators. Most translation tools support them.
# Alias for gettext method, parser will understand double underscore as plural (aka ngettext)
__ = i18n.gettext
if __name__ == "__main__":
# some likes manager
LIKES_STORAGE = {'count': 0}
def get_likes() -> int:
return LIKES_STORAGE['count']
def increase_likes() -> int:
LIKES_STORAGE['count'] += 1
return get_likes()
#
@dp.message_handler(commands='like')
async def cmd_like(message: types.Message, locale):
likes = increase_likes()
# NOTE: This is comment for a translator
await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes))
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

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

View file

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

View file

@ -0,0 +1,62 @@
"""
This bot is created for the demonstration of a usage of inline keyboards.
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(commands='start')
async def start_cmd_handler(message: types.Message):
keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually
# kept for clearness
text_and_data = (
('Yes!', 'yes'),
('No!', 'no'),
)
# in real life for the callback_data the callback data factory should be used
# here the raw string is used for the simplicity
row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data)
keyboard_markup.row(*row_btns)
keyboard_markup.add(
# url buttons have no callback data
types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'),
)
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
# Use multiple registrators. Handler will execute when one of the filters is OK
@dp.callback_query_handler(text='no') # if cb.data == 'no'
@dp.callback_query_handler(text='yes') # if cb.data == 'yes'
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
answer_data = query.data
# always answer callback queries, even if you have nothing to say
await query.answer(f'You answered with {answer_data!r}')
if answer_data == 'yes':
text = 'Great, me too!'
elif answer_data == 'no':
text = 'Oh no...Why so?'
else:
text = f'Unexpected callback data {answer_data!r}!'
await bot.send_message(query.from_user.id, text)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

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

View file

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

View file

@ -2,10 +2,10 @@ import asyncio
from aiogram import Bot, Dispatcher, executor, filters, types
API_TOKEN = "BOT TOKEN HERE"
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop)
API_TOKEN = 'BOT_TOKEN_HERE'
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@ -14,23 +14,23 @@ async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Do you want to see many pussies? Are you ready?")
# And wait few seconds...
# Wait a little...
await asyncio.sleep(1)
# Good bots should send chat actions. Or not.
# Good bots should send chat actions...
await types.ChatActions.upload_photo()
# Create media group
media = types.MediaGroup()
# Attach local file
media.attach_photo(types.InputFile("data/cat.jpg"), "Cat!")
media.attach_photo(types.InputFile('data/cat.jpg'), 'Cat!')
# More local files and more cats!
media.attach_photo(types.InputFile("data/cats.jpg"), "More cats!")
media.attach_photo(types.InputFile('data/cats.jpg'), 'More cats!')
# You can also use URL's
# For example: get random puss:
media.attach_photo("http://lorempixel.com/400/200/cats/", "Random cat.")
media.attach_photo('http://lorempixel.com/400/200/cats/', 'Random cat.')
# And you can also use file ID:
# media.attach_photo('<file_id>', 'cat-cat-cat.')
@ -39,5 +39,5 @@ async def send_welcome(message: types.Message):
await message.reply_media_group(media=media)
if __name__ == "__main__":
executor.start_polling(dp, loop=loop, skip_updates=True)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -7,14 +7,12 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
TOKEN = "BOT TOKEN HERE"
loop = asyncio.get_event_loop()
TOKEN = 'BOT_TOKEN_HERE'
# In this example Redis storage is used
storage = RedisStorage2(db=5)
bot = Bot(token=TOKEN, loop=loop)
bot = Bot(token=TOKEN)
dp = Dispatcher(bot, storage=storage)
@ -28,9 +26,9 @@ def rate_limit(limit: int, key=None):
"""
def decorator(func):
setattr(func, "throttling_rate_limit", limit)
setattr(func, 'throttling_rate_limit', limit)
if key:
setattr(func, "throttling_key", key)
setattr(func, 'throttling_key', key)
return func
return decorator
@ -41,7 +39,7 @@ class ThrottlingMiddleware(BaseMiddleware):
Simple middleware
"""
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix="antiflood_"):
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'):
self.rate_limit = limit
self.prefix = key_prefix
super(ThrottlingMiddleware, self).__init__()
@ -59,8 +57,8 @@ class ThrottlingMiddleware(BaseMiddleware):
dispatcher = Dispatcher.get_current()
# If handler was configured, get rate limit and key from handler
if handler:
limit = getattr(handler, "throttling_rate_limit", self.rate_limit)
key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}")
limit = getattr(handler, 'throttling_rate_limit', self.rate_limit)
key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}")
else:
limit = self.rate_limit
key = f"{self.prefix}_message"
@ -85,7 +83,7 @@ class ThrottlingMiddleware(BaseMiddleware):
handler = current_handler.get()
dispatcher = Dispatcher.get_current()
if handler:
key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}")
key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}")
else:
key = f"{self.prefix}_message"
@ -94,7 +92,7 @@ class ThrottlingMiddleware(BaseMiddleware):
# Prevent flooding
if throttled.exceeded_count <= 2:
await message.reply("Too many requests! ")
await message.reply('Too many requests! ')
# Sleep.
await asyncio.sleep(delta)
@ -104,21 +102,19 @@ class ThrottlingMiddleware(BaseMiddleware):
# If current message is not last with current key - do not send message
if thr.exceeded_count == throttled.exceeded_count:
await message.reply("Unlocked.")
await message.reply('Unlocked.')
@dp.message_handler(commands=["start"])
@rate_limit(
5, "start"
) # this is not required but you can configure throttling manager for current handler using it
@dp.message_handler(commands=['start'])
@rate_limit(5, 'start') # this is not required but you can configure throttling manager for current handler using it
async def cmd_test(message: types.Message):
# You can use this command every 5 seconds
await message.reply("Test passed! You can use this command every 5 seconds.")
await message.reply('Test passed! You can use this command every 5 seconds.')
if __name__ == "__main__":
if __name__ == '__main__':
# Setup middleware
dp.middleware.setup(ThrottlingMiddleware())
# Start long-polling
executor.start_polling(dp, loop=loop)
executor.start_polling(dp)

View file

@ -1,121 +1,97 @@
import asyncio
from aiogram import Bot
from aiogram import types
from aiogram.dispatcher import Dispatcher
from aiogram.types.message import ContentTypes
from aiogram.utils import executor
BOT_TOKEN = "BOT TOKEN HERE"
PAYMENTS_PROVIDER_TOKEN = "123456789:TEST:1234567890abcdef1234567890abcdef"
loop = asyncio.get_event_loop()
BOT_TOKEN = 'BOT_TOKEN_HERE'
PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1422'
bot = Bot(BOT_TOKEN)
dp = Dispatcher(bot, loop=loop)
dp = Dispatcher(bot)
# Setup prices
prices = [
types.LabeledPrice(label="Working Time Machine", amount=5750),
types.LabeledPrice(label="Gift wrapping", amount=500),
types.LabeledPrice(label='Working Time Machine', amount=5750),
types.LabeledPrice(label='Gift wrapping', amount=500),
]
# Setup shipping options
shipping_options = [
types.ShippingOption(id="instant", title="WorldWide Teleporter").add(
types.LabeledPrice("Teleporter", 1000)
),
types.ShippingOption(id="pickup", title="Local pickup").add(types.LabeledPrice("Pickup", 300)),
types.ShippingOption(id='instant', title='WorldWide Teleporter').add(types.LabeledPrice('Teleporter', 1000)),
types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)),
]
@dp.message_handler(commands=["start"])
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
await bot.send_message(
message.chat.id,
"Hello, I'm the demo merchant bot."
" I can sell you a Time Machine."
" Use /buy to order one, /terms for Terms and Conditions",
)
await bot.send_message(message.chat.id,
"Hello, I'm the demo merchant bot."
" I can sell you a Time Machine."
" Use /buy to order one, /terms for Terms and Conditions")
@dp.message_handler(commands=["terms"])
@dp.message_handler(commands=['terms'])
async def cmd_terms(message: types.Message):
await bot.send_message(
message.chat.id,
"Thank you for shopping with our demo bot. We hope you like your new time machine!\n"
"1. If your time machine was not delivered on time, please rethink your concept of time"
" and try again.\n"
"2. If you find that your time machine is not working, kindly contact our future service"
" workshops on Trappist-1e. They will be accessible anywhere between"
" May 2075 and November 4000 C.E.\n"
"3. If you would like a refund, kindly apply for one yesterday and we will have sent it"
" to you immediately.",
)
await bot.send_message(message.chat.id,
'Thank you for shopping with our demo bot. We hope you like your new time machine!\n'
'1. If your time machine was not delivered on time, please rethink your concept of time'
' and try again.\n'
'2. If you find that your time machine is not working, kindly contact our future service'
' workshops on Trappist-1e. They will be accessible anywhere between'
' May 2075 and November 4000 C.E.\n'
'3. If you would like a refund, kindly apply for one yesterday and we will have sent it'
' to you immediately.')
@dp.message_handler(commands=["buy"])
@dp.message_handler(commands=['buy'])
async def cmd_buy(message: types.Message):
await bot.send_message(
message.chat.id,
"Real cards won't work with me, no money will be debited from your account."
" Use this test card number to pay for your Time Machine: `4242 4242 4242 4242`"
"\n\nThis is your demo invoice:",
parse_mode="Markdown",
)
await bot.send_invoice(
message.chat.id,
title="Working Time Machine",
description="Want to visit your great-great-great-grandparents?"
" Make a fortune at the races?"
" Shake hands with Hammurabi and take a stroll in the Hanging Gardens?"
" Order our Working Time Machine today!",
provider_token=PAYMENTS_PROVIDER_TOKEN,
currency="usd",
photo_url="https://images.fineartamerica.com/images-medium-large/2-the-time-machine-dmitriy-khristenko.jpg",
photo_height=512, # !=0/None or picture won't be shown
photo_width=512,
photo_size=512,
is_flexible=True, # True If you need to set up Shipping Fee
prices=prices,
start_parameter="time-machine-example",
payload="HAPPY FRIDAYS COUPON",
)
await bot.send_message(message.chat.id,
"Real cards won't work with me, no money will be debited from your account."
" Use this test card number to pay for your Time Machine: `4242 4242 4242 4242`"
"\n\nThis is your demo invoice:", parse_mode='Markdown')
await bot.send_invoice(message.chat.id, title='Working Time Machine',
description='Want to visit your great-great-great-grandparents?'
' Make a fortune at the races?'
' Shake hands with Hammurabi and take a stroll in the Hanging Gardens?'
' Order our Working Time Machine today!',
provider_token=PAYMENTS_PROVIDER_TOKEN,
currency='usd',
photo_url='https://telegra.ph/file/d08ff863531f10bf2ea4b.jpg',
photo_height=512, # !=0/None or picture won't be shown
photo_width=512,
photo_size=512,
is_flexible=True, # True If you need to set up Shipping Fee
prices=prices,
start_parameter='time-machine-example',
payload='HAPPY FRIDAYS COUPON')
@dp.shipping_query_handler(func=lambda query: True)
@dp.shipping_query_handler(lambda query: True)
async def shipping(shipping_query: types.ShippingQuery):
await bot.answer_shipping_query(
shipping_query.id,
ok=True,
shipping_options=shipping_options,
error_message="Oh, seems like our Dog couriers are having a lunch right now."
" Try again later!",
)
await bot.answer_shipping_query(shipping_query.id, ok=True, shipping_options=shipping_options,
error_message='Oh, seems like our Dog couriers are having a lunch right now.'
' Try again later!')
@dp.pre_checkout_query_handler(func=lambda query: True)
@dp.pre_checkout_query_handler(lambda query: True)
async def checkout(pre_checkout_query: types.PreCheckoutQuery):
await bot.answer_pre_checkout_query(
pre_checkout_query.id,
ok=True,
error_message="Aliens tried to steal your card's CVV,"
" but we successfully protected your credentials,"
" try to pay again in a few minutes, we need a small rest.",
)
await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True,
error_message="Aliens tried to steal your card's CVV,"
" but we successfully protected your credentials,"
" try to pay again in a few minutes, we need a small rest.")
@dp.message_handler(content_types=ContentTypes.SUCCESSFUL_PAYMENT)
async def got_payment(message: types.Message):
await bot.send_message(
message.chat.id,
"Hoooooray! Thanks for payment! We will proceed your order for `{} {}`"
" as fast as possible! Stay in touch."
"\n\nUse /buy again to get a Time Machine for your friend!".format(
message.successful_payment.total_amount / 100, message.successful_payment.currency
),
parse_mode="Markdown",
)
await bot.send_message(message.chat.id,
'Hoooooray! Thanks for payment! We will proceed your order for `{} {}`'
' as fast as possible! Stay in touch.'
'\n\nUse /buy again to get a Time Machine for your friend!'.format(
message.successful_payment.total_amount / 100, message.successful_payment.currency),
parse_mode='Markdown')
if __name__ == "__main__":
executor.start_polling(dp, loop=loop)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -1,4 +1,3 @@
import asyncio
import logging
import aiohttp
@ -11,49 +10,52 @@ from aiogram.utils.executor import start_polling
from aiogram.utils.markdown import bold, code, italic, text
# Configure bot here
API_TOKEN = "BOT TOKEN HERE"
PROXY_URL = "http://PROXY_URL" # Or 'socks5://...'
API_TOKEN = 'BOT_TOKEN_HERE'
PROXY_URL = 'http://PROXY_URL' # Or 'socks5://host:port'
# If authentication is required in your proxy then uncomment next line and change login/password for it
# NOTE: If authentication is required in your proxy then uncomment next line and change login/password for it
# PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password')
# And add `proxy_auth=PROXY_AUTH` argument in line 25, like this:
# >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
# And add `proxy_auth=PROXY_AUTH` argument in line 30, like this:
# >>> bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
# Also you can use Socks5 proxy but you need manually install aiohttp_socks package.
# Get my ip URL
GET_IP_URL = "http://bot.whatismyipaddress.com/"
GET_IP_URL = 'http://bot.whatismyipaddress.com/'
logging.basicConfig(level=logging.INFO)
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL)
bot = Bot(token=API_TOKEN, proxy=PROXY_URL)
# If auth is required:
# bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH)
dp = Dispatcher(bot)
async def fetch(url, proxy=None, proxy_auth=None):
async with aiohttp.ClientSession() as session:
async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response:
return await response.text()
async def fetch(url, session):
async with session.get(url) as response:
return await response.text()
@dp.message_handler(commands=["start"])
@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message):
# fetching urls will take some time, so notify user that everything is OK
await types.ChatActions.typing()
content = []
# Make request (without proxy)
ip = await fetch(GET_IP_URL)
content.append(text(":globe_showing_Americas:", bold("IP:"), code(ip)))
async with aiohttp.ClientSession() as session:
ip = await fetch(GET_IP_URL, session)
content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip)))
# This line is formatted to '🌎 *IP:* `YOUR IP`'
# Make request through proxy
ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth)
content.append(text(":locked_with_key:", bold("IP:"), code(ip), italic("via proxy")))
# Make request through bot's proxy
ip = await fetch(GET_IP_URL, bot.session)
content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy')))
# This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_'
# Send content
await bot.send_message(
message.chat.id, emojize(text(*content, sep="\n")), parse_mode=ParseMode.MARKDOWN
)
await bot.send_message(message.chat.id, emojize(text(*content, sep='\n')), parse_mode=ParseMode.MARKDOWN)
# In this example you can see emoji codes: ":globe_showing_Americas:" and ":locked_with_key:"
# You can find full emoji cheat sheet at https://www.webpagefx.com/tools/emoji-cheat-sheet/
@ -63,5 +65,5 @@ async def cmd_start(message: types.Message):
# For example emojize('Moon face :new_moon_face:') is transformed to 'Moon face 🌚'
if __name__ == "__main__":
start_polling(dp, loop=loop, skip_updates=True)
if __name__ == '__main__':
start_polling(dp, skip_updates=True)

View file

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

View file

@ -0,0 +1,67 @@
"""
This bot is created for the demonstration of a usage of regular keyboards.
"""
import logging
from aiogram import Bot, Dispatcher, executor, types
API_TOKEN = 'BOT_TOKEN_HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(commands='start')
async def start_cmd_handler(message: types.Message):
keyboard_markup = types.ReplyKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually
# kept for clearness
btns_text = ('Yes!', 'No!')
keyboard_markup.row(*(types.KeyboardButton(text) for text in btns_text))
# adds buttons as a new row to the existing keyboard
# the behaviour doesn't depend on row_width attribute
more_btns_text = (
"I don't know",
"Who am i?",
"Where am i?",
"Who is there?",
)
keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text))
# adds buttons. New rows are formed according to row_width parameter
await message.reply("Hi!\nDo you like aiogram?", reply_markup=keyboard_markup)
@dp.message_handler()
async def all_msg_handler(message: types.Message):
# pressing of a KeyboardButton is the same as sending the regular message with the same text
# so, to handle the responses from the keyboard, we need to use a message_handler
# in real bot, it's better to define message_handler(text="...") for each button
# but here for the simplicity only one handler is defined
button_text = message.text
logger.debug('The answer is %r', button_text) # print the text we've got
if button_text == 'Yes!':
reply_text = "That's great"
elif button_text == 'No!':
reply_text = "Oh no! Why?"
else:
reply_text = "Keep calm...Everything is fine"
await message.reply(reply_text, reply_markup=types.ReplyKeyboardRemove())
# with message, we send types.ReplyKeyboardRemove() to hide the keyboard
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

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

View file

@ -1,43 +0,0 @@
"""
Example for throttling manager.
You can use that for flood controlling.
"""
import asyncio
import logging
from aiogram import Bot, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import Dispatcher
from aiogram.utils.exceptions import Throttled
from aiogram.utils.executor import start_polling
API_TOKEN = "BOT TOKEN HERE"
logging.basicConfig(level=logging.INFO)
loop = asyncio.get_event_loop()
bot = Bot(token=API_TOKEN, loop=loop)
# Throttling manager does not work without Leaky Bucket.
# Then need to use storages. For example use simple in-memory storage.
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
@dp.message_handler(commands=["start", "help"])
async def send_welcome(message: types.Message):
try:
# Execute throttling manager with rate-limit equal to 2 seconds for key "start"
await dp.throttle("start", rate=2)
except Throttled:
# If request is throttled, the `Throttled` exception will be raised
await message.reply("Too many requests!")
else:
# Otherwise do something
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
if __name__ == "__main__":
start_polling(dp, loop=loop, skip_updates=True)

View file

@ -0,0 +1,71 @@
"""
Example for throttling manager.
You can use that for flood controlling.
"""
import logging
from aiogram import Bot, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import Dispatcher
from aiogram.utils.exceptions import Throttled
from aiogram.utils.executor import start_polling
API_TOKEN = 'BOT_TOKEN_HERE'
logging.basicConfig(level=logging.INFO)
bot = Bot(token=API_TOKEN)
# Throttling manager does not work without Leaky Bucket.
# You need to use a storage. For example use simple in-memory storage.
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
@dp.message_handler(commands=['start'])
async def send_welcome(message: types.Message):
try:
# Execute throttling manager with rate-limit equal to 2 seconds for key "start"
await dp.throttle('start', rate=2)
except Throttled:
# If request is throttled, the `Throttled` exception will be raised
await message.reply('Too many requests!')
else:
# Otherwise do something
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
@dp.message_handler(commands=['hi'])
@dp.throttled(lambda msg, loop, *args, **kwargs: loop.create_task(bot.send_message(msg.from_user.id, "Throttled")),
rate=5)
# loop is added to the function to run coroutines from it
async def say_hi(message: types.Message):
await message.answer("Hi")
# the on_throttled object can be either a regular function or coroutine
async def hello_throttled(*args, **kwargs):
# args will be the same as in the original handler
# kwargs will be updated with parameters given to .throttled (rate, key, user_id, chat_id)
print(f"hello_throttled was called with args={args} and kwargs={kwargs}")
message = args[0] # as message was the first argument in the original handler
await message.answer("Throttled")
@dp.message_handler(commands=['hello'])
@dp.throttled(hello_throttled, rate=4)
async def say_hello(message: types.Message):
await message.answer("Hello!")
@dp.message_handler(commands=['help'])
@dp.throttled(rate=5)
# nothing will happen if the handler will be throttled
async def help_handler(message: types.Message):
await message.answer('Help!')
if __name__ == '__main__':
start_polling(dp, skip_updates=True)

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
aiohttp>=3.5.4
aiohttp>=3.5.4,<4.0.0
Babel>=2.6.0
certifi>=2019.3.9

View file

@ -15,9 +15,7 @@ WORK_DIR = pathlib.Path(__file__).parent
# Check python version
MINIMAL_PY_VERSION = (3, 7)
if sys.version_info < MINIMAL_PY_VERSION:
raise RuntimeError(
"aiogram works only with Python {}+".format(".".join(map(str, MINIMAL_PY_VERSION)))
)
raise RuntimeError('aiogram works only with Python {}+'.format('.'.join(map(str, MINIMAL_PY_VERSION))))
def get_version():
@ -26,11 +24,11 @@ def get_version():
:return: str
"""
txt = (WORK_DIR / "aiogram" / "__init__.py").read_text("utf-8")
txt = (WORK_DIR / 'aiogram' / '__init__.py').read_text('utf-8')
try:
return re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0]
except IndexError:
raise RuntimeError("Unable to determine version.")
raise RuntimeError('Unable to determine version.')
def get_description():
@ -40,7 +38,7 @@ def get_description():
:return: description
:rtype: str
"""
with open("README.rst", "r", encoding="utf-8") as f:
with open('README.rst', 'r', encoding='utf-8') as f:
return f.read()
@ -52,36 +50,36 @@ def get_requirements(filename=None):
:rtype: list
"""
if filename is None:
filename = "requirements.txt"
filename = 'requirements.txt'
file = WORK_DIR / filename
install_reqs = parse_requirements(str(file), session="hack")
install_reqs = parse_requirements(str(file), session='hack')
return [str(ir.req) for ir in install_reqs]
setup(
name="aiogram",
name='aiogram',
version=get_version(),
packages=find_packages(exclude=("tests", "tests.*", "examples.*", "docs", "generator")),
url="https://github.com/aiogram/aiogram",
license="MIT",
author="Alex Root Junior",
requires_python=">=3.7",
author_email="jroot.junior@gmail.com",
description="Is a pretty simple and fully asynchronous library for Telegram Bot API",
packages=find_packages(exclude=('tests', 'tests.*', 'examples.*', 'docs',)),
url='https://github.com/aiogram/aiogram',
license='MIT',
author='Alex Root Junior',
requires_python='>=3.7',
author_email='jroot.junior@gmail.com',
description='Is a pretty simple and fully asynchronous framework for Telegram Bot API',
long_description=get_description(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Libraries :: Application Frameworks",
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Framework :: AsyncIO',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.7',
'Topic :: Software Development :: Libraries :: Application Frameworks',
],
install_requires=get_requirements(),
package_data={"": ["requirements.txt"]},
package_data={'': ['requirements.txt']},
include_package_data=False,
)

View file

@ -3,7 +3,7 @@ import pytest
from aiogram import Bot, types
TOKEN = "123456789:AABBCCDDEEFFaabbccddeeff-1234567890"
TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
class FakeTelegram(aresponses.ResponsesMockServer):
@ -13,25 +13,23 @@ class FakeTelegram(aresponses.ResponsesMockServer):
async def __aenter__(self):
await super().__aenter__()
_response = self.Response(text=self._body, headers=self._headers, status=200, reason="OK")
_response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK')
self.add(self.ANY, response=_response)
@staticmethod
def parse_data(message_dict):
import json
_body = '{"ok":true,"result":' + json.dumps(message_dict) + "}"
_headers = {
"Server": "nginx/1.12.2",
"Date": "Tue, 03 Apr 2018 16:59:54 GMT",
"Content-Type": "application/json",
"Content-Length": str(len(_body)),
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Expose-Headers": "Content-Length,Content-Type,Date,Server,Connection",
"Strict-Transport-Security": "max-age=31536000; includeSubdomains",
}
_body = '{"ok":true,"result":' + json.dumps(message_dict) + '}'
_headers = {'Server': 'nginx/1.12.2',
'Date': 'Tue, 03 Apr 2018 16:59:54 GMT',
'Content-Type': 'application/json',
'Content-Length': str(len(_body)),
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection',
'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'}
return _body, _headers
@ -48,7 +46,6 @@ async def bot(event_loop):
async def test_get_me(bot: Bot, event_loop):
""" getMe method test """
from .types.dataset import USER
user = types.User(**USER)
async with FakeTelegram(message_dict=USER, loop=event_loop):
@ -60,7 +57,6 @@ async def test_get_me(bot: Bot, event_loop):
async def test_send_message(bot: Bot, event_loop):
""" sendMessage method test """
from .types.dataset import MESSAGE
msg = types.Message(**MESSAGE)
async with FakeTelegram(message_dict=MESSAGE, loop=event_loop):
@ -72,15 +68,11 @@ async def test_send_message(bot: Bot, event_loop):
async def test_forward_message(bot: Bot, event_loop):
""" forwardMessage method test """
from .types.dataset import FORWARDED_MESSAGE
msg = types.Message(**FORWARDED_MESSAGE)
async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop):
result = await bot.forward_message(
chat_id=msg.chat.id,
from_chat_id=msg.forward_from_chat.id,
message_id=msg.forward_from_message_id,
)
result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=msg.forward_from_chat.id,
message_id=msg.forward_from_message_id)
assert result == msg
@ -88,18 +80,12 @@ async def test_forward_message(bot: Bot, event_loop):
async def test_send_photo(bot: Bot, event_loop):
""" sendPhoto method test with file_id """
from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO
msg = types.Message(**MESSAGE_WITH_PHOTO)
photo = types.PhotoSize(**PHOTO)
async with FakeTelegram(message_dict=MESSAGE_WITH_PHOTO, loop=event_loop):
result = await bot.send_photo(
msg.chat.id,
photo=photo.file_id,
caption=msg.caption,
parse_mode=types.ParseMode.HTML,
disable_notification=False,
)
result = await bot.send_photo(msg.chat.id, photo=photo.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, disable_notification=False)
assert result == msg
@ -107,20 +93,12 @@ async def test_send_photo(bot: Bot, event_loop):
async def test_send_audio(bot: Bot, event_loop):
""" sendAudio method test with file_id """
from .types.dataset import MESSAGE_WITH_AUDIO
msg = types.Message(**MESSAGE_WITH_AUDIO)
async with FakeTelegram(message_dict=MESSAGE_WITH_AUDIO, loop=event_loop):
result = await bot.send_audio(
chat_id=msg.chat.id,
audio=msg.audio.file_id,
caption=msg.caption,
parse_mode=types.ParseMode.HTML,
duration=msg.audio.duration,
performer=msg.audio.performer,
title=msg.audio.title,
disable_notification=False,
)
result = await bot.send_audio(chat_id=msg.chat.id, audio=msg.audio.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, duration=msg.audio.duration,
performer=msg.audio.performer, title=msg.audio.title, disable_notification=False)
assert result == msg
@ -128,17 +106,11 @@ async def test_send_audio(bot: Bot, event_loop):
async def test_send_document(bot: Bot, event_loop):
""" sendDocument method test with file_id """
from .types.dataset import MESSAGE_WITH_DOCUMENT
msg = types.Message(**MESSAGE_WITH_DOCUMENT)
async with FakeTelegram(message_dict=MESSAGE_WITH_DOCUMENT, loop=event_loop):
result = await bot.send_document(
chat_id=msg.chat.id,
document=msg.document.file_id,
caption=msg.caption,
parse_mode=types.ParseMode.HTML,
disable_notification=False,
)
result = await bot.send_document(chat_id=msg.chat.id, document=msg.document.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, disable_notification=False)
assert result == msg
@ -146,22 +118,14 @@ async def test_send_document(bot: Bot, event_loop):
async def test_send_video(bot: Bot, event_loop):
""" sendVideo method test with file_id """
from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO
msg = types.Message(**MESSAGE_WITH_VIDEO)
video = types.Video(**VIDEO)
async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO, loop=event_loop):
result = await bot.send_video(
chat_id=msg.chat.id,
video=video.file_id,
duration=video.duration,
width=video.width,
height=video.height,
caption=msg.caption,
parse_mode=types.ParseMode.HTML,
supports_streaming=True,
disable_notification=False,
)
result = await bot.send_video(chat_id=msg.chat.id, video=video.file_id, duration=video.duration,
width=video.width, height=video.height, caption=msg.caption,
parse_mode=types.ParseMode.HTML, supports_streaming=True,
disable_notification=False)
assert result == msg
@ -169,19 +133,13 @@ async def test_send_video(bot: Bot, event_loop):
async def test_send_voice(bot: Bot, event_loop):
""" sendVoice method test with file_id """
from .types.dataset import MESSAGE_WITH_VOICE, VOICE
msg = types.Message(**MESSAGE_WITH_VOICE)
voice = types.Voice(**VOICE)
async with FakeTelegram(message_dict=MESSAGE_WITH_VOICE, loop=event_loop):
result = await bot.send_voice(
chat_id=msg.chat.id,
voice=voice.file_id,
caption=msg.caption,
parse_mode=types.ParseMode.HTML,
duration=voice.duration,
disable_notification=False,
)
result = await bot.send_voice(chat_id=msg.chat.id, voice=voice.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, duration=voice.duration,
disable_notification=False)
assert result == msg
@ -189,18 +147,13 @@ async def test_send_voice(bot: Bot, event_loop):
async def test_send_video_note(bot: Bot, event_loop):
""" sendVideoNote method test with file_id """
from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE
msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE)
video_note = types.VideoNote(**VIDEO_NOTE)
async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop):
result = await bot.send_video_note(
chat_id=msg.chat.id,
video_note=video_note.file_id,
duration=video_note.duration,
length=video_note.length,
disable_notification=False,
)
result = await bot.send_video_note(chat_id=msg.chat.id, video_note=video_note.file_id,
duration=video_note.duration, length=video_note.length,
disable_notification=False)
assert result == msg
@ -208,17 +161,11 @@ async def test_send_video_note(bot: Bot, event_loop):
async def test_send_media_group(bot: Bot, event_loop):
""" sendMediaGroup method test with file_id """
from .types.dataset import MESSAGE_WITH_MEDIA_GROUP, PHOTO
msg = types.Message(**MESSAGE_WITH_MEDIA_GROUP)
photo = types.PhotoSize(**PHOTO)
media = [
types.InputMediaPhoto(media=photo.file_id),
types.InputMediaPhoto(media=photo.file_id),
]
media = [types.InputMediaPhoto(media=photo.file_id), types.InputMediaPhoto(media=photo.file_id)]
async with FakeTelegram(
message_dict=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop
):
async with FakeTelegram(message_dict=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop):
result = await bot.send_media_group(msg.chat.id, media=media, disable_notification=False)
assert len(result) == len(media)
assert result.pop().media_group_id
@ -228,18 +175,12 @@ async def test_send_media_group(bot: Bot, event_loop):
async def test_send_location(bot: Bot, event_loop):
""" sendLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
location = types.Location(**LOCATION)
async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.send_location(
msg.chat.id,
latitude=location.latitude,
longitude=location.longitude,
live_period=10,
disable_notification=False,
)
result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude,
live_period=10, disable_notification=False)
assert result == msg
@ -247,18 +188,13 @@ async def test_send_location(bot: Bot, event_loop):
async def test_edit_message_live_location_by_bot(bot: Bot, event_loop):
""" editMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
location = types.Location(**LOCATION)
# editing bot message
async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.edit_message_live_location(
chat_id=msg.chat.id,
message_id=msg.message_id,
latitude=location.latitude,
longitude=location.longitude,
)
result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id,
latitude=location.latitude, longitude=location.longitude)
assert result == msg
@ -266,18 +202,13 @@ async def test_edit_message_live_location_by_bot(bot: Bot, event_loop):
async def test_edit_message_live_location_by_user(bot: Bot, event_loop):
""" editMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
location = types.Location(**LOCATION)
# editing user's message
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.edit_message_live_location(
chat_id=msg.chat.id,
message_id=msg.message_id,
latitude=location.latitude,
longitude=location.longitude,
)
result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id,
latitude=location.latitude, longitude=location.longitude)
assert isinstance(result, bool) and result is True
@ -285,14 +216,11 @@ async def test_edit_message_live_location_by_user(bot: Bot, event_loop):
async def test_stop_message_live_location_by_bot(bot: Bot, event_loop):
""" stopMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
# stopping bot message
async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.stop_message_live_location(
chat_id=msg.chat.id, message_id=msg.message_id
)
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
assert result == msg
@ -300,14 +228,11 @@ async def test_stop_message_live_location_by_bot(bot: Bot, event_loop):
async def test_stop_message_live_location_by_user(bot: Bot, event_loop):
""" stopMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
# stopping user's message
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.stop_message_live_location(
chat_id=msg.chat.id, message_id=msg.message_id
)
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
assert isinstance(result, bool)
assert result is True
@ -316,21 +241,14 @@ async def test_stop_message_live_location_by_user(bot: Bot, event_loop):
async def test_send_venue(bot: Bot, event_loop):
""" sendVenue method test """
from .types.dataset import MESSAGE_WITH_VENUE, VENUE, LOCATION
msg = types.Message(**MESSAGE_WITH_VENUE)
location = types.Location(**LOCATION)
venue = types.Venue(**VENUE)
async with FakeTelegram(message_dict=MESSAGE_WITH_VENUE, loop=event_loop):
result = await bot.send_venue(
msg.chat.id,
latitude=location.latitude,
longitude=location.longitude,
title=venue.title,
address=venue.address,
foursquare_id=venue.foursquare_id,
disable_notification=False,
)
result = await bot.send_venue(msg.chat.id, latitude=location.latitude, longitude=location.longitude,
title=venue.title, address=venue.address, foursquare_id=venue.foursquare_id,
disable_notification=False)
assert result == msg
@ -338,18 +256,12 @@ async def test_send_venue(bot: Bot, event_loop):
async def test_send_contact(bot: Bot, event_loop):
""" sendContact method test """
from .types.dataset import MESSAGE_WITH_CONTACT, CONTACT
msg = types.Message(**MESSAGE_WITH_CONTACT)
contact = types.Contact(**CONTACT)
async with FakeTelegram(message_dict=MESSAGE_WITH_CONTACT, loop=event_loop):
result = await bot.send_contact(
msg.chat.id,
phone_number=contact.phone_number,
first_name=contact.first_name,
last_name=contact.last_name,
disable_notification=False,
)
result = await bot.send_contact(msg.chat.id, phone_number=contact.phone_number, first_name=contact.first_name,
last_name=contact.last_name, disable_notification=False)
assert result == msg
@ -357,7 +269,6 @@ async def test_send_contact(bot: Bot, event_loop):
async def test_send_chat_action(bot: Bot, event_loop):
""" sendChatAction method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
@ -370,7 +281,6 @@ async def test_send_chat_action(bot: Bot, event_loop):
async def test_get_user_profile_photo(bot: Bot, event_loop):
""" getUserProfilePhotos method test """
from .types.dataset import USER_PROFILE_PHOTOS, USER
user = types.User(**USER)
async with FakeTelegram(message_dict=USER_PROFILE_PHOTOS, loop=event_loop):
@ -382,7 +292,6 @@ async def test_get_user_profile_photo(bot: Bot, event_loop):
async def test_get_file(bot: Bot, event_loop):
""" getFile method test """
from .types.dataset import FILE
file = types.File(**FILE)
async with FakeTelegram(message_dict=FILE, loop=event_loop):
@ -394,7 +303,6 @@ async def test_get_file(bot: Bot, event_loop):
async def test_kick_chat_member(bot: Bot, event_loop):
""" kickChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
@ -408,7 +316,6 @@ async def test_kick_chat_member(bot: Bot, event_loop):
async def test_unban_chat_member(bot: Bot, event_loop):
""" unbanChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
@ -422,7 +329,6 @@ async def test_unban_chat_member(bot: Bot, event_loop):
async def test_restrict_chat_member(bot: Bot, event_loop):
""" restrictChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
@ -430,12 +336,12 @@ async def test_restrict_chat_member(bot: Bot, event_loop):
result = await bot.restrict_chat_member(
chat_id=chat.id,
user_id=user.id,
can_add_web_page_previews=False,
can_send_media_messages=False,
can_send_messages=False,
can_send_other_messages=False,
until_date=123,
)
permissions=types.ChatPermissions(
can_add_web_page_previews=False,
can_send_media_messages=False,
can_send_messages=False,
can_send_other_messages=False
), until_date=123)
assert isinstance(result, bool)
assert result is True
@ -444,23 +350,14 @@ async def test_restrict_chat_member(bot: Bot, event_loop):
async def test_promote_chat_member(bot: Bot, event_loop):
""" promoteChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.promote_chat_member(
chat_id=chat.id,
user_id=user.id,
can_change_info=True,
can_delete_messages=True,
can_edit_messages=True,
can_invite_users=True,
can_pin_messages=True,
can_post_messages=True,
can_promote_members=True,
can_restrict_members=True,
)
result = await bot.promote_chat_member(chat_id=chat.id, user_id=user.id, can_change_info=True,
can_delete_messages=True, can_edit_messages=True,
can_invite_users=True, can_pin_messages=True, can_post_messages=True,
can_promote_members=True, can_restrict_members=True)
assert isinstance(result, bool)
assert result is True
@ -469,7 +366,6 @@ async def test_promote_chat_member(bot: Bot, event_loop):
async def test_export_chat_invite_link(bot: Bot, event_loop):
""" exportChatInviteLink method test """
from .types.dataset import CHAT, INVITE_LINK
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=INVITE_LINK, loop=event_loop):
@ -481,7 +377,6 @@ async def test_export_chat_invite_link(bot: Bot, event_loop):
async def test_delete_chat_photo(bot: Bot, event_loop):
""" deleteChatPhoto method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
@ -494,11 +389,10 @@ async def test_delete_chat_photo(bot: Bot, event_loop):
async def test_set_chat_title(bot: Bot, event_loop):
""" setChatTitle method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.set_chat_title(chat_id=chat.id, title="Test title")
result = await bot.set_chat_title(chat_id=chat.id, title='Test title')
assert isinstance(result, bool)
assert result is True
@ -507,11 +401,10 @@ async def test_set_chat_title(bot: Bot, event_loop):
async def test_set_chat_description(bot: Bot, event_loop):
""" setChatDescription method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.set_chat_description(chat_id=chat.id, description="Test description")
result = await bot.set_chat_description(chat_id=chat.id, description='Test description')
assert isinstance(result, bool)
assert result is True
@ -520,13 +413,11 @@ async def test_set_chat_description(bot: Bot, event_loop):
async def test_pin_chat_message(bot: Bot, event_loop):
""" pinChatMessage method test """
from .types.dataset import MESSAGE
message = types.Message(**MESSAGE)
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.pin_chat_message(
chat_id=message.chat.id, message_id=message.message_id, disable_notification=False
)
result = await bot.pin_chat_message(chat_id=message.chat.id, message_id=message.message_id,
disable_notification=False)
assert isinstance(result, bool)
assert result is True
@ -535,7 +426,6 @@ async def test_pin_chat_message(bot: Bot, event_loop):
async def test_unpin_chat_message(bot: Bot, event_loop):
""" unpinChatMessage method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
@ -548,7 +438,6 @@ async def test_unpin_chat_message(bot: Bot, event_loop):
async def test_leave_chat(bot: Bot, event_loop):
""" leaveChat method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
@ -561,7 +450,6 @@ async def test_leave_chat(bot: Bot, event_loop):
async def test_get_chat(bot: Bot, event_loop):
""" getChat method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=CHAT, loop=event_loop):
@ -573,7 +461,6 @@ async def test_get_chat(bot: Bot, event_loop):
async def test_get_chat_administrators(bot: Bot, event_loop):
""" getChatAdministrators method test """
from .types.dataset import CHAT, CHAT_MEMBER
chat = types.Chat(**CHAT)
member = types.ChatMember(**CHAT_MEMBER)
@ -587,7 +474,6 @@ async def test_get_chat_administrators(bot: Bot, event_loop):
async def test_get_chat_members_count(bot: Bot, event_loop):
""" getChatMembersCount method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
count = 5
@ -600,7 +486,6 @@ async def test_get_chat_members_count(bot: Bot, event_loop):
async def test_get_chat_member(bot: Bot, event_loop):
""" getChatMember method test """
from .types.dataset import CHAT, CHAT_MEMBER
chat = types.Chat(**CHAT)
member = types.ChatMember(**CHAT_MEMBER)
@ -614,13 +499,10 @@ async def test_get_chat_member(bot: Bot, event_loop):
async def test_set_chat_sticker_set(bot: Bot, event_loop):
""" setChatStickerSet method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.set_chat_sticker_set(
chat_id=chat.id, sticker_set_name="aiogram_stickers"
)
result = await bot.set_chat_sticker_set(chat_id=chat.id, sticker_set_name='aiogram_stickers')
assert isinstance(result, bool)
assert result is True
@ -629,7 +511,6 @@ async def test_set_chat_sticker_set(bot: Bot, event_loop):
async def test_delete_chat_sticker_set(bot: Bot, event_loop):
""" setChatStickerSet method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
async with FakeTelegram(message_dict=True, loop=event_loop):
@ -643,7 +524,7 @@ async def test_answer_callback_query(bot: Bot, event_loop):
""" answerCallbackQuery method test """
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.answer_callback_query(callback_query_id="QuERyId", text="Test Answer")
result = await bot.answer_callback_query(callback_query_id='QuERyId', text='Test Answer')
assert isinstance(result, bool)
assert result is True
@ -652,14 +533,11 @@ async def test_answer_callback_query(bot: Bot, event_loop):
async def test_edit_message_text_by_bot(bot: Bot, event_loop):
""" editMessageText method test """
from .types.dataset import EDITED_MESSAGE
msg = types.Message(**EDITED_MESSAGE)
# message by bot
async with FakeTelegram(message_dict=EDITED_MESSAGE, loop=event_loop):
result = await bot.edit_message_text(
text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id
)
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
assert result == msg
@ -667,13 +545,10 @@ async def test_edit_message_text_by_bot(bot: Bot, event_loop):
async def test_edit_message_text_by_user(bot: Bot, event_loop):
""" editMessageText method test """
from .types.dataset import EDITED_MESSAGE
msg = types.Message(**EDITED_MESSAGE)
# message by user
async with FakeTelegram(message_dict=True, loop=event_loop):
result = await bot.edit_message_text(
text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id
)
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
assert isinstance(result, bool)
assert result is True

View file

@ -0,0 +1,18 @@
import pytest
from aiogram.bot.api import check_token
from aiogram.utils.exceptions import ValidationError
VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length
class Test_check_token:
def test_valid(self):
assert check_token(VALID_TOKEN) is True
def test_invalid_token(self):
with pytest.raises(ValidationError):
check_token(INVALID_TOKEN)

View file

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

View file

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

263
tests/test_filters.py Normal file
View file

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

View file

@ -1,41 +0,0 @@
import pytest
from aiogram.bot import api
from aiogram.utils import auth_widget, exceptions
VALID_TOKEN = "123456789:AABBCCDDEEFFaabbccddeeff-1234567890"
INVALID_TOKEN = "123456789:AABBCCDDEEFFaabbccddeeff 123456789" # Space in token and wrong length
VALID_DATA = {
"date": 1525385236,
"first_name": "Test",
"last_name": "User",
"id": 123456789,
"username": "username",
"hash": "69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b",
}
INVALID_DATA = {
"date": 1525385237,
"first_name": "Test",
"last_name": "User",
"id": 123456789,
"username": "username",
"hash": "69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b",
}
def test_valid_token():
assert api.check_token(VALID_TOKEN)
def test_invalid_token():
with pytest.raises(exceptions.ValidationError):
api.check_token(INVALID_TOKEN)
def test_widget():
assert auth_widget.check_token(VALID_DATA, VALID_TOKEN)
def test_invalid_widget_data():
assert not auth_widget.check_token(INVALID_DATA, VALID_TOKEN)

View file

@ -0,0 +1,46 @@
import pytest
from aiogram.utils.auth_widget import check_integrity, \
generate_hash, check_token
TOKEN = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
@pytest.fixture
def data():
return {
'id': '42',
'first_name': 'John',
'last_name': 'Smith',
'username': 'username',
'photo_url': 'https://t.me/i/userpic/320/picname.jpg',
'auth_date': '1565810688',
'hash': 'c303db2b5a06fe41d23a9b14f7c545cfc11dcc7473c07c9c5034ae60062461ce',
}
def test_generate_hash(data):
res = generate_hash(data, TOKEN)
assert res == data['hash']
class Test_check_token:
"""
This case gonna be deleted
"""
def test_ok(self, data):
assert check_token(data, TOKEN) is True
def test_fail(self, data):
data.pop('username')
assert check_token(data, TOKEN) is False
class Test_check_integrity:
def test_ok(self, data):
assert check_integrity(TOKEN, data) is True
def test_fail(self, data):
data.pop('username')
assert check_integrity(TOKEN, data) is False

View file

@ -0,0 +1,22 @@
from aiogram.utils.helper import OrderedHelper, Item, ListItem
class TestOrderedHelper:
def test_items_are_ordered(self):
class Helper(OrderedHelper):
A = Item()
D = Item()
C = Item()
B = Item()
assert Helper.all() == ['A', 'D', 'C', 'B']
def test_list_items_are_ordered(self):
class Helper(OrderedHelper):
A = ListItem()
D = ListItem()
C = ListItem()
B = ListItem()
assert Helper.all() == ['A', 'D', 'C', 'B']

View file

@ -47,7 +47,11 @@ CHAT_MEMBER = {
"can_promote_members": False,
}
CONTACT = {"phone_number": "88005553535", "first_name": "John", "last_name": "Smith"}
CONTACT = {
"phone_number": "88005553535",
"first_name": "John",
"last_name": "Smith",
}
DOCUMENT = {
"file_name": "test.docx",
@ -64,17 +68,42 @@ ANIMATION = {
"file_size": 65837,
}
ENTITY_BOLD = {"offset": 5, "length": 2, "type": "bold"}
ENTITY_BOLD = {
"offset": 5,
"length": 2,
"type": "bold",
}
ENTITY_ITALIC = {"offset": 8, "length": 1, "type": "italic"}
ENTITY_ITALIC = {
"offset": 8,
"length": 1,
"type": "italic",
}
ENTITY_LINK = {"offset": 10, "length": 6, "type": "text_link", "url": "http://google.com/"}
ENTITY_LINK = {
"offset": 10,
"length": 6,
"type": "text_link",
"url": "http://google.com/",
}
ENTITY_CODE = {"offset": 17, "length": 7, "type": "code"}
ENTITY_CODE = {
"offset": 17,
"length": 7,
"type": "code",
}
ENTITY_PRE = {"offset": 30, "length": 4, "type": "pre"}
ENTITY_PRE = {
"offset": 30,
"length": 4,
"type": "pre",
}
ENTITY_MENTION = {"offset": 47, "length": 9, "type": "mention"}
ENTITY_MENTION = {
"offset": 47,
"length": 9,
"type": "mention",
}
GAME = {
"title": "Karate Kido",
@ -86,15 +115,18 @@ GAME = {
INVOICE = {
"title": "Working Time Machine",
"description": "Want to visit your great-great-great-grandparents? "
"Make a fortune at the races? "
"Shake hands with Hammurabi and take a stroll in the Hanging Gardens? "
"Order our Working Time Machine today!",
"Make a fortune at the races? "
"Shake hands with Hammurabi and take a stroll in the Hanging Gardens? "
"Order our Working Time Machine today!",
"start_parameter": "time-machine-example",
"currency": "USD",
"total_amount": 6250,
}
LOCATION = {"latitude": 50.693416, "longitude": 30.624605}
LOCATION = {
"latitude": 50.693416,
"longitude": 30.624605,
}
VENUE = {
"location": LOCATION,
@ -121,7 +153,7 @@ STICKER = {
"file_id": "AAbbCCddEEffGGhh1234567890",
"file_size": 1234,
"width": 128,
"height": 128,
"height": 128
},
"file_id": "AAbbCCddEEffGGhh1234567890",
"file_size": 12345,
@ -186,15 +218,8 @@ FORWARDED_MESSAGE = {
"forward_from_message_id": 123,
"forward_date": 1522749037,
"text": "Forwarded text with entities from public channel ",
"entities": [
ENTITY_BOLD,
ENTITY_CODE,
ENTITY_ITALIC,
ENTITY_LINK,
ENTITY_LINK,
ENTITY_MENTION,
ENTITY_PRE,
],
"entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK,
ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE],
}
INLINE_QUERY = {}
@ -391,12 +416,27 @@ SHIPPING_QUERY = {
"shipping_address": SHIPPING_ADDRESS,
}
USER_PROFILE_PHOTOS = {"total_count": 1, "photos": [[PHOTO, PHOTO, PHOTO]]}
USER_PROFILE_PHOTOS = {
"total_count": 1, "photos": [
[PHOTO, PHOTO, PHOTO],
],
}
FILE = {"file_id": "XXXYYYZZZ", "file_size": 5254, "file_path": "voice/file_8"}
FILE = {
"file_id": "XXXYYYZZZ",
"file_size": 5254,
"file_path": "voice/file_8",
}
INVITE_LINK = "https://t.me/joinchat/AbCdEfjKILDADwdd123"
INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123'
UPDATE = {"update_id": 123456789, "message": MESSAGE}
UPDATE = {
"update_id": 123456789,
"message": MESSAGE,
}
WEBHOOK_INFO = {"url": "", "has_custom_certificate": False, "pending_update_count": 0}
WEBHOOK_INFO = {
"url": "",
"has_custom_certificate": False,
"pending_update_count": 0,
}

View file

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