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

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