mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-08 17:13:56 +00:00
Merge branch 'dev-2.x' into is_chat_member
This commit is contained in:
commit
54d5406967
45 changed files with 947 additions and 266 deletions
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -8,7 +8,7 @@ Fixes # (issue)
|
|||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Documentstion (typos, code examples or any documentation update)
|
||||
- [ ] Documentation (typos, code examples or any documentation update)
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -1,11 +1,12 @@
|
|||
# AIOGram
|
||||
|
||||
[](https://opencollective.com/aiogram)
|
||||
[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
@ -24,3 +25,33 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
|
|||
- Source: [Github repo](https://github.com/aiogram/aiogram)
|
||||
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
|
||||
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)].
|
||||
<a href="https://github.com/aiogram/aiogram/graphs/contributors"><img src="https://opencollective.com/aiogram/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/aiogram"><img src="https://opencollective.com/aiogram/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/aiogram/organization/0/website"><img src="https://opencollective.com/aiogram/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/1/website"><img src="https://opencollective.com/aiogram/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/2/website"><img src="https://opencollective.com/aiogram/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/3/website"><img src="https://opencollective.com/aiogram/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/4/website"><img src="https://opencollective.com/aiogram/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/5/website"><img src="https://opencollective.com/aiogram/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/6/website"><img src="https://opencollective.com/aiogram/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/7/website"><img src="https://opencollective.com/aiogram/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/8/website"><img src="https://opencollective.com/aiogram/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/aiogram/organization/9/website"><img src="https://opencollective.com/aiogram/organization/9/avatar.svg"></a>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ AIOGramBot
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -38,5 +38,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.2.1.dev1'
|
||||
__api_version__ = '4.3'
|
||||
__version__ = '2.3.dev1'
|
||||
__api_version__ = '4.4'
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 4.3
|
||||
List is updated to Bot API 4.4
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -182,6 +182,7 @@ class Methods(Helper):
|
|||
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
|
||||
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
|
||||
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
|
||||
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
|
||||
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
|
||||
SET_CHAT_PHOTO = Item() # setChatPhoto
|
||||
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from .base import BaseBot, api
|
||||
from .. import types
|
||||
|
|
@ -337,12 +338,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
reply_markup = prepare_arg(reply_markup)
|
||||
payload = generate_payload(**locals(), exclude=['audio'])
|
||||
payload = generate_payload(**locals(), exclude=['audio', 'thumb'])
|
||||
if self.parse_mode:
|
||||
payload.setdefault('parse_mode', self.parse_mode)
|
||||
|
||||
files = {}
|
||||
prepare_file(payload, files, 'audio', audio)
|
||||
prepare_attachment(payload, files, 'thumb', thumb)
|
||||
|
||||
result = await self.request(api.Methods.SEND_AUDIO, payload, files)
|
||||
return types.Message(**result)
|
||||
|
|
@ -1014,6 +1016,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
|
||||
async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
permissions: typing.Optional[types.ChatPermissions] = None,
|
||||
# permissions argument need to be required after removing other `can_*` arguments
|
||||
until_date: typing.Union[base.Integer, None] = None,
|
||||
can_send_messages: typing.Union[base.Boolean, None] = None,
|
||||
can_send_media_messages: typing.Union[base.Boolean, None] = None,
|
||||
|
|
@ -1030,6 +1034,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
:param permissions: New user permissions
|
||||
:type permissions: :obj:`ChatPermissions`
|
||||
:param until_date: Date when restrictions will be lifted for the user, unix time
|
||||
:type until_date: :obj:`typing.Union[base.Integer, None]`
|
||||
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
|
||||
|
|
@ -1047,8 +1053,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
until_date = prepare_arg(until_date)
|
||||
permissions = prepare_arg(permissions)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
for permission in ['can_send_messages',
|
||||
'can_send_media_messages',
|
||||
'can_send_other_messages',
|
||||
'can_add_web_page_previews']:
|
||||
if permission in payload:
|
||||
warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions "
|
||||
f"in a single argument of the type ChatPermissions instead of "
|
||||
f"passing regular argument {payload[permission]}",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload)
|
||||
return result
|
||||
|
||||
|
|
@ -1099,6 +1116,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
|
||||
return result
|
||||
|
||||
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
permissions: types.ChatPermissions) -> base.Boolean:
|
||||
"""
|
||||
Use this method to set default chat permissions for all members.
|
||||
The bot must be an administrator in the group or a supergroup for this to work and must have the
|
||||
can_restrict_members admin rights.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target supergroup
|
||||
:param permissions: New default chat permissions
|
||||
:return: True on success.
|
||||
"""
|
||||
permissions = prepare_arg(permissions)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SET_CHAT_PERMISSIONS)
|
||||
return result
|
||||
|
||||
async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String:
|
||||
"""
|
||||
Use this method to generate a new invite link for a chat; any previously generated link is revoked.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
200
aiogram/contrib/fsm_storage/mongo.py
Normal file
200
aiogram/contrib/fsm_storage/mongo.py
Normal 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
|
||||
|
|
@ -35,7 +35,6 @@ class RedisStorage(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
|
@ -62,8 +61,6 @@ class RedisStorage(BaseStorage):
|
|||
async def redis(self) -> aioredis.RedisConnection:
|
||||
"""
|
||||
Get Redis connection
|
||||
|
||||
This property is awaitable.
|
||||
"""
|
||||
# Use thread-safe asyncio Lock because this method without that is not safe
|
||||
async with self._connection_lock:
|
||||
|
|
@ -222,9 +219,12 @@ class RedisStorage2(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None,
|
||||
pool_size=10, loop=None, prefix='fsm', **kwargs):
|
||||
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
|
||||
ssl=None, pool_size=10, loop=None, prefix='fsm',
|
||||
state_ttl: int = 0,
|
||||
data_ttl: int = 0,
|
||||
bucket_ttl: int = 0,
|
||||
**kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db = db
|
||||
|
|
@ -235,14 +235,16 @@ class RedisStorage2(BaseStorage):
|
|||
self._kwargs = kwargs
|
||||
self._prefix = (prefix,)
|
||||
|
||||
self._state_ttl = state_ttl
|
||||
self._data_ttl = data_ttl
|
||||
self._bucket_ttl = bucket_ttl
|
||||
|
||||
self._redis: aioredis.RedisConnection = None
|
||||
self._connection_lock = asyncio.Lock(loop=self._loop)
|
||||
|
||||
async def redis(self) -> aioredis.Redis:
|
||||
"""
|
||||
Get Redis connection
|
||||
|
||||
This property is awaitable.
|
||||
"""
|
||||
# Use thread-safe asyncio Lock because this method without that is not safe
|
||||
async with self._connection_lock:
|
||||
|
|
@ -294,14 +296,14 @@ class RedisStorage2(BaseStorage):
|
|||
if state is None:
|
||||
await redis.delete(key)
|
||||
else:
|
||||
await redis.set(key, state)
|
||||
await redis.set(key, state, expire=self._state_ttl)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_DATA_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(data))
|
||||
await redis.set(key, json.dumps(data), expire=self._data_ttl)
|
||||
|
||||
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None, **kwargs):
|
||||
|
|
@ -329,7 +331,7 @@ class RedisStorage2(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(bucket))
|
||||
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
|
||||
|
||||
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
|
|
@ -338,7 +340,7 @@ class RedisStorage2(BaseStorage):
|
|||
bucket = {}
|
||||
temp_bucket = await self.get_bucket(chat=chat, user=user)
|
||||
temp_bucket.update(bucket, **kwargs)
|
||||
await self.set_bucket(chat=chat, user=user, data=temp_bucket)
|
||||
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
|
||||
|
||||
async def reset_all(self, full=True):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class I18nMiddleware(BaseMiddleware):
|
|||
else:
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
|
||||
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy:
|
||||
"""
|
||||
Lazy get text
|
||||
|
||||
|
|
@ -115,9 +115,10 @@ class I18nMiddleware(BaseMiddleware):
|
|||
:param plural:
|
||||
:param n:
|
||||
:param locale:
|
||||
:param enable_cache:
|
||||
:return:
|
||||
"""
|
||||
return LazyProxy(self.gettext, singular, plural, n, locale)
|
||||
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
|
||||
|
|
|
|||
|
|
@ -89,34 +89,39 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
|
||||
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
|
||||
if callback_query.message:
|
||||
text = (f"Received callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
|
||||
f"from user [ID:{callback_query.message.from_user.id}]")
|
||||
else:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
else:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"from inline message [ID:{callback_query.inline_message_id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}]")
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for inline message [ID:{callback_query.inline_message_id}] ")
|
||||
|
||||
async def on_post_process_callback_query(self, callback_query, results, data: dict):
|
||||
if callback_query.message:
|
||||
text = (f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
|
||||
f"from user [ID:{callback_query.message.from_user.id}]")
|
||||
else:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
else:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"from inline message [ID:{callback_query.inline_message_id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}]")
|
||||
f"from user [ID:{callback_query.from_user.id}]"
|
||||
f"from inline message [ID:{callback_query.inline_message_id}]")
|
||||
|
||||
async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict):
|
||||
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import aiohttp
|
|||
from aiohttp.helpers import sentinel
|
||||
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
|
||||
RegexpCommandsFilter, StateFilter, Text
|
||||
RegexpCommandsFilter, StateFilter, Text, IdFilter
|
||||
from .handler import Handler
|
||||
from .middlewares import MiddlewareManager
|
||||
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
|
||||
|
|
@ -97,7 +97,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(Text, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.poll_handlers
|
||||
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers
|
||||
])
|
||||
filters_factory.bind(HashTag, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
|
|
@ -106,7 +106,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(Regexp, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.poll_handlers
|
||||
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers
|
||||
])
|
||||
filters_factory.bind(RegexpCommandsFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers
|
||||
|
|
@ -114,6 +114,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(ExceptionsFilter, event_handlers=[
|
||||
self.errors_handlers
|
||||
])
|
||||
filters_factory.bind(IdFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.inline_query_handlers
|
||||
])
|
||||
|
||||
def __del__(self):
|
||||
self.stop_polling()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter
|
||||
from .factory import FiltersFactory
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
|
||||
check_filters, get_filter_spec, get_filters_spec
|
||||
|
|
@ -23,6 +23,7 @@ __all__ = [
|
|||
'Regexp',
|
||||
'StateFilter',
|
||||
'Text',
|
||||
'IdFilter',
|
||||
'get_filter_spec',
|
||||
'get_filters_spec',
|
||||
'execute_filter',
|
||||
|
|
|
|||
|
|
@ -221,13 +221,13 @@ class Text(Filter):
|
|||
:param ignore_case: case insensitive
|
||||
"""
|
||||
# Only one mode can be used. check it.
|
||||
check = sum(map(bool, (equals, contains, startswith, endswith)))
|
||||
check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
|
||||
if check > 1:
|
||||
args = "' and '".join([arg[0] for arg in [('equals', equals),
|
||||
('contains', contains),
|
||||
('startswith', startswith),
|
||||
('endswith', endswith)
|
||||
] if arg[1]])
|
||||
] if arg[1] is not None])
|
||||
raise ValueError(f"Arguments '{args}' cannot be used together.")
|
||||
elif check == 0:
|
||||
raise ValueError(f"No one mode is specified!")
|
||||
|
|
@ -249,7 +249,7 @@ class Text(Filter):
|
|||
elif 'text_endswith' in full_config:
|
||||
return {'endswith': full_config.pop('text_endswith')}
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
|
||||
if isinstance(obj, Message):
|
||||
text = obj.text or obj.caption or ''
|
||||
if not text and obj.poll:
|
||||
|
|
@ -266,14 +266,26 @@ class Text(Filter):
|
|||
if self.ignore_case:
|
||||
text = text.lower()
|
||||
|
||||
if self.equals:
|
||||
return text == str(self.equals)
|
||||
elif self.contains:
|
||||
return str(self.contains) in text
|
||||
elif self.startswith:
|
||||
return text.startswith(str(self.startswith))
|
||||
elif self.endswith:
|
||||
return text.endswith(str(self.endswith))
|
||||
if self.equals is not None:
|
||||
self.equals = str(self.equals)
|
||||
if self.ignore_case:
|
||||
self.equals = self.equals.lower()
|
||||
return text == self.equals
|
||||
elif self.contains is not None:
|
||||
self.contains = str(self.contains)
|
||||
if self.ignore_case:
|
||||
self.contains = self.contains.lower()
|
||||
return self.contains in text
|
||||
elif self.startswith is not None:
|
||||
self.startswith = str(self.startswith)
|
||||
if self.ignore_case:
|
||||
self.startswith = self.startswith.lower()
|
||||
return text.startswith(self.startswith)
|
||||
elif self.endswith is not None:
|
||||
self.endswith = str(self.endswith)
|
||||
if self.ignore_case:
|
||||
self.endswith = self.endswith.lower()
|
||||
return text.endswith(self.endswith)
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -359,13 +371,17 @@ class Regexp(Filter):
|
|||
if 'regexp' in full_config:
|
||||
return {'regexp': full_config.pop('regexp')}
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery]):
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
|
||||
if isinstance(obj, Message):
|
||||
content = obj.text or obj.caption or ''
|
||||
if not content and obj.poll:
|
||||
content = obj.poll.question
|
||||
elif isinstance(obj, CallbackQuery) and obj.data:
|
||||
content = obj.data
|
||||
elif isinstance(obj, InlineQuery):
|
||||
content = obj.query
|
||||
elif isinstance(obj, Poll):
|
||||
content = obj.question
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -487,3 +503,66 @@ class ExceptionsFilter(BoundFilter):
|
|||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
class IdFilter(Filter):
|
||||
|
||||
def __init__(self,
|
||||
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
):
|
||||
"""
|
||||
:param user_id:
|
||||
:param chat_id:
|
||||
"""
|
||||
if user_id is None and chat_id is None:
|
||||
raise ValueError("Both user_id and chat_id can't be None")
|
||||
|
||||
self.user_id = None
|
||||
self.chat_id = None
|
||||
if user_id:
|
||||
if isinstance(user_id, Iterable):
|
||||
self.user_id = list(map(int, user_id))
|
||||
else:
|
||||
self.user_id = [int(user_id), ]
|
||||
if chat_id:
|
||||
if isinstance(chat_id, Iterable):
|
||||
self.chat_id = list(map(int, chat_id))
|
||||
else:
|
||||
self.chat_id = [int(chat_id), ]
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
result = {}
|
||||
if 'user_id' in full_config:
|
||||
result['user_id'] = full_config.pop('user_id')
|
||||
|
||||
if 'chat_id' in full_config:
|
||||
result['chat_id'] = full_config.pop('chat_id')
|
||||
|
||||
return result
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
|
||||
if isinstance(obj, Message):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = obj.chat.id
|
||||
elif isinstance(obj, CallbackQuery):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = None
|
||||
if obj.message is not None:
|
||||
# if the button was sent with message
|
||||
chat_id = obj.message.chat.id
|
||||
elif isinstance(obj, InlineQuery):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = None
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.user_id and self.chat_id:
|
||||
return user_id in self.user_id and chat_id in self.chat_id
|
||||
elif self.user_id:
|
||||
return user_id in self.user_id
|
||||
elif self.chat_id:
|
||||
return chat_id in self.chat_id
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -203,12 +203,12 @@ class BoundFilter(Filter):
|
|||
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]:
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ class CancelHandler(Exception):
|
|||
def _get_spec(func: callable):
|
||||
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
|
||||
func = func.__wrapped__
|
||||
|
||||
spec = inspect.getfullargspec(func)
|
||||
return spec, func
|
||||
return spec
|
||||
|
||||
|
||||
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
|
||||
|
|
@ -56,7 +55,7 @@ class Handler:
|
|||
:param filters: list of filters
|
||||
:param index: you can reorder handlers
|
||||
"""
|
||||
spec, handler = _get_spec(handler)
|
||||
spec = _get_spec(handler)
|
||||
|
||||
if filters and not isinstance(filters, (list, tuple, set)):
|
||||
filters = [filters]
|
||||
|
|
@ -105,7 +104,7 @@ class Handler:
|
|||
try:
|
||||
for handler_obj in self.handlers:
|
||||
try:
|
||||
data.update(await check_filters(handler_obj.filters, args + (data,)))
|
||||
data.update(await check_filters(handler_obj.filters, args))
|
||||
except FilterNotPassed:
|
||||
continue
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import functools
|
|||
import ipaddress
|
||||
import itertools
|
||||
import typing
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from aiohttp import web
|
||||
|
|
@ -35,6 +36,8 @@ TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22')
|
|||
|
||||
allowed_ips = set()
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_ip(ip: str) -> bool:
|
||||
"""
|
||||
|
|
@ -77,7 +80,7 @@ class WebhookRequestHandler(web.View):
|
|||
|
||||
.. code-block:: python3
|
||||
|
||||
app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler')
|
||||
app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler')
|
||||
|
||||
But first you need to configure application for getting Dispatcher instance from request handler!
|
||||
It must always be with key 'BOT_DISPATCHER'
|
||||
|
|
@ -258,7 +261,9 @@ class WebhookRequestHandler(web.View):
|
|||
if self.request.app.get('_check_ip', False):
|
||||
ip_address, accept = self.check_ip()
|
||||
if not accept:
|
||||
log.warning(f"Blocking request from an unauthorized IP: {ip_address}")
|
||||
raise web.HTTPUnauthorized()
|
||||
|
||||
# context.set_value('TELEGRAM_IP', ip_address)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import typing
|
|||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .chat_permissions import ChatPermissions
|
||||
from .chat_photo import ChatPhoto
|
||||
from ..utils import helper
|
||||
from ..utils import markdown
|
||||
|
|
@ -27,6 +28,7 @@ class Chat(base.TelegramObject):
|
|||
description: base.String = fields.Field()
|
||||
invite_link: base.String = fields.Field()
|
||||
pinned_message: 'Message' = fields.Field(base='Message')
|
||||
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
|
||||
sticker_set_name: base.String = fields.Field()
|
||||
can_set_sticker_set: base.Boolean = fields.Field()
|
||||
|
||||
|
|
@ -202,6 +204,7 @@ class Chat(base.TelegramObject):
|
|||
return await self.bot.unban_chat_member(self.id, user_id=user_id)
|
||||
|
||||
async def restrict(self, user_id: base.Integer,
|
||||
permissions: typing.Optional[ChatPermissions] = None,
|
||||
until_date: typing.Union[base.Integer, None] = None,
|
||||
can_send_messages: typing.Union[base.Boolean, None] = None,
|
||||
can_send_media_messages: typing.Union[base.Boolean, None] = None,
|
||||
|
|
@ -216,6 +219,8 @@ class Chat(base.TelegramObject):
|
|||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
:param permissions: New user permissions
|
||||
:type permissions: :obj:`ChatPermissions`
|
||||
:param until_date: Date when restrictions will be lifted for the user, unix time.
|
||||
:type until_date: :obj:`typing.Union[base.Integer, None]`
|
||||
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
|
||||
|
|
@ -232,7 +237,9 @@ class Chat(base.TelegramObject):
|
|||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date,
|
||||
return await self.bot.restrict_chat_member(self.id, user_id=user_id,
|
||||
permissions=permissions,
|
||||
until_date=until_date,
|
||||
can_send_messages=can_send_messages,
|
||||
can_send_media_messages=can_send_media_messages,
|
||||
can_send_other_messages=can_send_other_messages,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class ChatMember(base.TelegramObject):
|
|||
is_member: base.Boolean = fields.Field()
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: base.Boolean = fields.Field()
|
||||
can_send_polls: base.Boolean = fields.Field()
|
||||
can_send_other_messages: base.Boolean = fields.Field()
|
||||
can_add_web_page_previews: base.Boolean = fields.Field()
|
||||
|
||||
|
|
|
|||
39
aiogram/types/chat_permissions.py
Normal file
39
aiogram/types/chat_permissions.py
Normal 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,
|
||||
)
|
||||
|
|
@ -26,7 +26,7 @@ class InputMedia(base.TelegramObject):
|
|||
media: base.String = fields.Field(alias='media', on_change='_media_changed')
|
||||
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
|
||||
caption: base.String = fields.Field()
|
||||
parse_mode: base.Boolean = fields.Field()
|
||||
parse_mode: base.String = fields.Field()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._thumb_file = None
|
||||
|
|
@ -110,7 +110,7 @@ class InputMediaAnimation(InputMedia):
|
|||
thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None,
|
||||
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
|
||||
parse_mode: base.Boolean = None, **kwargs):
|
||||
parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
parse_mode=parse_mode, conf=kwargs)
|
||||
|
|
@ -124,7 +124,7 @@ class InputMediaDocument(InputMedia):
|
|||
"""
|
||||
|
||||
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
|
||||
caption: base.String = None, parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
|
||||
caption=caption, parse_mode=parse_mode,
|
||||
conf=kwargs)
|
||||
|
|
@ -150,7 +150,7 @@ class InputMediaAudio(InputMedia):
|
|||
duration: base.Integer = None,
|
||||
performer: base.String = None,
|
||||
title: base.String = None,
|
||||
parse_mode: base.Boolean = None, **kwargs):
|
||||
parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
performer=performer, title=title,
|
||||
|
|
@ -165,7 +165,7 @@ class InputMediaPhoto(InputMedia):
|
|||
"""
|
||||
|
||||
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
|
||||
caption: base.String = None, parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
|
||||
caption=caption, parse_mode=parse_mode,
|
||||
conf=kwargs)
|
||||
|
|
@ -186,7 +186,7 @@ class InputMediaVideo(InputMedia):
|
|||
thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None,
|
||||
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
|
||||
parse_mode: base.Boolean = None,
|
||||
parse_mode: base.String = None,
|
||||
supports_streaming: base.Boolean = None, **kwargs):
|
||||
super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
|
|
@ -277,7 +277,7 @@ class MediaGroup(base.TelegramObject):
|
|||
duration: base.Integer = None,
|
||||
performer: base.String = None,
|
||||
title: base.String = None,
|
||||
parse_mode: base.Boolean = None):
|
||||
parse_mode: base.String = None):
|
||||
"""
|
||||
Attach animation
|
||||
|
||||
|
|
@ -299,7 +299,7 @@ class MediaGroup(base.TelegramObject):
|
|||
self.attach(audio)
|
||||
|
||||
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None):
|
||||
caption: base.String = None, parse_mode: base.String = None):
|
||||
"""
|
||||
Attach document
|
||||
|
||||
|
|
|
|||
|
|
@ -959,70 +959,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_animation(self, animation: typing.Union[base.InputFile, base.String],
|
||||
duration: typing.Union[base.Integer, None] = None,
|
||||
width: typing.Union[base.Integer, None] = None,
|
||||
height: typing.Union[base.Integer, None] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
caption: typing.Union[base.String, None] = None,
|
||||
parse_mode: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
|
||||
|
||||
On success, the sent Message is returned.
|
||||
Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
|
||||
|
||||
Source https://core.telegram.org/bots/api#sendanimation
|
||||
|
||||
:param animation: Animation to send. Pass a file_id as String to send an animation that exists
|
||||
on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation
|
||||
from the Internet, or upload a new animation using multipart/form-data
|
||||
:type animation: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
:param duration: Duration of sent animation in seconds
|
||||
:type duration: :obj:`typing.Union[base.Integer, None]`
|
||||
:param width: Animation width
|
||||
:type width: :obj:`typing.Union[base.Integer, None]`
|
||||
:param height: Animation height
|
||||
:type height: :obj:`typing.Union[base.Integer, None]`
|
||||
:param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
|
||||
A thumbnail‘s width and height should not exceed 90.
|
||||
:type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
|
||||
:param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters
|
||||
:type caption: :obj:`typing.Union[base.String, None]`
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
fixed-width text or inline URLs in the media caption
|
||||
:type parse_mode: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove, types.ForceReply], None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_animation" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_animation(self.chat.id,
|
||||
animation=animation,
|
||||
duration=duration,
|
||||
width=width,
|
||||
height=height,
|
||||
thumb=thumb,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
|
||||
duration: typing.Union[base.Integer, None] = None,
|
||||
|
|
@ -1323,55 +1259,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_venue(self,
|
||||
latitude: base.Float, longitude: base.Float,
|
||||
title: base.String, address: base.String,
|
||||
foursquare_id: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send information about a venue.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendvenue
|
||||
|
||||
:param latitude: Latitude of the venue
|
||||
:type latitude: :obj:`base.Float`
|
||||
:param longitude: Longitude of the venue
|
||||
:type longitude: :obj:`base.Float`
|
||||
:param title: Name of the venue
|
||||
:type title: :obj:`base.String`
|
||||
:param address: Address of the venue
|
||||
:type address: :obj:`base.String`
|
||||
:param foursquare_id: Foursquare identifier of the venue
|
||||
:type foursquare_id: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned.
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_venue" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_venue(chat_id=self.chat.id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
title=title,
|
||||
address=address,
|
||||
foursquare_id=foursquare_id,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_venue(self,
|
||||
latitude: base.Float, longitude: base.Float,
|
||||
title: base.String, address: base.String,
|
||||
|
|
@ -1417,46 +1304,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_contact(self, phone_number: base.String,
|
||||
first_name: base.String, last_name: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send phone contacts.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendcontact
|
||||
|
||||
:param phone_number: Contact's phone number
|
||||
:type phone_number: :obj:`base.String`
|
||||
:param first_name: Contact's first name
|
||||
:type first_name: :obj:`base.String`
|
||||
:param last_name: Contact's last name
|
||||
:type last_name: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned.
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_contact" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_contact(chat_id=self.chat.id,
|
||||
phone_number=phone_number,
|
||||
first_name=first_name, last_name=last_name,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_contact(self, phone_number: base.String,
|
||||
first_name: base.String, last_name: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
|
|||
file_id: base.String = fields.Field()
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
is_animated: base.Boolean = fields.Field()
|
||||
thumb: PhotoSize = fields.Field(base=PhotoSize)
|
||||
emoji: base.String = fields.Field()
|
||||
set_name: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ class StickerSet(base.TelegramObject):
|
|||
"""
|
||||
name: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
is_animated: base.Boolean = fields.Field()
|
||||
contains_masks: base.Boolean = fields.Field()
|
||||
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
|
|||
|
||||
|
||||
class BotKicked(Unauthorized):
|
||||
match = 'Bot was kicked from a chat'
|
||||
match = 'bot was kicked from a chat'
|
||||
|
||||
|
||||
class BotBlocked(Unauthorized):
|
||||
|
|
|
|||
|
|
@ -111,6 +111,14 @@ ExceptionsFilter
|
|||
:show-inheritance:
|
||||
|
||||
|
||||
IdFilter
|
||||
----------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Making own filters (Custom filters)
|
||||
===================================
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ API_TOKEN = 'BOT TOKEN HERE'
|
|||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger('broadcast')
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
|
||||
dp = Dispatcher(bot, loop=loop)
|
||||
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
def get_users():
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ logging.basicConfig(level=logging.INFO)
|
|||
|
||||
API_TOKEN = 'BOT TOKEN HERE'
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)
|
||||
|
||||
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML)
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(bot, storage=storage)
|
||||
dp.middleware.setup(LoggingMiddleware())
|
||||
|
|
|
|||
70
examples/callback_data_factory_simple.py
Normal file
70
examples/callback_data_factory_simple.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""
|
||||
This is a simple example of usage of CallbackData factory
|
||||
For more comprehensive example see callback_data_factory.py
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
from aiogram.contrib.middlewares.logging import LoggingMiddleware
|
||||
from aiogram.utils.callback_data import CallbackData
|
||||
from aiogram.utils.exceptions import MessageNotModified
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
API_TOKEN = 'BOT_TOKEN_HERE'
|
||||
|
||||
|
||||
bot = Bot(token=API_TOKEN)
|
||||
|
||||
dp = Dispatcher(bot)
|
||||
dp.middleware.setup(LoggingMiddleware())
|
||||
|
||||
vote_cb = CallbackData('vote', 'action') # vote:<action>
|
||||
likes = {} # user_id: amount_of_likes
|
||||
|
||||
|
||||
def get_keyboard():
|
||||
return types.InlineKeyboardMarkup().row(
|
||||
types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')),
|
||||
types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')))
|
||||
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def cmd_start(message: types.Message):
|
||||
amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0
|
||||
await message.reply(f'Vote! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard())
|
||||
|
||||
|
||||
@dp.callback_query_handler(vote_cb.filter(action='up'))
|
||||
async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict):
|
||||
logging.info(callback_data) # callback_data contains all info from callback data
|
||||
likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage
|
||||
amount_of_likes = likes[query.from_user.id]
|
||||
|
||||
await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.',
|
||||
query.from_user.id,
|
||||
query.message.message_id,
|
||||
reply_markup=get_keyboard())
|
||||
|
||||
|
||||
@dp.callback_query_handler(vote_cb.filter(action='down'))
|
||||
async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict):
|
||||
logging.info(callback_data) # callback_data contains all info from callback data
|
||||
likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage
|
||||
amount_of_likes = likes[query.from_user.id]
|
||||
|
||||
await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.',
|
||||
query.from_user.id,
|
||||
query.message.message_id,
|
||||
reply_markup=get_keyboard())
|
||||
|
||||
|
||||
@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
|
||||
async def message_not_modified_handler(update, error):
|
||||
# pass
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
@ -11,8 +11,8 @@ API_TOKEN = 'BOT TOKEN HERE'
|
|||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.MARKDOWN)
|
||||
|
||||
bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ from aiogram.utils import executor
|
|||
|
||||
API_TOKEN = 'BOT TOKEN HERE'
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
bot = Bot(token=API_TOKEN, loop=loop)
|
||||
bot = Bot(token=API_TOKEN)
|
||||
|
||||
# For example use simple MemoryStorage for Dispatcher.
|
||||
storage = MemoryStorage()
|
||||
|
|
@ -112,9 +110,9 @@ async def process_gender(message: types.Message, state: FSMContext):
|
|||
md.text('Gender:', data['gender']),
|
||||
sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
# Finish conversation
|
||||
data.state = None
|
||||
# Finish conversation
|
||||
await state.finish()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, loop=loop, skip_updates=True)
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
38
examples/id_filter_example.py
Normal file
38
examples/id_filter_example.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from aiogram import Bot, Dispatcher, executor, types
|
||||
from aiogram.dispatcher.handler import SkipHandler
|
||||
|
||||
API_TOKEN = 'API_TOKE_HERE'
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
user_id_to_test = None # todo: Set id here
|
||||
chat_id_to_test = user_id_to_test
|
||||
|
||||
|
||||
@dp.message_handler(user_id=user_id_to_test)
|
||||
async def handler1(msg: types.Message):
|
||||
await bot.send_message(msg.chat.id,
|
||||
"Hello, checking with user_id=")
|
||||
raise SkipHandler
|
||||
|
||||
|
||||
@dp.message_handler(chat_id=chat_id_to_test)
|
||||
async def handler2(msg: types.Message):
|
||||
await bot.send_message(msg.chat.id,
|
||||
"Hello, checking with chat_id=")
|
||||
raise SkipHandler
|
||||
|
||||
|
||||
@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test)
|
||||
async def handler3(msg: types.Message):
|
||||
await bot.send_message(msg.chat.id,
|
||||
"Hello from user= & chat_id=")
|
||||
|
||||
|
||||
@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here
|
||||
async def handler4(msg: types.Message):
|
||||
print("Checked user_id with list!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp)
|
||||
|
|
@ -7,8 +7,7 @@ API_TOKEN = 'BOT TOKEN HERE'
|
|||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop)
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
@ -21,4 +20,4 @@ async def inline_echo(inline_query: types.InlineQuery):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, loop=loop, skip_updates=True)
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
56
examples/inline_keyboard_example.py
Normal file
56
examples/inline_keyboard_example.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
This bot is created for the demonstration of a usage of inline keyboards.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
|
||||
API_TOKEN = 'BOT_TOKEN_HERE'
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Initialize bot and dispatcher
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def start_cmd_handler(message: types.Message):
|
||||
keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
|
||||
# default row_width is 3, so here we can omit it actually
|
||||
# kept for clearness
|
||||
|
||||
keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'),
|
||||
# in real life for the callback_data the callback data factory should be used
|
||||
# here the raw string is used for the simplicity
|
||||
types.InlineKeyboardButton("No!", callback_data='no'))
|
||||
|
||||
keyboard_markup.add(types.InlineKeyboardButton("aiogram link",
|
||||
url='https://github.com/aiogram/aiogram'))
|
||||
# url buttons has no callback data
|
||||
|
||||
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
|
||||
|
||||
|
||||
@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no'
|
||||
# @dp.callback_query_handler(text='yes') # if cb.data == 'yes'
|
||||
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
|
||||
await query.answer() # send answer to close the rounding circle
|
||||
|
||||
answer_data = query.data
|
||||
logger.debug(f"answer_data={answer_data}")
|
||||
# here we can work with query.data
|
||||
if answer_data == 'yes':
|
||||
await bot.send_message(query.from_user.id, "That's great!")
|
||||
elif answer_data == 'no':
|
||||
await bot.send_message(query.from_user.id, "Oh no...Why so?")
|
||||
else:
|
||||
await bot.send_message(query.from_user.id, "Invalid callback data!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
@ -4,8 +4,7 @@ from aiogram import Bot, Dispatcher, executor, filters, types
|
|||
|
||||
API_TOKEN = 'BOT TOKEN HERE'
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop)
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
@ -40,4 +39,4 @@ async def send_welcome(message: types.Message):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, loop=loop, skip_updates=True)
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ from aiogram.utils.exceptions import Throttled
|
|||
|
||||
TOKEN = 'BOT TOKEN HERE'
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# In this example Redis storage is used
|
||||
storage = RedisStorage2(db=5)
|
||||
|
||||
bot = Bot(token=TOKEN, loop=loop)
|
||||
bot = Bot(token=TOKEN)
|
||||
dp = Dispatcher(bot, storage=storage)
|
||||
|
||||
|
||||
|
|
@ -119,4 +117,4 @@ if __name__ == '__main__':
|
|||
dp.middleware.setup(ThrottlingMiddleware())
|
||||
|
||||
# Start long-polling
|
||||
executor.start_polling(dp, loop=loop)
|
||||
executor.start_polling(dp)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ from aiogram.utils import executor
|
|||
BOT_TOKEN = 'BOT TOKEN HERE'
|
||||
PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef'
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(BOT_TOKEN)
|
||||
dp = Dispatcher(bot, loop=loop)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
# Setup prices
|
||||
prices = [
|
||||
|
|
@ -96,4 +95,4 @@ async def got_payment(message: types.Message):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, loop=loop)
|
||||
executor.start_polling(dp)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/'
|
|||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL)
|
||||
bot = Bot(token=API_TOKEN, proxy=PROXY_URL)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
@ -62,4 +61,4 @@ async def cmd_start(message: types.Message):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_polling(dp, loop=loop, skip_updates=True)
|
||||
start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
61
examples/regular_keyboard_example.py
Normal file
61
examples/regular_keyboard_example.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
This bot is created for the demonstration of a usage of regular keyboards.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
|
||||
API_TOKEN = 'BOT_TOKEN_HERE'
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Initialize bot and dispatcher
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def start_cmd_handler(message: types.Message):
|
||||
keyboard_markup = types.ReplyKeyboardMarkup(row_width=3)
|
||||
# default row_width is 3, so here we can omit it actually
|
||||
# kept for clearness
|
||||
|
||||
keyboard_markup.row(types.KeyboardButton("Yes!"),
|
||||
types.KeyboardButton("No!"))
|
||||
# adds buttons as a new row to the existing keyboard
|
||||
# the behaviour doesn't depend on row_width attribute
|
||||
|
||||
keyboard_markup.add(types.KeyboardButton("I don't know"),
|
||||
types.KeyboardButton("Who am i?"),
|
||||
types.KeyboardButton("Where am i?"),
|
||||
types.KeyboardButton("Who is there?"))
|
||||
# adds buttons. New rows is formed according to row_width parameter
|
||||
|
||||
await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup)
|
||||
|
||||
|
||||
@dp.message_handler()
|
||||
async def all_msg_handler(message: types.Message):
|
||||
# pressing of a KeyboardButton is the same as sending the regular message with the same text
|
||||
# so, to handle the responses from the keyboard, we need to use a message_handler
|
||||
# in real bot, it's better to define message_handler(text="...") for each button
|
||||
# but here for the simplicity only one handler is defined
|
||||
|
||||
text_of_button = message.text
|
||||
logger.debug(text_of_button) # print the text we got
|
||||
|
||||
if text_of_button == 'Yes!':
|
||||
await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove())
|
||||
elif text_of_button == 'No!':
|
||||
await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove())
|
||||
else:
|
||||
await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove())
|
||||
# with message, we send types.ReplyKeyboardRemove() to hide the keyboard
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
@ -17,8 +17,7 @@ API_TOKEN = 'BOT TOKEN HERE'
|
|||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop)
|
||||
bot = Bot(token=API_TOKEN)
|
||||
|
||||
# Throttling manager does not work without Leaky Bucket.
|
||||
# Then need to use storages. For example use simple in-memory storage.
|
||||
|
|
@ -40,4 +39,4 @@ async def send_welcome(message: types.Message):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_polling(dp, loop=loop, skip_updates=True)
|
||||
start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
|
|
@ -37,8 +37,7 @@ WEBAPP_PORT = 3001
|
|||
|
||||
BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(TOKEN, loop=loop)
|
||||
bot = Bot(TOKEN)
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(bot, storage=storage)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ WEBAPP_PORT = 3001
|
|||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
bot = Bot(token=API_TOKEN, loop=loop)
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
|
|
|
|||
197
tests/test_filters.py
Normal file
197
tests/test_filters.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.dispatcher.filters import Text
|
||||
from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
|
||||
|
||||
|
||||
class TestTextFilter:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("test_prefix, test_text, ignore_case",
|
||||
[('', '', True),
|
||||
('', 'exAmple_string', True),
|
||||
('', '', False),
|
||||
('', 'exAmple_string', False),
|
||||
|
||||
('example_string', 'example_string', True),
|
||||
('example_string', 'exAmple_string', True),
|
||||
('exAmple_string', 'example_string', True),
|
||||
|
||||
('example_string', 'example_string', False),
|
||||
('example_string', 'exAmple_string', False),
|
||||
('exAmple_string', 'example_string', False),
|
||||
|
||||
('example_string', 'example_string_dsf', True),
|
||||
('example_string', 'example_striNG_dsf', True),
|
||||
('example_striNG', 'example_string_dsf', True),
|
||||
|
||||
('example_string', 'example_string_dsf', False),
|
||||
('example_string', 'example_striNG_dsf', False),
|
||||
('example_striNG', 'example_string_dsf', False),
|
||||
|
||||
('example_string', 'not_example_string', True),
|
||||
('example_string', 'not_eXample_string', True),
|
||||
('EXample_string', 'not_example_string', True),
|
||||
|
||||
('example_string', 'not_example_string', False),
|
||||
('example_string', 'not_eXample_string', False),
|
||||
('EXample_string', 'not_example_string', False),
|
||||
])
|
||||
async def test_startswith(self, test_prefix, test_text, ignore_case):
|
||||
test_filter = Text(startswith=test_prefix, ignore_case=ignore_case)
|
||||
|
||||
async def check(obj):
|
||||
result = await test_filter.check(obj)
|
||||
if ignore_case:
|
||||
_test_prefix = test_prefix.lower()
|
||||
_test_text = test_text.lower()
|
||||
else:
|
||||
_test_prefix = test_prefix
|
||||
_test_text = test_text
|
||||
|
||||
return result is _test_text.startswith(_test_prefix)
|
||||
|
||||
assert await check(Message(text=test_text))
|
||||
assert await check(CallbackQuery(data=test_text))
|
||||
assert await check(InlineQuery(query=test_text))
|
||||
assert await check(Poll(question=test_text))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("test_postfix, test_text, ignore_case",
|
||||
[('', '', True),
|
||||
('', 'exAmple_string', True),
|
||||
('', '', False),
|
||||
('', 'exAmple_string', False),
|
||||
|
||||
('example_string', 'example_string', True),
|
||||
('example_string', 'exAmple_string', True),
|
||||
('exAmple_string', 'example_string', True),
|
||||
|
||||
('example_string', 'example_string', False),
|
||||
('example_string', 'exAmple_string', False),
|
||||
('exAmple_string', 'example_string', False),
|
||||
|
||||
('example_string', 'example_string_dsf', True),
|
||||
('example_string', 'example_striNG_dsf', True),
|
||||
('example_striNG', 'example_string_dsf', True),
|
||||
|
||||
('example_string', 'example_string_dsf', False),
|
||||
('example_string', 'example_striNG_dsf', False),
|
||||
('example_striNG', 'example_string_dsf', False),
|
||||
|
||||
('example_string', 'not_example_string', True),
|
||||
('example_string', 'not_eXample_string', True),
|
||||
('EXample_string', 'not_eXample_string', True),
|
||||
|
||||
('example_string', 'not_example_string', False),
|
||||
('example_string', 'not_eXample_string', False),
|
||||
('EXample_string', 'not_example_string', False),
|
||||
])
|
||||
async def test_endswith(self, test_postfix, test_text, ignore_case):
|
||||
test_filter = Text(endswith=test_postfix, ignore_case=ignore_case)
|
||||
|
||||
async def check(obj):
|
||||
result = await test_filter.check(obj)
|
||||
if ignore_case:
|
||||
_test_postfix = test_postfix.lower()
|
||||
_test_text = test_text.lower()
|
||||
else:
|
||||
_test_postfix = test_postfix
|
||||
_test_text = test_text
|
||||
|
||||
return result is _test_text.endswith(_test_postfix)
|
||||
|
||||
assert await check(Message(text=test_text))
|
||||
assert await check(CallbackQuery(data=test_text))
|
||||
assert await check(InlineQuery(query=test_text))
|
||||
assert await check(Poll(question=test_text))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("test_string, test_text, ignore_case",
|
||||
[('', '', True),
|
||||
('', 'exAmple_string', True),
|
||||
('', '', False),
|
||||
('', 'exAmple_string', False),
|
||||
|
||||
('example_string', 'example_string', True),
|
||||
('example_string', 'exAmple_string', True),
|
||||
('exAmple_string', 'example_string', True),
|
||||
|
||||
('example_string', 'example_string', False),
|
||||
('example_string', 'exAmple_string', False),
|
||||
('exAmple_string', 'example_string', False),
|
||||
|
||||
('example_string', 'example_string_dsf', True),
|
||||
('example_string', 'example_striNG_dsf', True),
|
||||
('example_striNG', 'example_string_dsf', True),
|
||||
|
||||
('example_string', 'example_string_dsf', False),
|
||||
('example_string', 'example_striNG_dsf', False),
|
||||
('example_striNG', 'example_string_dsf', False),
|
||||
|
||||
('example_string', 'not_example_strin', True),
|
||||
('example_string', 'not_eXample_strin', True),
|
||||
('EXample_string', 'not_eXample_strin', True),
|
||||
|
||||
('example_string', 'not_example_strin', False),
|
||||
('example_string', 'not_eXample_strin', False),
|
||||
('EXample_string', 'not_example_strin', False),
|
||||
])
|
||||
async def test_contains(self, test_string, test_text, ignore_case):
|
||||
test_filter = Text(contains=test_string, ignore_case=ignore_case)
|
||||
|
||||
async def check(obj):
|
||||
result = await test_filter.check(obj)
|
||||
if ignore_case:
|
||||
_test_string = test_string.lower()
|
||||
_test_text = test_text.lower()
|
||||
else:
|
||||
_test_string = test_string
|
||||
_test_text = test_text
|
||||
|
||||
return result is (_test_string in _test_text)
|
||||
|
||||
assert await check(Message(text=test_text))
|
||||
assert await check(CallbackQuery(data=test_text))
|
||||
assert await check(InlineQuery(query=test_text))
|
||||
assert await check(Poll(question=test_text))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("test_filter_text, test_text, ignore_case",
|
||||
[('', '', True),
|
||||
('', 'exAmple_string', True),
|
||||
('', '', False),
|
||||
('', 'exAmple_string', False),
|
||||
|
||||
('example_string', 'example_string', True),
|
||||
('example_string', 'exAmple_string', True),
|
||||
('exAmple_string', 'example_string', True),
|
||||
|
||||
('example_string', 'example_string', False),
|
||||
('example_string', 'exAmple_string', False),
|
||||
('exAmple_string', 'example_string', False),
|
||||
|
||||
('example_string', 'not_example_string', True),
|
||||
('example_string', 'not_eXample_string', True),
|
||||
('EXample_string', 'not_eXample_string', True),
|
||||
|
||||
('example_string', 'not_example_string', False),
|
||||
('example_string', 'not_eXample_string', False),
|
||||
('EXample_string', 'not_example_string', False),
|
||||
])
|
||||
async def test_equals_string(self, test_filter_text, test_text, ignore_case):
|
||||
test_filter = Text(equals=test_filter_text, ignore_case=ignore_case)
|
||||
|
||||
async def check(obj):
|
||||
result = await test_filter.check(obj)
|
||||
if ignore_case:
|
||||
_test_filter_text = test_filter_text.lower()
|
||||
_test_text = test_text.lower()
|
||||
else:
|
||||
_test_filter_text = test_filter_text
|
||||
_test_text = test_text
|
||||
return result is (_test_text == _test_filter_text)
|
||||
|
||||
assert await check(Message(text=test_text))
|
||||
assert await check(CallbackQuery(data=test_text))
|
||||
assert await check(InlineQuery(query=test_text))
|
||||
assert await check(Poll(question=test_text))
|
||||
Loading…
Add table
Add a link
Reference in a new issue