Merge branch 'aiogram:dev-2.x' into dev-2.x

This commit is contained in:
Юрий 2021-07-26 23:19:17 +03:00 committed by GitHub
commit 371f589298
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 5362 additions and 1990 deletions

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
open_collective: aiogram
patreon: aiogram

View file

@ -43,7 +43,7 @@ test:
tox
summary:
cloc aiogram/ tests/ examples/ setup.py
cloc aiogram/ tests/ examples/ docs/ setup.py
docs: docs/source/*
cd docs && $(MAKE) html

View file

@ -6,22 +6,82 @@
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT)
**aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
You can [read the docs here](http://docs.aiogram.dev/en/latest/).
## Examples
<details>
<summary>📚 Click to see some basic examples</summary>
**Few steps before getting started...**
- First, you should obtain token for your bot from [BotFather](https://t.me/BotFather).
- Install latest stable version of aiogram, simply running `pip install aiogram`
### Simple [`getMe`](https://core.telegram.org/bots/api#getme) request
```python
import asyncio
from aiogram import Bot
BOT_TOKEN = ""
async def main():
bot = Bot(token=BOT_TOKEN)
try:
me = await bot.get_me()
print(f"🤖 Hello, I'm {me.first_name}.\nHave a nice Day!")
finally:
await bot.close()
asyncio.run(main())
```
### Poll BotAPI for updates and process updates
```python
import asyncio
from aiogram import Bot, Dispatcher, types
BOT_TOKEN = ""
async def start_handler(event: types.Message):
await event.answer(
f"Hello, {event.from_user.get_mention(as_html=True)} 👋!",
parse_mode=types.ParseMode.HTML,
)
async def main():
bot = Bot(token=BOT_TOKEN)
try:
disp = Dispatcher(bot=bot)
disp.register_message_handler(start_handler, commands={"start", "restart"})
await disp.start_polling()
finally:
await bot.close()
asyncio.run(main())
```
### Moar!
You can find more examples in [`examples/`](https://github.com/aiogram/aiogram/tree/dev-2.x/examples) directory
</details>
## Official aiogram resources:
- News: [@aiogram_live](https://t.me/aiogram_live)
- Community: [@aiogram](https://t.me/aiogram)
- Russian community: [@aiogram_ru](https://t.me/aiogram_ru)
- Pip: [aiogram](https://pypi.python.org/pypi/aiogram)
- Docs: [aiogram site](https://docs.aiogram.dev/)
- PyPI: [aiogram](https://pypi.python.org/pypi/aiogram)
- Documentation: [aiogram site](https://docs.aiogram.dev/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)

View file

@ -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.9-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -25,7 +25,7 @@ else:
if 'DISABLE_UVLOOP' not in os.environ:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
__all__ = [
__all__ = (
'Bot',
'Dispatcher',
'__api_version__',
@ -40,8 +40,8 @@ __all__ = [
'md',
'middlewares',
'types',
'utils'
]
'utils',
)
__version__ = '2.9.2'
__api_version__ = '4.9'
__version__ = '2.14.3'
__api_version__ = '5.3'

View file

@ -2,8 +2,8 @@ from . import api
from .base import BaseBot
from .bot import Bot
__all__ = [
__all__ = (
'BaseBot',
'Bot',
'api'
]
'api',
)

View file

@ -1,20 +1,57 @@
import logging
import os
from dataclasses import dataclass
from http import HTTPStatus
import aiohttp
from .. import types
from ..utils import exceptions
from ..utils import json
from ..utils import exceptions, json
from ..utils.helper import Helper, HelperMode, Item
# Main aiogram logger
log = logging.getLogger('aiogram')
# API Url's
API_URL = "https://api.telegram.org/bot{token}/{method}"
FILE_URL = "https://api.telegram.org/file/bot{token}/{path}"
@dataclass(frozen=True)
class TelegramAPIServer:
"""
Base config for API Endpoints
"""
base: str
file: str
def api_url(self, token: str, method: str) -> str:
"""
Generate URL for API methods
:param token: Bot token
:param method: API method name (case insensitive)
:return: URL
"""
return self.base.format(token=token, method=method)
def file_url(self, token: str, path: str) -> str:
"""
Generate URL for downloading files
:param token: Bot token
:param path: file path
:return: URL
"""
return self.file.format(token=token, path=path)
@classmethod
def from_base(cls, base: str) -> 'TelegramAPIServer':
base = base.rstrip("/")
return cls(
base=f"{base}/bot{{token}}/{{method}}",
file=f"{base}/file/bot{{token}}/{{path}}",
)
TELEGRAM_PRODUCTION = TelegramAPIServer.from_base("https://api.telegram.org")
def check_token(token: str) -> bool:
@ -80,7 +117,7 @@ def check_result(method_name: str, content_type: str, status_code: int, body: st
exceptions.NotFound.detect(description)
elif status_code == HTTPStatus.CONFLICT:
exceptions.ConflictError.detect(description)
elif status_code in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]:
elif status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
exceptions.Unauthorized.detect(description)
elif status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
raise exceptions.NetworkError('File too large for uploading. '
@ -92,11 +129,10 @@ def check_result(method_name: str, content_type: str, status_code: int, body: st
raise exceptions.TelegramAPIError(f"{description} [{status_code}]")
async def make_request(session, token, method, data=None, files=None, **kwargs):
# log.debug(f"Make request: '{method}' with data: {data} and files {files}")
async def make_request(session, server, token, method, data=None, files=None, **kwargs):
log.debug('Make request: "%s" with data: "%r" and files "%r"', method, data, files)
url = Methods.api_url(token=token, method=method)
url = server.api_url(token=token, method=method)
req = compose_data(data, files)
try:
@ -153,7 +189,7 @@ class Methods(Helper):
"""
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
List is updated to Bot API 4.9
List is updated to Bot API 5.3
"""
mode = HelperMode.lowerCamelCase
@ -165,8 +201,11 @@ class Methods(Helper):
# Available methods
GET_ME = Item() # getMe
LOG_OUT = Item() # logOut
CLOSE = Item() # close
SEND_MESSAGE = Item() # sendMessage
FORWARD_MESSAGE = Item() # forwardMessage
COPY_MESSAGE = Item() # copyMessage
SEND_PHOTO = Item() # sendPhoto
SEND_AUDIO = Item() # sendAudio
SEND_DOCUMENT = Item() # sendDocument
@ -186,27 +225,34 @@ class Methods(Helper):
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
GET_FILE = Item() # getFile
KICK_CHAT_MEMBER = Item() # kickChatMember
BAN_CHAT_MEMBER = Item() # banChatMember
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
CREATE_CHAT_INVITE_LINK = Item() # createChatInviteLink
EDIT_CHAT_INVITE_LINK = Item() # editChatInviteLink
REVOKE_CHAT_INVITE_LINK = Item() # revokeChatInviteLink
SET_CHAT_PHOTO = Item() # setChatPhoto
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
SET_CHAT_TITLE = Item() # setChatTitle
SET_CHAT_DESCRIPTION = Item() # setChatDescription
PIN_CHAT_MESSAGE = Item() # pinChatMessage
UNPIN_CHAT_MESSAGE = Item() # unpinChatMessage
UNPIN_ALL_CHAT_MESSAGES = Item() # unpinAllChatMessages
LEAVE_CHAT = Item() # leaveChat
GET_CHAT = Item() # getChat
GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators
GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount
GET_CHAT_MEMBER_COUNT = Item() # getChatMemberCount
GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount (renamed to getChatMemberCount)
GET_CHAT_MEMBER = Item() # getChatMember
SET_CHAT_STICKER_SET = Item() # setChatStickerSet
DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
SET_MY_COMMANDS = Item() # setMyCommands
DELETE_MY_COMMANDS = Item() # deleteMyCommands
GET_MY_COMMANDS = Item() # getMyCommands
# Updating messages
@ -242,25 +288,3 @@ class Methods(Helper):
SEND_GAME = Item() # sendGame
SET_GAME_SCORE = Item() # setGameScore
GET_GAME_HIGH_SCORES = Item() # getGameHighScores
@staticmethod
def api_url(token, method):
"""
Generate API URL with included token and method name
:param token:
:param method:
:return:
"""
return API_URL.format(token=token, method=method)
@staticmethod
def file_url(token, path):
"""
Generate File URL with included token and file path
:param token:
:param path:
:return:
"""
return FILE_URL.format(token=token, path=path)

View file

@ -12,9 +12,11 @@ import certifi
from aiohttp.helpers import sentinel
from . import api
from .api import TelegramAPIServer, TELEGRAM_PRODUCTION
from ..types import ParseMode, base
from ..utils import json
from ..utils.auth_widget import check_integrity
from ..utils.deprecated import deprecated
class BaseBot:
@ -33,7 +35,8 @@ class BaseBot:
proxy_auth: Optional[aiohttp.BasicAuth] = None,
validate_token: Optional[base.Boolean] = True,
parse_mode: typing.Optional[base.String] = None,
timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None
timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None,
server: TelegramAPIServer = TELEGRAM_PRODUCTION
):
"""
Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot
@ -54,31 +57,29 @@ class BaseBot:
:type parse_mode: :obj:`str`
:param timeout: Request timeout
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
:param server: Telegram Bot API Server endpoint.
:type server: :obj:`TelegramAPIServer`
:raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError`
"""
self._main_loop = loop
# Authentication
if validate_token:
api.check_token(token)
self._token = None
self.__token = token
self.id = int(token.split(sep=':')[0])
self.server = server
self.proxy = proxy
self.proxy_auth = proxy_auth
# Asyncio loop instance
if loop is None:
loop = asyncio.get_event_loop()
self.loop = loop
# aiohttp main session
ssl_context = ssl.create_default_context(cafile=certifi.where())
self._session: Optional[aiohttp.ClientSession] = None
self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector
self._connector_init = dict(
limit=connections_limit, ssl=ssl_context, loop=self.loop
)
self._connector_init = dict(limit=connections_limit, ssl=ssl_context)
if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')):
from aiohttp_socks import SocksConnector
@ -106,11 +107,15 @@ class BaseBot:
def get_new_session(self) -> aiohttp.ClientSession:
return aiohttp.ClientSession(
connector=self._connector_class(**self._connector_init),
loop=self.loop,
connector=self._connector_class(**self._connector_init, loop=self._main_loop),
loop=self._main_loop,
json_serialize=json.dumps
)
@property
def loop(self) -> Optional[asyncio.AbstractEventLoop]:
return self._main_loop
@property
def session(self) -> Optional[aiohttp.ClientSession]:
if self._session is None or self._session.closed:
@ -174,6 +179,8 @@ class BaseBot:
finally:
self._ctx_token.reset(token)
@deprecated("This method's behavior will be changed in aiogram v3.0. "
"More info: https://core.telegram.org/bots/api#close", stacklevel=3)
async def close(self):
"""
Close all client sessions
@ -198,7 +205,7 @@ class BaseBot:
:rtype: Union[List, Dict]
:raise: :obj:`aiogram.exceptions.TelegramApiError`
"""
return await api.make_request(self.session, self.__token, method, data, files,
return await api.make_request(self.session, self.server, self.__token, method, data, files,
proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs)
async def download_file(self, file_path: base.String,
@ -238,7 +245,7 @@ class BaseBot:
return dest
def get_file_url(self, file_path):
return api.Methods.file_url(token=self.__token, path=file_path)
return self.server.file_url(token=self.__token, path=file_path)
async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]:
"""

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@ class MemoryStorage(BaseStorage):
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Optional[str]:
chat, user = self.resolve_address(chat=chat, user=user)
return self.data[chat][user]['state']
return self.data[chat][user].get("state", self.resolve_state(default))
async def get_data(self, *,
chat: typing.Union[str, int, None] = None,
@ -58,7 +58,7 @@ class MemoryStorage(BaseStorage):
user: typing.Union[str, int, None] = None,
state: typing.AnyStr = None):
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]['state'] = state
self.data[chat][user]['state'] = self.resolve_state(state)
async def set_data(self, *,
chat: typing.Union[str, int, None] = None,
@ -66,6 +66,7 @@ class MemoryStorage(BaseStorage):
data: typing.Dict = None):
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]['data'] = copy.deepcopy(data)
self._cleanup(chat, user)
async def reset_state(self, *,
chat: typing.Union[str, int, None] = None,
@ -74,6 +75,7 @@ class MemoryStorage(BaseStorage):
await self.set_state(chat=chat, user=user, state=None)
if with_data:
await self.set_data(chat=chat, user=user, data={})
self._cleanup(chat, user)
def has_bucket(self):
return True
@ -91,6 +93,7 @@ class MemoryStorage(BaseStorage):
bucket: typing.Dict = None):
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]['bucket'] = copy.deepcopy(bucket)
self._cleanup(chat, user)
async def update_bucket(self, *,
chat: typing.Union[str, int, None] = None,
@ -100,3 +103,10 @@ class MemoryStorage(BaseStorage):
bucket = {}
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]['bucket'].update(bucket, **kwargs)
def _cleanup(self, chat, user):
chat, user = self.resolve_address(chat=chat, user=user)
if self.data[chat][user] == {'state': None, 'data': {}, 'bucket': {}}:
del self.data[chat][user]
if not self.data[chat]:
del self.data[chat]

View file

@ -5,9 +5,9 @@ This module has mongo storage for finite-state machine
from typing import Union, Dict, Optional, List, Tuple, AnyStr
import pymongo
try:
import pymongo
import motor
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
except ModuleNotFoundError as e:
@ -26,6 +26,7 @@ COLLECTIONS = (STATE, DATA, BUCKET)
class MongoStorage(BaseStorage):
"""
Mongo-based storage for FSM.
Usage:
.. code-block:: python3
@ -39,7 +40,6 @@ class MongoStorage(BaseStorage):
await dp.storage.close()
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm', uri=None,
@ -65,7 +65,7 @@ class MongoStorage(BaseStorage):
try:
self._mongo = AsyncIOMotorClient(self._uri)
except pymongo.errors.ConfigurationError as e:
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
import logging
logger = logging.getLogger("aiogram")
logger.warning("Run `pip install dnspython==1.16.0` in order to fix ConfigurationError. More information: https://github.com/mongodb/mongo-python-driver/pull/423#issuecomment-528998245")
@ -114,7 +114,9 @@ class MongoStorage(BaseStorage):
async def wait_closed(self):
return True
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
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()
@ -122,8 +124,11 @@ class MongoStorage(BaseStorage):
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)
await db[STATE].update_one(
filter={'chat': chat, 'user': user},
update={'$set': {'state': self.resolve_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]:
@ -131,15 +136,17 @@ class MongoStorage(BaseStorage):
db = await self.get_db()
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
return result.get('state') if result else default
return result.get('state') if result else self.resolve_state(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)
if not data:
await db[DATA].delete_one(filter={'chat': chat, 'user': user})
else:
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:
@ -206,12 +213,5 @@ class MongoStorage(BaseStorage):
: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
return [(int(item['chat']), int(item['user'])) for item in items]

View file

@ -110,24 +110,29 @@ class RedisStorage(BaseStorage):
chat, user = self.check_address(chat=chat, user=user)
addr = f"fsm:{chat}:{user}"
record = {'state': state, 'data': data, 'bucket': bucket}
conn = await self.redis()
await conn.execute('SET', addr, json.dumps(record))
if state is None and data == bucket == {}:
await conn.execute('DEL', addr)
else:
record = {'state': state, 'data': data, 'bucket': bucket}
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]:
record = await self.get_record(chat=chat, user=user)
return record['state']
return record.get('state', self.resolve_state(default))
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']
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = 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)
state = self.resolve_state(state)
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,
@ -208,7 +213,7 @@ class RedisStorage2(BaseStorage):
.. code-block:: python3
storage = RedisStorage('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key')
storage = RedisStorage2('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key')
dp = Dispatcher(bot, storage=storage)
And need to close Redis connection when shutdown
@ -219,7 +224,7 @@ class RedisStorage2(BaseStorage):
await dp.storage.wait_closed()
"""
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
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,
@ -274,7 +279,7 @@ class RedisStorage2(BaseStorage):
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 self.resolve_state(default)
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:
@ -294,14 +299,17 @@ class RedisStorage2(BaseStorage):
if state is None:
await redis.delete(key)
else:
await redis.set(key, state, expire=self._state_ttl)
await redis.set(key, self.resolve_state(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), expire=self._data_ttl)
if data:
await redis.set(key, json.dumps(data), expire=self._data_ttl)
else:
await redis.delete(key)
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 +337,10 @@ 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), expire=self._bucket_ttl)
if bucket:
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
else:
await redis.delete(key)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,

View file

@ -7,7 +7,7 @@ from rethinkdb.asyncio_net.net_asyncio import Connection
from ...dispatcher.storage import BaseStorage
__all__ = ['RethinkDBStorage']
__all__ = ('RethinkDBStorage',)
r = rethinkdb.RethinkDB()
r.set_loop_type('asyncio')
@ -19,16 +19,17 @@ class RethinkDBStorage(BaseStorage):
Usage:
..code-block:: python3
.. code-block:: python3
storage = RethinkDBStorage(db='aiogram', table='aiogram', user='aiogram', password='aiogram_secret')
dispatcher = Dispatcher(bot, storage=storage)
And need to close connection when shutdown
..code-clock:: python3
.. code-block:: python3
await storage.close()
await storage.wait_closed()
"""
@ -54,7 +55,7 @@ class RethinkDBStorage(BaseStorage):
self._ssl = ssl or {}
self._loop = loop
self._conn: typing.Union[Connection, None] = None
self._conn: typing.Optional[Connection] = None
async def connect(self) -> Connection:
"""
@ -94,7 +95,9 @@ class RethinkDBStorage(BaseStorage):
default: typing.Optional[str] = None) -> typing.Optional[str]:
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
return await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn)
return await r.table(self._table).get(chat)[user]['state'].default(
self.resolve_state(default) or None
).run(conn)
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:
@ -102,11 +105,16 @@ class RethinkDBStorage(BaseStorage):
async with self.connection() as conn:
return await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn)
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = 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 = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
await r.table(self._table).insert({'id': chat, user: {'state': state}}, conflict="update").run(conn)
await r.table(self._table).insert(
{'id': chat, user: {'state': self.resolve_state(state)}},
conflict="update",
).run(conn)
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):

View file

@ -1,3 +1,5 @@
import asyncio
from aiogram.dispatcher.middlewares import BaseMiddleware
@ -14,7 +16,7 @@ class EnvironmentMiddleware(BaseMiddleware):
data.update(
bot=dp.bot,
dispatcher=dp,
loop=dp.loop
loop=dp.loop or asyncio.get_event_loop()
)
if self.context:
data.update(self.context)

View file

@ -1,7 +1,7 @@
import gettext
import os
from contextvars import ContextVar
from typing import Any, Dict, Tuple
from typing import Any, Dict, Tuple, Optional
from babel import Locale
from babel.support import LazyProxy
@ -59,13 +59,13 @@ class I18nMiddleware(BaseMiddleware):
with open(mo_path, 'rb') as fp:
translations[name] = gettext.GNUTranslations(fp)
elif os.path.exists(mo_path[:-2] + 'po'):
raise RuntimeError(f"Found locale '{name} but this language is not compiled!")
raise RuntimeError(f"Found locale '{name}' but this language is not compiled!")
return translations
def reload(self):
"""
Hot reload locles
Hot reload locales
"""
self.locales = self.find_locales()
@ -119,22 +119,24 @@ class I18nMiddleware(BaseMiddleware):
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:
async def get_user_locale(self, action: str, args: Tuple[Any]) -> Optional[str]:
"""
User locale getter
You can override the method if you want to use different way of getting user language.
You can override the method if you want to use different way of
getting user language.
:param action: event name
:param args: event arguments
:return: locale name
:return: locale name or None
"""
user: types.User = types.User.get_current()
locale: Locale = user.locale
user: Optional[types.User] = types.User.get_current()
locale: Optional[Locale] = user.locale if user else None
if locale:
if locale and locale.language in self.locales:
*_, data = args
language = data['locale'] = locale.language
return language
return self.default
async def trigger(self, action, args):
"""

View file

@ -160,6 +160,26 @@ class LoggingMiddleware(BaseMiddleware):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll answer [ID:{poll_answer.poll_id}] "
f"from user [ID:{poll_answer.user.id}]")
async def on_pre_process_my_chat_member(self, my_chat_member_update, data):
self.logger.info(f"Received chat member update "
f"for user [ID:{my_chat_member_update.from_user.id}]. "
f"Old state: {my_chat_member_update.old_chat_member.to_python()} "
f"New state: {my_chat_member_update.new_chat_member.to_python()} ")
async def on_post_process_my_chat_member(self, my_chat_member_update, results, data):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} my_chat_member "
f"for user [ID:{my_chat_member_update.from_user.id}]")
async def on_pre_process_chat_member(self, chat_member_update, data):
self.logger.info(f"Received chat member update "
f"for user [ID:{chat_member_update.from_user.id}]. "
f"Old state: {chat_member_update.old_chat_member.to_python()} "
f"New state: {chat_member_update.new_chat_member.to_python()} ")
async def on_post_process_chat_member(self, chat_member_update, results, data):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat_member "
f"for user [ID:{chat_member_update.from_user.id}]")
class LoggingFilter(logging.Filter):
"""

View file

@ -5,7 +5,7 @@ from . import storage
from . import webhook
from .dispatcher import Dispatcher, FSMContext, DEFAULT_RATE_LIMIT
__all__ = [
__all__ = (
'DEFAULT_RATE_LIMIT',
'Dispatcher',
'FSMContext',
@ -14,4 +14,4 @@ __all__ = [
'middlewares',
'storage',
'webhook'
]
)

View file

@ -11,7 +11,7 @@ from aiohttp.helpers import sentinel
from aiogram.utils.deprecated import renamed_argument
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter, \
IsSenderContact, ChatTypeFilter, AbstractFilter
IsSenderContact, ChatTypeFilter, MediaGroupFilter, AbstractFilter
from .handler import Handler
from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@ -27,6 +27,13 @@ log = logging.getLogger(__name__)
DEFAULT_RATE_LIMIT = .1
def _ensure_loop(x: "asyncio.AbstractEventLoop"):
assert isinstance(
x, asyncio.AbstractEventLoop
), f"Loop must be the implementation of {asyncio.AbstractEventLoop!r}, " \
f"not {type(x)!r}"
class Dispatcher(DataMixin, ContextInstanceMixin):
"""
Simple Updates dispatcher
@ -43,15 +50,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
if not isinstance(bot, Bot):
raise TypeError(f"Argument 'bot' must be an instance of Bot, not '{type(bot).__name__}'")
if loop is None:
loop = bot.loop
if storage is None:
storage = DisabledStorage()
if filters_factory is None:
filters_factory = FiltersFactory(self)
self.bot: Bot = bot
self.loop = loop
if loop is not None:
_ensure_loop(loop)
self._main_loop = loop
self.storage = storage
self.run_tasks_by_default = run_tasks_by_default
@ -71,6 +78,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.pre_checkout_query_handlers = Handler(self, middleware_key='pre_checkout_query')
self.poll_handlers = Handler(self, middleware_key='poll')
self.poll_answer_handlers = Handler(self, middleware_key='poll_answer')
self.my_chat_member_handlers = Handler(self, middleware_key='my_chat_member')
self.chat_member_handlers = Handler(self, middleware_key='chat_member')
self.errors_handlers = Handler(self, once=False, middleware_key='error')
self.middleware = MiddlewareManager(self)
@ -79,10 +88,27 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self._polling = False
self._closed = True
self._close_waiter = loop.create_future()
self._dispatcher_close_waiter = None
self._setup_filters()
@property
def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
# for the sake of backward compatibility
# lib internally must delegate tasks with respect to _main_loop attribute
# however should never be used by the library itself
# use more generic approaches from asyncio's namespace
return self._main_loop
@property
def _close_waiter(self) -> "asyncio.Future":
if self._dispatcher_close_waiter is None:
if self._main_loop is not None:
self._dispatcher_close_waiter = self._main_loop.create_future()
else:
self._dispatcher_close_waiter = asyncio.get_event_loop().create_future()
return self._dispatcher_close_waiter
def _setup_filters(self):
filters_factory = self.filters_factory
@ -139,6 +165,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
self.chat_member_handlers,
])
filters_factory.bind(IDFilter, event_handlers=[
self.message_handlers,
@ -147,6 +174,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.inline_query_handlers,
self.chat_member_handlers,
self.my_chat_member_handlers,
])
filters_factory.bind(IsReplyFilter, event_handlers=[
self.message_handlers,
@ -172,6 +201,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.channel_post_handlers,
self.edited_channel_post_handlers,
self.callback_query_handlers,
self.my_chat_member_handlers,
self.chat_member_handlers
])
filters_factory.bind(MediaGroupFilter, event_handlers=[
self.message_handlers,
self.edited_channel_post_handlers,
self.channel_post_handlers,
self.edited_channel_post_handlers
])
def __del__(self):
@ -195,9 +232,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
:return:
"""
if fast:
tasks = []
for update in updates:
tasks.append(self.updates_handler.notify(update))
tasks = [self.updates_handler.notify(update) for update in updates]
return await asyncio.gather(*tasks)
results = []
@ -262,6 +297,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
types.PollAnswer.set_current(update.poll_answer)
types.User.set_current(update.poll_answer.user)
return await self.poll_answer_handlers.notify(update.poll_answer)
if update.my_chat_member:
types.ChatMemberUpdated.set_current(update.my_chat_member)
types.User.set_current(update.my_chat_member.from_user)
return await self.my_chat_member_handlers.notify(update.my_chat_member)
if update.chat_member:
types.ChatMemberUpdated.set_current(update.chat_member)
types.User.set_current(update.chat_member.from_user)
return await self.chat_member_handlers.notify(update.chat_member)
except Exception as e:
err = await self.errors_handlers.notify(update, e)
if err:
@ -282,13 +325,20 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
return await self.bot.delete_webhook()
def _loop_create_task(self, coro):
if self._main_loop is None:
return asyncio.create_task(coro)
_ensure_loop(self._main_loop)
return self._main_loop.create_task(coro)
async def start_polling(self,
timeout=20,
relax=0.1,
limit=None,
reset_webhook=None,
fast: typing.Optional[bool] = True,
error_sleep: int = 5):
error_sleep: int = 5,
allowed_updates: typing.Optional[typing.List[str]] = None):
"""
Start long-polling
@ -297,6 +347,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
:param limit:
:param reset_webhook:
:param fast:
:param error_sleep:
:param allowed_updates:
:return:
"""
if self._polling:
@ -325,10 +377,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
while self._polling:
try:
with self.bot.request_timeout(request_timeout):
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
updates = await self.bot.get_updates(
limit=limit,
offset=offset,
timeout=timeout,
allowed_updates=allowed_updates
)
except asyncio.CancelledError:
break
except:
except Exception as e:
log.exception('Cause exception while getting updates.')
await asyncio.sleep(error_sleep)
continue
@ -337,7 +394,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
log.debug(f"Received {len(updates)} updates.")
offset = updates[-1].update_id + 1
self.loop.create_task(self._process_polling_updates(updates, fast))
self._loop_create_task(self._process_polling_updates(updates, fast))
if relax:
await asyncio.sleep(relax)
@ -381,7 +438,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
:return:
"""
await asyncio.shield(self._close_waiter, loop=self.loop)
await asyncio.shield(self._close_waiter)
def is_polling(self):
"""
@ -974,6 +1031,118 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
return decorator
def register_my_chat_member_handler(self,
callback: typing.Callable,
*custom_filters,
run_task: typing.Optional[bool] = None,
**kwargs) -> None:
"""
Register handler for my_chat_member
Example:
.. code-block:: python3
dp.register_my_chat_member_handler(some_my_chat_member_handler)
:param callback:
:param custom_filters:
:param run_task: run callback in task (no wait results)
:param kwargs:
"""
filters_set = self.filters_factory.resolve(
self.my_chat_member_handlers,
*custom_filters,
**kwargs,
)
self.my_chat_member_handlers.register(
handler=self._wrap_async_task(callback, run_task),
filters=filters_set,
)
def my_chat_member_handler(self, *custom_filters, run_task=None, **kwargs):
"""
Decorator for my_chat_member handler
Example:
.. code-block:: python3
@dp.my_chat_member_handler()
async def some_handler(my_chat_member: types.ChatMemberUpdated)
:param custom_filters:
:param run_task: run callback in task (no wait results)
:param kwargs:
"""
def decorator(callback):
self.register_my_chat_member_handler(
callback,
*custom_filters,
run_task=run_task,
**kwargs,
)
return callback
return decorator
def register_chat_member_handler(self,
callback: typing.Callable,
*custom_filters,
run_task: typing.Optional[bool] = None,
**kwargs) -> None:
"""
Register handler for chat_member
Example:
.. code-block:: python3
dp.register_chat_member_handler(some_chat_member_handler)
:param callback:
:param custom_filters:
:param run_task: run callback in task (no wait results)
:param kwargs:
"""
filters_set = self.filters_factory.resolve(
self.chat_member_handlers,
*custom_filters,
**kwargs,
)
self.chat_member_handlers.register(
handler=self._wrap_async_task(callback, run_task),
filters=filters_set,
)
def chat_member_handler(self, *custom_filters, run_task=None, **kwargs):
"""
Decorator for chat_member handler
Example:
.. code-block:: python3
@dp.chat_member_handler()
async def some_handler(chat_member: types.ChatMemberUpdated)
:param custom_filters:
:param run_task: run callback in task (no wait results)
:param kwargs:
"""
def decorator(callback):
self.register_chat_member_handler(
callback,
*custom_filters,
run_task=run_task,
**kwargs,
)
return callback
return decorator
def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs):
"""
Register handler for errors
@ -1053,8 +1222,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
if rate is None:
rate = self.throttling_rate_limit
if user_id is None and chat_id is None:
user_id = types.User.get_current().id
chat_id = types.Chat.get_current().id
chat_obj = types.Chat.get_current()
chat_id = chat_obj.id if chat_obj else None
user_obj = types.User.get_current()
user_id = user_obj.id if user_obj else None
# Detect current time
now = time.time()
@ -1105,8 +1277,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
raise RuntimeError('This storage does not provide Leaky Bucket')
if user_id is None and chat_id is None:
user_id = types.User.get_current()
chat_id = types.Chat.get_current()
chat_obj = types.Chat.get_current()
chat_id = chat_obj.id if chat_obj else None
user_obj = types.User.get_current()
user_id = user_obj.id if user_obj else None
bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
data = bucket.get(key, {})
@ -1127,8 +1302,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
raise RuntimeError('This storage does not provide Leaky Bucket')
if user_id is None and chat_id is None:
user_id = types.User.get_current()
chat_id = types.Chat.get_current()
chat_obj = types.Chat.get_current()
chat_id = chat_obj.id if chat_obj else None
user_obj = types.User.get_current()
user_id = user_obj.id if user_obj else None
bucket = await self.storage.get_bucket(chat=chat_id, user=user_id)
if bucket and key in bucket:
@ -1158,15 +1336,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
try:
response = task.result()
except Exception as e:
self.loop.create_task(
self._loop_create_task(
self.errors_handlers.notify(types.Update.get_current(), e))
else:
if isinstance(response, BaseResponse):
self.loop.create_task(response.execute_response(self.bot))
self._loop_create_task(response.execute_response(self.bot))
@functools.wraps(func)
async def wrapper(*args, **kwargs):
task = self.loop.create_task(func(*args, **kwargs))
task = self._loop_create_task(func(*args, **kwargs))
task.add_done_callback(process_response)
return wrapper
@ -1213,29 +1391,26 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
no_error=True)
if is_not_throttled:
return await func(*args, **kwargs)
else:
kwargs.update(
{
'rate': rate,
'key': key,
'user_id': user_id,
'chat_id': chat_id
}
) # update kwargs with parameters which were given to throttled
kwargs.update(
{
'rate': rate,
'key': key,
'user_id': user_id,
'chat_id': chat_id,
}
) # update kwargs with parameters which were given to throttled
if on_throttled:
if asyncio.iscoroutinefunction(on_throttled):
await on_throttled(*args, **kwargs)
else:
kwargs.update(
{
'loop': asyncio.get_running_loop()
}
)
partial_func = functools.partial(on_throttled, *args, **kwargs)
asyncio.get_running_loop().run_in_executor(None,
partial_func
)
if on_throttled:
if asyncio.iscoroutinefunction(on_throttled):
await on_throttled(*args, **kwargs)
else:
kwargs.update({'loop': asyncio.get_running_loop()})
partial_func = functools.partial(
on_throttled, *args, **kwargs
)
asyncio.get_running_loop().run_in_executor(
None, partial_func
)
return wrapped
return decorator

View file

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

View file

@ -4,13 +4,13 @@ import typing
import warnings
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, Optional, Union
from typing import Any, Dict, Iterable, List, Optional, Union
from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll
from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated
ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
@ -110,7 +110,8 @@ class Command(Filter):
if not text:
return False
full_command = text.split()[0]
full_command, *args_list = text.split(maxsplit=1)
args = args_list[0] if args_list else None
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():
@ -120,7 +121,7 @@ class Command(Filter):
if (command.lower() if ignore_case else command) not in commands:
return False
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention)}
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention, args=args)}
@dataclass
class CommandObj:
@ -278,9 +279,10 @@ class Text(Filter):
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))
equals, contains, endswith, startswith = map(
lambda e: [e] if isinstance(e, (str, LazyProxy)) else e,
(equals, contains, endswith, startswith),
)
self.equals = equals
self.contains = contains
self.endswith = endswith
@ -529,6 +531,8 @@ class StateFilter(BoundFilter):
self.states = states
def get_target(self, obj):
if isinstance(obj, CallbackQuery):
return getattr(getattr(getattr(obj, 'message', None),'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):
@ -604,7 +608,7 @@ class IDFilter(Filter):
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, ChatMemberUpdated]):
if isinstance(obj, Message):
user_id = None
if obj.from_user is not None:
@ -619,6 +623,9 @@ class IDFilter(Filter):
elif isinstance(obj, InlineQuery):
user_id = obj.from_user.id
chat_id = None
elif isinstance(obj, ChatMemberUpdated):
user_id = obj.from_user.id
chat_id = obj.chat.id
else:
return False
@ -663,19 +670,21 @@ class AdminFilter(Filter):
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool:
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, ChatMemberUpdated]) -> bool:
user_id = obj.from_user.id
if self._check_current:
if isinstance(obj, Message):
message = obj
chat = obj.chat
elif isinstance(obj, CallbackQuery) and obj.message:
message = obj.message
chat = obj.message.chat
elif isinstance(obj, ChatMemberUpdated):
chat = obj.chat
else:
return False
if ChatType.is_private(message): # there is no admin in private chats
if chat.type == ChatType.PRIVATE: # there is no admin in private chats
return False
chat_ids = [message.chat.id]
chat_ids = [chat.id]
else:
chat_ids = self._chat_ids
@ -719,13 +728,32 @@ class ChatTypeFilter(BoundFilter):
self.chat_type: typing.Set[str] = set(chat_type)
async def check(self, obj: Union[Message, CallbackQuery]):
async def check(self, obj: Union[Message, CallbackQuery, ChatMemberUpdated]):
if isinstance(obj, Message):
obj = obj.chat
elif isinstance(obj, CallbackQuery):
obj = obj.message.chat
elif isinstance(obj, ChatMemberUpdated):
obj = obj.chat
else:
warnings.warn("ChatTypeFilter doesn't support %s as input", type(obj))
return False
return obj.type in self.chat_type
class MediaGroupFilter(BoundFilter):
"""
Check if message is part of a media group.
`is_media_group=True` - the message is part of a media group
`is_media_group=False` - the message is NOT part of a media group
"""
key = "is_media_group"
def __init__(self, is_media_group: bool):
self.is_media_group = is_media_group
async def check(self, message: types.Message) -> bool:
return bool(getattr(message, "media_group_id")) is self.is_media_group

View file

@ -48,9 +48,13 @@ class FiltersFactory:
:param full_config:
:return:
"""
filters_set = []
filters_set.extend(self._resolve_registered(event_handler,
{k: v for k, v in full_config.items() if v is not None}))
filters_set = list(
self._resolve_registered(
event_handler,
{k: v for k, v in full_config.items() if v is not None},
)
)
if custom_filters:
filters_set.extend(custom_filters)

View file

@ -242,7 +242,7 @@ class NotFilter(_LogicFilter):
class AndFilter(_LogicFilter):
def __init__(self, *targets):
self.targets = list(wrap_async(target) for target in targets)
self.targets = [wrap_async(target) for target in targets]
async def check(self, *args):
"""
@ -268,7 +268,7 @@ class AndFilter(_LogicFilter):
class OrFilter(_LogicFilter):
def __init__(self, *targets):
self.targets = list(wrap_async(target) for target in targets)
self.targets = [wrap_async(target) for target in targets]
async def check(self, *args):
"""

View file

@ -25,15 +25,14 @@ 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
return inspect.getfullargspec(func)
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
if spec.varkw:
return kwargs
return {k: v for k, v in kwargs.items() if k in spec.args}
return {k: v for k, v in kwargs.items() if k in set(spec.args + spec.kwonlyargs)}
class Handler:

View file

@ -16,11 +16,14 @@ class MiddlewareManager:
:param dispatcher: instance of Dispatcher
"""
self.dispatcher = dispatcher
self.loop = dispatcher.loop
self.bot = dispatcher.bot
self.storage = dispatcher.storage
self.applications = []
@property
def loop(self):
return self.dispatcher.loop
def setup(self, middleware):
"""
Setup middleware

View file

@ -40,24 +40,27 @@ class BaseStorage:
@classmethod
def check_address(cls, *,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None) -> (typing.Union[str, int], typing.Union[str, int]):
user: typing.Union[str, int, None] = None,
) -> (typing.Union[str, int], typing.Union[str, int]):
"""
In all storage's methods chat or user is always required.
If one of them is not provided, you have to set missing value based on the provided one.
This method performs the check described above.
:param chat:
:param user:
:param chat: chat_id
:param user: user_id
:return:
"""
if chat is None and user is None:
raise ValueError('`user` or `chat` parameter is required but no one is provided!')
if user is None and chat is not None:
if user is None:
user = chat
elif user is not None and chat is None:
elif chat is None:
chat = user
return chat, user
async def get_state(self, *,
@ -270,6 +273,21 @@ class BaseStorage:
"""
await self.set_data(chat=chat, user=user, data={})
@staticmethod
def resolve_state(value):
from .filters.state import State
if value is None:
return
if isinstance(value, str):
return value
if isinstance(value, State):
return value.state
return str(value)
class FSMContext:
def __init__(self, storage, chat, user):
@ -279,20 +297,8 @@ class FSMContext:
def proxy(self):
return FSMContextProxy(self)
@staticmethod
def _resolve_state(value):
from .filters.state import State
if value is None:
return
elif isinstance(value, str):
return value
elif isinstance(value, State):
return value.state
return str(value)
async def get_state(self, default: typing.Optional[str] = None) -> typing.Optional[str]:
return await self.storage.get_state(chat=self.chat, user=self.user, default=self._resolve_state(default))
return await self.storage.get_state(chat=self.chat, user=self.user, default=default)
async def get_data(self, default: typing.Optional[str] = None) -> typing.Dict:
return await self.storage.get_data(chat=self.chat, user=self.user, default=default)
@ -300,8 +306,8 @@ class FSMContext:
async def update_data(self, data: typing.Dict = None, **kwargs):
await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs)
async def set_state(self, state: typing.Union[typing.AnyStr, None] = None):
await self.storage.set_state(chat=self.chat, user=self.user, state=self._resolve_state(state))
async def set_state(self, state: typing.Optional[typing.AnyStr] = None):
await self.storage.set_state(chat=self.chat, user=self.user, state=state)
async def set_data(self, data: typing.Dict = None):
await self.storage.set_data(chat=self.chat, user=self.user, data=data)
@ -397,7 +403,7 @@ class FSMContextProxy:
def setdefault(self, key, default):
self._check_closed()
self._data.setdefault(key, default)
return self._data.setdefault(key, default)
def update(self, data=None, **kwargs):
self._check_closed()

View file

@ -116,8 +116,7 @@ class WebhookRequestHandler(web.View):
:return: :class:`aiogram.types.Update`
"""
data = await self.request.json()
update = types.Update(**data)
return update
return types.Update(**data)
async def post(self):
"""
@ -169,7 +168,7 @@ class WebhookRequestHandler(web.View):
:return:
"""
dispatcher = self.get_dispatcher()
loop = dispatcher.loop
loop = dispatcher.loop or asyncio.get_event_loop()
# Analog of `asyncio.wait_for` but without cancelling task
waiter = loop.create_future()
@ -189,10 +188,9 @@ class WebhookRequestHandler(web.View):
if fut.done():
return fut.result()
else:
# context.set_value(WEBHOOK_CONNECTION, False)
fut.remove_done_callback(cb)
fut.add_done_callback(self.respond_via_request)
# context.set_value(WEBHOOK_CONNECTION, False)
fut.remove_done_callback(cb)
fut.add_done_callback(self.respond_via_request)
finally:
timeout_handle.cancel()
@ -209,7 +207,7 @@ class WebhookRequestHandler(web.View):
TimeoutWarning)
dispatcher = self.get_dispatcher()
loop = dispatcher.loop
loop = dispatcher.loop or asyncio.get_event_loop()
try:
results = task.result()
@ -620,7 +618,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for
Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data.
:param caption: String (Optional) - Photo caption (may also be used when resending photos by file_id),
0-200 characters
0-1024 characters after entities parsing
:param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive
a notification with no sound.
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
@ -673,7 +671,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL
as a String for Telegram to get an audio file from the Internet, or upload a new one
using multipart/form-data.
:param caption: String (Optional) - Audio caption, 0-200 characters
:param caption: String (Optional) - Audio caption, 0-1024 characters after entities parsing
:param duration: Integer (Optional) - Duration of the audio in seconds
:param performer: String (Optional) - Performer
:param title: String (Optional) - Track name
@ -732,7 +730,7 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin):
as a String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
:param caption: String (Optional) - Document caption
(may also be used when resending documents by file_id), 0-200 characters
(may also be used when resending documents by file_id), 0-1024 characters after entities parsing
:param disable_notification: Boolean (Optional) - Sends the message silently.
Users will receive a notification with no sound.
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
@ -789,7 +787,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
:param width: Integer (Optional) - Video width
:param height: Integer (Optional) - Video height
:param caption: String (Optional) - Video caption (may also be used when resending videos by file_id),
0-200 characters
0-1024 characters after entities parsing
:param disable_notification: Boolean (Optional) - Sends the message silently.
Users will receive a notification with no sound.
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
@ -846,7 +844,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
to send a file that exists on the Telegram servers (recommended), pass an HTTP URL
as a String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
:param caption: String (Optional) - Voice message caption, 0-200 characters
:param caption: String (Optional) - Voice message caption, 0-1024 characters after entities parsing
:param duration: Integer (Optional) - Duration of the voice message in seconds
:param disable_notification: Boolean (Optional) - Sends the message silently.
Users will receive a notification with no sound.
@ -939,8 +937,8 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin):
def __init__(self, chat_id: Union[Integer, String],
media: Union[types.MediaGroup, List] = None,
disable_notification: typing.Union[Boolean, None] = None,
reply_to_message_id: typing.Union[Integer, None] = None):
disable_notification: typing.Optional[Boolean] = None,
reply_to_message_id: typing.Optional[Integer] = None):
"""
Use this method to send a group of photos or videos as an album.
@ -951,9 +949,9 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin):
:param media: A JSON-serialized array describing photos and videos to be sent
:type media: :obj:`typing.Union[types.MediaGroup, typing.List]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
:param reply_to_message_id: If the message is a reply, ID of the original message
:type reply_to_message_id: :obj:`typing.Union[base.Integer, None]`
:type reply_to_message_id: :obj:`typing.Optional[base.Integer]`
:return: On success, an array of the sent Messages is returned.
:rtype: typing.List[types.Message]
"""

View file

@ -4,10 +4,19 @@ from .animation import Animation
from .audio import Audio
from .auth_widget_data import AuthWidgetData
from .bot_command import BotCommand
from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministrators, \
BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, BotCommandScopeChat, \
BotCommandScopeChatAdministrators, BotCommandScopeChatMember, \
BotCommandScopeDefault, BotCommandScopeType
from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
from .chat_member import ChatMember, ChatMemberStatus
from .chat_invite_link import ChatInviteLink
from .chat_location import ChatLocation
from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberBanned, \
ChatMemberLeft, ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, \
ChatMemberStatus
from .chat_member_updated import ChatMemberUpdated
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
@ -32,14 +41,16 @@ from .input_file import InputFile
from .input_media import InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, \
InputMediaVideo, MediaGroup
from .input_message_content import InputContactMessageContent, InputLocationMessageContent, InputMessageContent, \
InputTextMessageContent, InputVenueMessageContent
InputTextMessageContent, InputVenueMessageContent, InputInvoiceMessageContent
from .invoice import Invoice
from .labeled_price import LabeledPrice
from .location import Location
from .login_url import LoginUrl
from .mask_position import MaskPosition
from .message import ContentType, ContentTypes, Message, ParseMode
from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged
from .message_entity import MessageEntity, MessageEntityType
from .message_id import MessageId
from .order_info import OrderInfo
from .passport_data import PassportData
from .passport_element_error import PassportElementError, PassportElementErrorDataField, PassportElementErrorFile, \
@ -49,6 +60,7 @@ from .passport_file import PassportFile
from .photo_size import PhotoSize
from .poll import PollOption, Poll, PollAnswer, PollType
from .pre_checkout_query import PreCheckoutQuery
from .proximity_alert_triggered import ProximityAlertTriggered
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType
from .response_parameters import ResponseParameters
from .shipping_address import ShippingAddress
@ -64,6 +76,10 @@ from .venue import Venue
from .video import Video
from .video_note import VideoNote
from .voice import Voice
from .voice_chat_ended import VoiceChatEnded
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
from .voice_chat_scheduled import VoiceChatScheduled
from .voice_chat_started import VoiceChatStarted
from .webhook_info import WebhookInfo
__all__ = (
@ -72,12 +88,31 @@ __all__ = (
'Audio',
'AuthWidgetData',
'BotCommand',
'BotCommandScope',
'BotCommandScopeAllChatAdministrators',
'BotCommandScopeAllGroupChats',
'BotCommandScopeAllPrivateChats',
'BotCommandScopeChat',
'BotCommandScopeChatAdministrators',
'BotCommandScopeChatMember',
'BotCommandScopeDefault',
'BotCommandScopeType',
'CallbackGame',
'CallbackQuery',
'Chat',
'ChatActions',
'ChatInviteLink',
'ChatLocation',
'ChatMember',
'ChatMemberStatus',
'ChatMemberUpdated',
'ChatMemberOwner',
'ChatMemberAdministrator',
'ChatMemberMember',
'ChatMemberRestricted',
'ChatMemberLeft',
'ChatMemberBanned',
'ChatPermissions',
'ChatPhoto',
'ChatType',
'ChosenInlineResult',
@ -118,6 +153,7 @@ __all__ = (
'InlineQueryResultVideo',
'InlineQueryResultVoice',
'InputContactMessageContent',
'InputInvoiceMessageContent',
'InputFile',
'InputLocationMessageContent',
'InputMedia',
@ -138,8 +174,10 @@ __all__ = (
'MaskPosition',
'MediaGroup',
'Message',
'MessageAutoDeleteTimerChanged',
'MessageEntity',
'MessageEntityType',
'MessageId',
'OrderInfo',
'ParseMode',
'PassportData',
@ -157,6 +195,7 @@ __all__ = (
'PollOption',
'PollType',
'PreCheckoutQuery',
'ProximityAlertTriggered',
'ReplyKeyboardMarkup',
'ReplyKeyboardRemove',
'ResponseParameters',
@ -173,6 +212,10 @@ __all__ = (
'Video',
'VideoNote',
'Voice',
'VoiceChatEnded',
'VoiceChatParticipantsInvited',
'VoiceChatScheduled',
'VoiceChatStarted',
'WebhookInfo',
'base',
'fields',

View file

@ -15,6 +15,9 @@ class Animation(base.TelegramObject, mixins.Downloadable):
file_id: base.String = fields.Field()
file_unique_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()

View file

@ -15,6 +15,7 @@ class Audio(base.TelegramObject, mixins.Downloadable):
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import io
import logging
import typing
from typing import TypeVar
@ -26,6 +27,9 @@ Float = TypeVar('Float', bound=float)
Boolean = TypeVar('Boolean', bound=bool)
T = TypeVar('T')
# Main aiogram logger
log = logging.getLogger('aiogram')
class MetaTelegramObject(type):
"""
@ -135,14 +139,18 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return type(self).telegram_types
@classmethod
def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T:
def to_object(cls: typing.Type[T],
data: typing.Dict[str, typing.Any],
conf: typing.Dict[str, typing.Any] = None
) -> T:
"""
Deserialize object
:param data:
:param conf:
:return:
"""
return cls(**data)
return cls(conf=conf, **data)
@property
def bot(self) -> Bot:
@ -212,7 +220,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
if item in self.props:
return self.props[item].get_value(self)
raise KeyError(item)
return self.values[item]
def __setitem__(self, key: str, value: typing.Any) -> None:
"""
@ -224,7 +232,10 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
if key in self.props:
return self.props[key].set_value(self, value, self.conf.get('parent', None))
raise KeyError(key)
self.values[key] = value
# Log warning when Telegram silently adds new Fields
log.warning("Field '%s' doesn't exist in %s", key, self.__class__)
def __contains__(self, item: str) -> bool:
"""
@ -242,8 +253,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
:return:
"""
for item in self.to_python().items():
yield item
yield from self.to_python().items()
def iter_keys(self) -> typing.Generator[typing.Any, None, None]:
"""

View file

@ -0,0 +1,121 @@
import typing
from . import base, fields
from ..utils import helper
class BotCommandScopeType(helper.Helper):
mode = helper.HelperMode.snake_case
DEFAULT = helper.Item() # default
ALL_PRIVATE_CHATS = helper.Item() # all_private_chats
ALL_GROUP_CHATS = helper.Item() # all_group_chats
ALL_CHAT_ADMINISTRATORS = helper.Item() # all_chat_administrators
CHAT = helper.Item() # chat
CHAT_ADMINISTRATORS = helper.Item() # chat_administrators
CHAT_MEMBER = helper.Item() # chat_member
class BotCommandScope(base.TelegramObject):
"""
This object represents the scope to which bot commands are applied.
Currently, the following 7 scopes are supported:
BotCommandScopeDefault
BotCommandScopeAllPrivateChats
BotCommandScopeAllGroupChats
BotCommandScopeAllChatAdministrators
BotCommandScopeChat
BotCommandScopeChatAdministrators
BotCommandScopeChatMember
https://core.telegram.org/bots/api#botcommandscope
"""
type: base.String = fields.Field()
@classmethod
def from_type(cls, type: str, **kwargs: typing.Any):
if type == BotCommandScopeType.DEFAULT:
return BotCommandScopeDefault(type=type, **kwargs)
if type == BotCommandScopeType.ALL_PRIVATE_CHATS:
return BotCommandScopeAllPrivateChats(type=type, **kwargs)
if type == BotCommandScopeType.ALL_GROUP_CHATS:
return BotCommandScopeAllGroupChats(type=type, **kwargs)
if type == BotCommandScopeType.ALL_CHAT_ADMINISTRATORS:
return BotCommandScopeAllChatAdministrators(type=type, **kwargs)
if type == BotCommandScopeType.CHAT:
return BotCommandScopeChat(type=type, **kwargs)
if type == BotCommandScopeType.CHAT_ADMINISTRATORS:
return BotCommandScopeChatAdministrators(type=type, **kwargs)
if type == BotCommandScopeType.CHAT_MEMBER:
return BotCommandScopeChatMember(type=type, **kwargs)
raise ValueError(f"Unknown BotCommandScope type {type!r}")
class BotCommandScopeDefault(BotCommandScope):
"""
Represents the default scope of bot commands.
Default commands are used if no commands with a narrower scope are
specified for the user.
"""
type = fields.Field(default=BotCommandScopeType.DEFAULT)
class BotCommandScopeAllPrivateChats(BotCommandScope):
"""
Represents the scope of bot commands, covering all private chats.
"""
type = fields.Field(default=BotCommandScopeType.ALL_PRIVATE_CHATS)
class BotCommandScopeAllGroupChats(BotCommandScope):
"""
Represents the scope of bot commands, covering all group and
supergroup chats.
"""
type = fields.Field(default=BotCommandScopeType.ALL_GROUP_CHATS)
class BotCommandScopeAllChatAdministrators(BotCommandScope):
"""
Represents the scope of bot commands, covering all group and
supergroup chat administrators.
"""
type = fields.Field(default=BotCommandScopeType.ALL_CHAT_ADMINISTRATORS)
class BotCommandScopeChat(BotCommandScope):
"""
Represents the scope of bot commands, covering a specific chat.
"""
type = fields.Field(default=BotCommandScopeType.CHAT)
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
def __init__(self, chat_id: typing.Union[base.String, base.Integer], **kwargs):
super().__init__(chat_id=chat_id, **kwargs)
class BotCommandScopeChatAdministrators(BotCommandScopeChat):
"""
Represents the scope of bot commands, covering all administrators
of a specific group or supergroup chat.
"""
type = fields.Field(default=BotCommandScopeType.CHAT_ADMINISTRATORS)
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
class BotCommandScopeChatMember(BotCommandScopeChat):
"""
Represents the scope of bot commands, covering a specific member of
a group or supergroup chat.
"""
type = fields.Field(default=BotCommandScopeType.CHAT_MEMBER)
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
user_id: base.Integer = fields.Field()
def __init__(
self,
chat_id: typing.Union[base.String, base.Integer],
user_id: base.Integer,
**kwargs,
):
super().__init__(chat_id=chat_id, user_id=user_id, **kwargs)

View file

@ -28,10 +28,10 @@ class CallbackQuery(base.TelegramObject):
data: base.String = fields.Field()
game_short_name: base.String = fields.Field()
async def answer(self, text: typing.Union[base.String, None] = None,
show_alert: typing.Union[base.Boolean, None] = None,
url: typing.Union[base.String, None] = None,
cache_time: typing.Union[base.Integer, None] = None):
async def answer(self, text: typing.Optional[base.String] = None,
show_alert: typing.Optional[base.Boolean] = None,
url: typing.Optional[base.String] = None,
cache_time: typing.Optional[base.Integer] = None):
"""
Use this method to send answers to callback queries sent from inline keyboards.
The answer will be displayed to the user as a notification at the top of the chat screen or as an alert.
@ -43,19 +43,22 @@ class CallbackQuery(base.TelegramObject):
Source: https://core.telegram.org/bots/api#answercallbackquery
:param text: Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters
:type text: :obj:`typing.Union[base.String, None]`
:type text: :obj:`typing.Optional[base.String]`
:param show_alert: If true, an alert will be shown by the client instead of a notification
at the top of the chat screen. Defaults to false.
:type show_alert: :obj:`typing.Union[base.Boolean, None]`
:type show_alert: :obj:`typing.Optional[base.Boolean]`
:param url: URL that will be opened by the user's client.
:type url: :obj:`typing.Union[base.String, None]`
:type url: :obj:`typing.Optional[base.String]`
:param cache_time: The maximum amount of time in seconds that the
result of the callback query may be cached client-side.
:type cache_time: :obj:`typing.Union[base.Integer, None]`
:type cache_time: :obj:`typing.Optional[base.Integer]`
:return: On success, True is returned.
:rtype: :obj:`base.Boolean`"""
await self.bot.answer_callback_query(callback_query_id=self.id, text=text,
show_alert=show_alert, url=url, cache_time=cache_time)
return await self.bot.answer_callback_query(callback_query_id=self.id,
text=text,
show_alert=show_alert,
url=url,
cache_time=cache_time)
def __hash__(self):
return hash(self.id)

View file

@ -4,13 +4,15 @@ import asyncio
import datetime
import typing
from ..utils import helper, markdown
from . import base, fields
from .chat_invite_link import ChatInviteLink
from .chat_location import ChatLocation
from .chat_member import ChatMember
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .input_file import InputFile
from ..utils.deprecated import deprecated
from ..utils import helper, markdown
from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar
class Chat(base.TelegramObject):
@ -27,13 +29,17 @@ class Chat(base.TelegramObject):
last_name: base.String = fields.Field()
all_members_are_administrators: base.Boolean = fields.Field()
photo: ChatPhoto = fields.Field(base=ChatPhoto)
bio: base.String = fields.Field()
description: base.String = fields.Field()
invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
slow_mode_delay: base.Integer = fields.Field()
message_auto_delete_time: base.Integer = fields.Field()
sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field()
linked_chat_id: base.Integer = fields.Field()
location: ChatLocation = fields.Field()
def __hash__(self):
return self.id
@ -48,7 +54,7 @@ class Chat(base.TelegramObject):
return self.title
@property
def mention(self) -> typing.Union[base.String, None]:
def mention(self) -> typing.Optional[base.String]:
"""
Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned
"""
@ -109,7 +115,7 @@ class Chat(base.TelegramObject):
async def update_chat(self):
"""
User this method to update Chat data
Use this method to update Chat data
:return: None
"""
@ -175,59 +181,91 @@ class Chat(base.TelegramObject):
Source: https://core.telegram.org/bots/api#setchatdescription
:param description: New chat description, 0-255 characters
:type description: :obj:`typing.Union[base.String, None]`
:type description: :obj:`typing.Optional[base.String]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.set_chat_description(self.id, description)
async def kick(self, user_id: base.Integer,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean:
async def kick(self,
user_id: base.Integer,
until_date: typing.Union[base.Integer, datetime.datetime,
datetime.timedelta, None] = None,
revoke_messages: typing.Optional[base.Boolean] = None,
) -> base.Boolean:
"""
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
on their own using invite links, etc., unless unbanned first.
In the case of supergroups and channels, the user will not be able to return
to the chat on their own using invite links, etc., unless unbanned first.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
Note: In regular groups (non-supergroups), this method will only work if the All Members Are Admins setting
is off in the target group.
Otherwise members may only be removed by the group's creator or by the member that added them.
The bot must be an administrator in the chat for this to work and must have
the appropriate admin rights.
Source: https://core.telegram.org/bots/api#kickchatmember
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
:param until_date: Date when the user will be unbanned, unix time.
:type until_date: :obj:`typing.Union[base.Integer, None]`
:return: Returns True on success.
:param until_date: Date when the user will be unbanned. If user is banned
for more than 366 days or less than 30 seconds from the current time they
are considered to be banned forever. Applied for supergroups and channels
only.
:type until_date: :obj:`typing.Union[base.Integer, datetime.datetime,
datetime.timedelta, None]`
:param revoke_messages: Pass True to delete all messages from the chat for
the user that is being removed. If False, the user will be able to see
messages in the group that were sent before the user was removed. Always
True for supergroups and channels.
:type revoke_messages: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date)
return await self.bot.kick_chat_member(
chat_id=self.id,
user_id=user_id,
until_date=until_date,
revoke_messages=revoke_messages,
)
async def unban(self, user_id: base.Integer) -> base.Boolean:
async def unban(self,
user_id: base.Integer,
only_if_banned: typing.Optional[base.Boolean] = None,
) -> base.Boolean:
"""
Use this method to unban a previously kicked user in a supergroup or channel. `
The user will not return to the group or channel automatically, but will be able to join via link, etc.
The bot must be an administrator for this to work.
Use this method to unban a previously kicked user in a supergroup or channel.
The user will not return to the group or channel automatically, but will be
able to join via link, etc. The bot must be an administrator for this to
work. By default, this method guarantees that after the call the user is not
a member of the chat, but will be able to join it. So if the user is a member
of the chat they will also be removed from the chat. If you don't want this,
use the parameter only_if_banned. Returns True on success.
Source: https://core.telegram.org/bots/api#unbanchatmember
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
:param only_if_banned: Do nothing if the user is not banned
:type only_if_banned: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.unban_chat_member(self.id, user_id=user_id)
return await self.bot.unban_chat_member(
chat_id=self.id,
user_id=user_id,
only_if_banned=only_if_banned,
)
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:
can_send_messages: typing.Optional[base.Boolean] = None,
can_send_media_messages: typing.Optional[base.Boolean] = None,
can_send_other_messages: typing.Optional[base.Boolean] = None,
can_add_web_page_previews: typing.Optional[base.Boolean] = 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.
@ -240,18 +278,18 @@ class Chat(base.TelegramObject):
: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]`
:type until_date: :obj:`typing.Optional[base.Integer]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
:type can_send_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_send_messages: :obj:`typing.Optional[base.Boolean]`
:param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos,
video notes and voice notes, implies can_send_messages
:type can_send_media_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_send_media_messages: :obj:`typing.Optional[base.Boolean]`
:param can_send_other_messages: Pass True, if the user can send animations, games, stickers and
use inline bots, implies can_send_media_messages
:type can_send_other_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_send_other_messages: :obj:`typing.Optional[base.Boolean]`
:param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages,
implies can_send_media_messages
:type can_add_web_page_previews: :obj:`typing.Union[base.Boolean, None]`
:type can_add_web_page_previews: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
@ -263,15 +301,17 @@ class Chat(base.TelegramObject):
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,
is_anonymous: typing.Optional[base.Boolean] = None,
can_change_info: typing.Optional[base.Boolean] = None,
can_post_messages: typing.Optional[base.Boolean] = None,
can_edit_messages: typing.Optional[base.Boolean] = None,
can_delete_messages: typing.Optional[base.Boolean] = None,
can_invite_users: typing.Optional[base.Boolean] = None,
can_restrict_members: typing.Optional[base.Boolean] = None,
can_pin_messages: typing.Optional[base.Boolean] = None,
can_promote_members: typing.Optional[base.Boolean] = 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.
@ -281,29 +321,42 @@ class Chat(base.TelegramObject):
:param user_id: Unique identifier of the target user
:type user_id: :obj:`base.Integer`
:param is_anonymous: Pass True, if the administrator's presence in the chat is hidden
:type is_anonymous: :obj:`typing.Optional[base.Boolean]`
:param can_change_info: Pass True, if the administrator can change chat title, photo and other settings
:type can_change_info: :obj:`typing.Union[base.Boolean, None]`
:type can_change_info: :obj:`typing.Optional[base.Boolean]`
:param can_post_messages: Pass True, if the administrator can create channel posts, channels only
:type can_post_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_post_messages: :obj:`typing.Optional[base.Boolean]`
:param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only
:type can_edit_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_edit_messages: :obj:`typing.Optional[base.Boolean]`
:param can_delete_messages: Pass True, if the administrator can delete messages of other users
:type can_delete_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_delete_messages: :obj:`typing.Optional[base.Boolean]`
:param can_invite_users: Pass True, if the administrator can invite new users to the chat
:type can_invite_users: :obj:`typing.Union[base.Boolean, None]`
:type can_invite_users: :obj:`typing.Optional[base.Boolean]`
:param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members
:type can_restrict_members: :obj:`typing.Union[base.Boolean, None]`
:type can_restrict_members: :obj:`typing.Optional[base.Boolean]`
:param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only
:type can_pin_messages: :obj:`typing.Union[base.Boolean, None]`
:type can_pin_messages: :obj:`typing.Optional[base.Boolean]`
:param can_promote_members: Pass True, if the administrator can add new administrators
with a subset of his own privileges or demote administrators that he has promoted,
directly or indirectly (promoted by administrators that were appointed by him)
:type can_promote_members: :obj:`typing.Union[base.Boolean, None]`
:type can_promote_members: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.promote_chat_member(self.id,
user_id=user_id,
is_anonymous=is_anonymous,
can_change_info=can_change_info,
can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages,
@ -338,36 +391,73 @@ class Chat(base.TelegramObject):
:param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed
:return: True on success.
"""
return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id, custom_title=custom_title)
return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id,
custom_title=custom_title)
async def pin_message(self, message_id: base.Integer, disable_notification: base.Boolean = False) -> base.Boolean:
async def pin_message(self,
message_id: base.Integer,
disable_notification: typing.Optional[base.Boolean] = False,
) -> base.Boolean:
"""
Use this method to pin a message in a supergroup.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
Use this method to add a message to the list of pinned messages in a chat.
If the chat is not a private chat, the bot must be an administrator in the
chat for this to work and must have the 'can_pin_messages' admin right in a
supergroup or 'can_edit_messages' admin right in a channel. Returns True on
success.
Source: https://core.telegram.org/bots/api#pinchatmessage
:param message_id: Identifier of a message to pin
:type message_id: :obj:`base.Integer`
:param disable_notification: Pass True, if it is not necessary to send a notification to
all group members about the new pinned message
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
:return: Returns True on success.
:param disable_notification: Pass True, if it is not necessary to send a
notification to all group members about the new pinned message
:type disable_notification: :obj:`typing.Optional[base.Boolean]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
return await self.bot.pin_chat_message(self.id, message_id, disable_notification)
async def unpin_message(self) -> base.Boolean:
async def unpin_message(self,
message_id: typing.Optional[base.Integer] = None,
) -> base.Boolean:
"""
Use this method to unpin a message in a supergroup chat.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
Use this method to remove a message from the list of pinned messages in a
chat. If the chat is not a private chat, the bot must be an administrator in
the chat for this to work and must have the 'can_pin_messages' admin right in
a supergroup or 'can_edit_messages' admin right in a channel. Returns True on
success.
Source: https://core.telegram.org/bots/api#unpinchatmessage
:return: Returns True on success.
:param message_id: Identifier of a message to unpin. If not specified, the
most recent pinned message (by sending date) will be unpinned.
:type message_id: :obj:`typing.Optional[base.Integer]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
return await self.bot.unpin_chat_message(self.id)
return await self.bot.unpin_chat_message(
chat_id=self.id,
message_id=message_id,
)
async def unpin_all_messages(self):
"""
Use this method to clear the list of pinned messages in a chat. If the chat
is not a private chat, the bot must be an administrator in the chat for this
to work and must have the 'can_pin_messages' admin right in a supergroup or
'can_edit_messages' admin right in a channel. Returns True on success.
Source: https://core.telegram.org/bots/api#unpinallchatmessages
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
return await self.bot.unpin_all_chat_messages(
chat_id=self.id,
)
async def leave(self) -> base.Boolean:
"""
@ -394,16 +484,20 @@ class Chat(base.TelegramObject):
"""
return await self.bot.get_chat_administrators(self.id)
async def get_members_count(self) -> base.Integer:
async def get_member_count(self) -> base.Integer:
"""
Use this method to get the number of members in a chat.
Source: https://core.telegram.org/bots/api#getchatmemberscount
Source: https://core.telegram.org/bots/api#getchatmembercount
:return: Returns Int on success.
:rtype: :obj:`base.Integer`
"""
return await self.bot.get_chat_members_count(self.id)
return await self.bot.get_chat_member_count(self.id)
async def get_members_count(self) -> base.Integer:
"""Renamed to get_member_count."""
return await self.get_member_count(self.id)
async def get_member(self, user_id: base.Integer) -> ChatMember:
"""
@ -483,6 +577,50 @@ class Chat(base.TelegramObject):
return self.invite_link
async def create_invite_link(self,
expire_date: typing.Union[base.Integer, datetime.datetime,
datetime.timedelta, None] = None,
member_limit: typing.Optional[base.Integer] = None,
) -> ChatInviteLink:
""" Shortcut for createChatInviteLink method. """
return await self.bot.create_chat_invite_link(
chat_id=self.id,
expire_date=expire_date,
member_limit=member_limit,
)
async def edit_invite_link(self,
invite_link: base.String,
expire_date: typing.Union[base.Integer, datetime.datetime,
datetime.timedelta, None] = None,
member_limit: typing.Optional[base.Integer] = None,
) -> ChatInviteLink:
""" Shortcut for editChatInviteLink method. """
return await self.bot.edit_chat_invite_link(
chat_id=self.id,
invite_link=invite_link,
expire_date=expire_date,
member_limit=member_limit,
)
async def revoke_invite_link(self,
invite_link: base.String,
) -> ChatInviteLink:
""" Shortcut for revokeChatInviteLink method. """
return await self.bot.revoke_chat_invite_link(
chat_id=self.id,
invite_link=invite_link,
)
async def delete_message(self,
message_id: base.Integer,
) -> base.Boolean:
""" Shortcut for deleteMessage method. """
return await self.bot.delete_message(
chat_id=self.id,
message_id=message_id,
)
def __int__(self):
return self.id
@ -494,6 +632,7 @@ class ChatType(helper.Helper):
:key: PRIVATE
:key: GROUP
:key: SUPER_GROUP
:key: SUPERGROUP
:key: CHANNEL
"""
@ -501,9 +640,14 @@ class ChatType(helper.Helper):
PRIVATE = helper.Item() # private
GROUP = helper.Item() # group
SUPER_GROUP = helper.Item() # supergroup
SUPERGROUP = helper.Item() # supergroup
CHANNEL = helper.Item() # channel
SUPER_GROUP: DeprecatedReadOnlyClassVar[ChatType, helper.Item] \
= DeprecatedReadOnlyClassVar(
"SUPER_GROUP chat type is deprecated, use SUPERGROUP instead.",
new_value_getter=lambda cls: cls.SUPERGROUP)
@staticmethod
def _check(obj, chat_types) -> bool:
if hasattr(obj, 'chat'):
@ -543,7 +687,7 @@ class ChatType(helper.Helper):
:param obj:
:return:
"""
return cls._check(obj, [cls.SUPER_GROUP])
return cls._check(obj, [cls.SUPER_GROUP, cls.SUPERGROUP])
@classmethod
@deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0")
@ -554,7 +698,7 @@ class ChatType(helper.Helper):
:param obj:
:return:
"""
return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP])
return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP, cls.SUPERGROUP])
@classmethod
@deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0")
@ -592,6 +736,8 @@ class ChatActions(helper.Helper):
UPLOAD_VIDEO: str = helper.Item() # upload_video
RECORD_AUDIO: str = helper.Item() # record_audio
UPLOAD_AUDIO: str = helper.Item() # upload_audio
RECORD_VOICE: str = helper.Item() # record_voice
UPLOAD_VOICE: str = helper.Item() # upload_voice
UPLOAD_DOCUMENT: str = helper.Item() # upload_document
FIND_LOCATION: str = helper.Item() # find_location
RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note
@ -677,6 +823,26 @@ class ChatActions(helper.Helper):
"""
await cls._do(cls.UPLOAD_AUDIO, sleep)
@classmethod
async def record_voice(cls, sleep=None):
"""
Do record voice
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.RECORD_VOICE, sleep)
@classmethod
async def upload_voice(cls, sleep=None):
"""
Do upload voice
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_VOICE, sleep)
@classmethod
async def upload_document(cls, sleep=None):
"""

View file

@ -0,0 +1,20 @@
from datetime import datetime
from . import base
from . import fields
from .user import User
class ChatInviteLink(base.TelegramObject):
"""
Represents an invite link for a chat.
https://core.telegram.org/bots/api#chatinvitelink
"""
invite_link: base.String = fields.Field()
creator: User = fields.Field(base=User)
is_primary: base.Boolean = fields.Field()
is_revoked: base.Boolean = fields.Field()
expire_date: datetime = fields.DateTimeField()
member_limit: base.Integer = fields.Field()

View file

@ -0,0 +1,16 @@
from . import base
from . import fields
from .location import Location
class ChatLocation(base.TelegramObject):
"""
Represents a location to which a chat is connected.
https://core.telegram.org/bots/api#chatlocation
"""
location: Location = fields.Field()
address: base.String = fields.Field()
def __init__(self, location: Location, address: base.String):
super().__init__(location=location, address=address)

View file

@ -1,47 +1,13 @@
import datetime
import warnings
import typing
from typing import Optional
from . import base
from . import fields
from . import base, fields
from .user import User
from ..utils import helper
class ChatMember(base.TelegramObject):
"""
This object contains information about one member of a chat.
https://core.telegram.org/bots/api#chatmember
"""
user: User = fields.Field(base=User)
status: base.String = fields.Field()
custom_title: base.String = fields.Field()
until_date: datetime.datetime = fields.DateTimeField()
can_be_edited: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
can_post_messages: base.Boolean = fields.Field()
can_edit_messages: base.Boolean = fields.Field()
can_delete_messages: base.Boolean = fields.Field()
can_invite_users: base.Boolean = fields.Field()
can_restrict_members: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()
can_promote_members: base.Boolean = fields.Field()
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_chat_admin(self) -> bool:
return ChatMemberStatus.is_chat_admin(self.status)
def is_chat_member(self) -> bool:
return ChatMemberStatus.is_chat_member(self.status)
def __int__(self) -> int:
return self.user.id
T = typing.TypeVar('T')
class ChatMemberStatus(helper.Helper):
@ -51,16 +17,176 @@ class ChatMemberStatus(helper.Helper):
mode = helper.HelperMode.lowercase
CREATOR = helper.Item() # creator
OWNER = CREATOR # creator
ADMINISTRATOR = helper.Item() # administrator
MEMBER = helper.Item() # member
RESTRICTED = helper.Item() # restricted
LEFT = helper.Item() # left
KICKED = helper.Item() # kicked
BANNED = KICKED # kicked
@classmethod
def is_chat_creator(cls, role: str) -> bool:
return role == cls.CREATOR
@classmethod
def is_chat_admin(cls, role: str) -> bool:
return role in [cls.ADMINISTRATOR, cls.CREATOR]
return role in (cls.ADMINISTRATOR, cls.CREATOR)
@classmethod
def is_chat_member(cls, role: str) -> bool:
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED]
return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED)
@classmethod
def get_class_by_status(cls, status: str) -> Optional["ChatMember"]:
return {
cls.OWNER: ChatMemberOwner,
cls.ADMINISTRATOR: ChatMemberAdministrator,
cls.MEMBER: ChatMemberMember,
cls.RESTRICTED: ChatMemberRestricted,
cls.LEFT: ChatMemberLeft,
cls.BANNED: ChatMemberBanned,
}.get(status)
class ChatMember(base.TelegramObject):
"""
This object contains information about one member of a chat.
Currently, the following 6 types of chat members are supported:
ChatMemberOwner
ChatMemberAdministrator
ChatMemberMember
ChatMemberRestricted
ChatMemberLeft
ChatMemberBanned
https://core.telegram.org/bots/api#chatmember
"""
status: base.String = fields.Field()
user: User = fields.Field(base=User)
def __int__(self) -> int:
return self.user.id
@classmethod
def resolve(cls, **kwargs) -> "ChatMember":
status = kwargs.get("status")
mapping = {
ChatMemberStatus.OWNER: ChatMemberOwner,
ChatMemberStatus.ADMINISTRATOR: ChatMemberAdministrator,
ChatMemberStatus.MEMBER: ChatMemberMember,
ChatMemberStatus.RESTRICTED: ChatMemberRestricted,
ChatMemberStatus.LEFT: ChatMemberLeft,
ChatMemberStatus.BANNED: ChatMemberBanned,
}
class_ = mapping.get(status)
if class_ is None:
raise ValueError(f"Can't find `ChatMember` class for status `{status}`")
return class_(**kwargs)
@classmethod
def to_object(cls,
data: typing.Dict[str, typing.Any],
conf: typing.Dict[str, typing.Any] = None
) -> "ChatMember":
return cls.resolve(**data)
def is_chat_creator(self) -> bool:
return ChatMemberStatus.is_chat_creator(self.status)
def is_chat_admin(self) -> bool:
return ChatMemberStatus.is_chat_admin(self.status)
def is_chat_member(self) -> bool:
return ChatMemberStatus.is_chat_member(self.status)
class ChatMemberOwner(ChatMember):
"""
Represents a chat member that owns the chat and has all
administrator privileges.
https://core.telegram.org/bots/api#chatmemberowner
"""
status: base.String = fields.Field(default=ChatMemberStatus.OWNER)
user: User = fields.Field(base=User)
custom_title: base.String = fields.Field()
is_anonymous: base.Boolean = fields.Field()
class ChatMemberAdministrator(ChatMember):
"""
Represents a chat member that has some additional privileges.
https://core.telegram.org/bots/api#chatmemberadministrator
"""
status: base.String = fields.Field(default=ChatMemberStatus.ADMINISTRATOR)
user: User = fields.Field(base=User)
can_be_edited: base.Boolean = fields.Field()
custom_title: base.String = fields.Field()
is_anonymous: base.Boolean = fields.Field()
can_manage_chat: base.Boolean = fields.Field()
can_post_messages: base.Boolean = fields.Field()
can_edit_messages: base.Boolean = fields.Field()
can_delete_messages: base.Boolean = fields.Field()
can_manage_voice_chats: base.Boolean = fields.Field()
can_restrict_members: base.Boolean = fields.Field()
can_promote_members: 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()
class ChatMemberMember(ChatMember):
"""
Represents a chat member that has no additional privileges or
restrictions.
https://core.telegram.org/bots/api#chatmembermember
"""
status: base.String = fields.Field(default=ChatMemberStatus.MEMBER)
user: User = fields.Field(base=User)
class ChatMemberRestricted(ChatMember):
"""
Represents a chat member that is under certain restrictions in the
chat. Supergroups only.
https://core.telegram.org/bots/api#chatmemberrestricted
"""
status: base.String = fields.Field(default=ChatMemberStatus.RESTRICTED)
user: User = fields.Field(base=User)
is_member: 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()
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()
until_date: datetime.datetime = fields.DateTimeField()
class ChatMemberLeft(ChatMember):
"""
Represents a chat member that isn't currently a member of the chat,
but may join it themselves.
https://core.telegram.org/bots/api#chatmemberleft
"""
status: base.String = fields.Field(default=ChatMemberStatus.LEFT)
user: User = fields.Field(base=User)
class ChatMemberBanned(ChatMember):
"""
Represents a chat member that was banned in the chat and can't
return to the chat or view chat messages.
https://core.telegram.org/bots/api#chatmemberbanned
"""
status: base.String = fields.Field(default=ChatMemberStatus.BANNED)
user: User = fields.Field(base=User)
until_date: datetime.datetime = fields.DateTimeField()

View file

@ -0,0 +1,22 @@
import datetime
from . import base
from . import fields
from .chat import Chat
from .chat_invite_link import ChatInviteLink
from .chat_member import ChatMember
from .user import User
class ChatMemberUpdated(base.TelegramObject):
"""
This object represents changes in the status of a chat member.
https://core.telegram.org/bots/api#chatmemberupdated
"""
chat: Chat = fields.Field(base=Chat)
from_user: User = fields.Field(alias="from", base=User)
date: datetime.datetime = fields.DateTimeField()
old_chat_member: ChatMember = fields.Field(base=ChatMember)
new_chat_member: ChatMember = fields.Field(base=ChatMember)
invite_link: ChatInviteLink = fields.Field(base=ChatInviteLink)

View file

@ -3,9 +3,7 @@ from . import base, fields
class Dice(base.TelegramObject):
"""
This object represents a dice with random value from 1 to 6.
(Yes, we're aware of the “proper” singular of die.
But it's awkward, and we decided to help it change. One dice at a time!)
This object represents an animated emoji that displays a random value.
https://core.telegram.org/bots/api#dice
"""
@ -17,3 +15,6 @@ class DiceEmoji:
DICE = '🎲'
DART = '🎯'
BASKETBALL = '🏀'
FOOTBALL = ''
SLOT_MACHINE = '🎰'
BOWLING = '🎳'

View file

@ -2,6 +2,7 @@ from . import base
from . import fields
from . import mixins
from .photo_size import PhotoSize
from ..utils import helper
class Document(base.TelegramObject, mixins.Downloadable):
@ -16,3 +17,34 @@ class Document(base.TelegramObject, mixins.Downloadable):
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()
@property
def mime_base(self) -> str:
base_type, _, _ = self.mime_type.partition('/')
return base_type
@property
def mime_subtype(self) -> str:
_, _, subtype = self.mime_type.partition('/')
return subtype
class MimeBase(helper.Helper):
"""
List of mime base types registered in IANA
https://www.iana.org/assignments/media-types/media-types.xhtml
"""
mode = helper.HelperMode.lowercase
APPLICATION = helper.Item() # application
AUDIO = helper.Item() # audio
EXAMPLE = helper.Item() # example
FONT = helper.Item() # font
IMAGE = helper.Item() # image
MESSAGE = helper.Item() # message
MODEL = helper.Item() # model
MULTIPART = helper.Item() # multipart
TEXT = helper.Item() # text
VIDEO = helper.Item() # video

View file

@ -112,7 +112,7 @@ class Field(BaseField):
and not hasattr(value, 'to_python'):
if not isinstance(parent, weakref.ReferenceType):
parent = weakref.ref(parent)
return self.base_object(conf={'parent':parent}, **value)
return self.base_object.to_object(conf={'parent': parent}, data=value)
return value
@ -129,18 +129,16 @@ class ListField(Field):
super(ListField, self).__init__(*args, default=default, **kwargs)
def serialize(self, value):
result = []
if value is None:
return None
serialize = super(ListField, self).serialize
for item in value:
result.append(serialize(item))
return result
return [serialize(item) for item in value]
def deserialize(self, value, parent=None):
result = []
if value is None:
return None
deserialize = super(ListField, self).deserialize
for item in value:
result.append(deserialize(item, parent=parent))
return result
return [deserialize(item, parent=parent) for item in value]
class ListOfLists(Field):
@ -148,9 +146,7 @@ class ListOfLists(Field):
result = []
serialize = super(ListOfLists, self).serialize
for row in value:
row_result = []
for item in row:
row_result.append(serialize(item))
row_result = [serialize(item) for item in row]
result.append(row_result)
return result
@ -159,9 +155,7 @@ class ListOfLists(Field):
deserialize = super(ListOfLists, self).deserialize
if hasattr(value, '__iter__'):
for row in value:
row_result = []
for item in row:
row_result.append(deserialize(item, parent=parent))
row_result = [deserialize(item, parent=parent) for item in row]
result.append(row_result)
return result

View file

@ -6,31 +6,28 @@ from . import fields
class ForceReply(base.TelegramObject):
"""
Upon receiving a message with this object,
Telegram clients will display a reply interface to the user
(act as if the user has selected the bots message and tapped Reply').
This can be extremely useful if you want to create user-friendly step-by-step
Upon receiving a message with this object, Telegram clients will
display a reply interface to the user (act as if the user has
selected the bot's message and tapped 'Reply'). This can be
extremely useful if you want to create user-friendly step-by-step
interfaces without having to sacrifice privacy mode.
Example: A poll bot for groups runs in privacy mode
(only receives commands, replies to its messages and mentions).
There could be two ways to create a new poll
The last option is definitely more attractive.
And if you use ForceReply in your bots questions, it will receive the users answers even
if it only receives replies, commands and mentions without any extra work for the user.
https://core.telegram.org/bots/api#forcereply
"""
force_reply: base.Boolean = fields.Field(default=True)
input_field_placeholder: base.String = fields.Field()
selective: base.Boolean = fields.Field()
@classmethod
def create(cls, selective: typing.Optional[base.Boolean] = None):
def create(cls,
input_field_placeholder: typing.Optional[base.String] = None,
selective: typing.Optional[base.Boolean] = None,
) -> 'ForceReply':
"""
Create new force reply
:param selective:
:param input_field_placeholder:
:return:
"""
return cls(selective=selective)
return cls(selective=selective, input_field_placeholder=input_field_placeholder)

View file

@ -50,7 +50,7 @@ class InlineKeyboardMarkup(base.TelegramObject):
if index % self.row_width == 0:
self.inline_keyboard.append(row)
row = []
if len(row) > 0:
if row:
self.inline_keyboard.append(row)
return self
@ -62,9 +62,7 @@ class InlineKeyboardMarkup(base.TelegramObject):
:return: self
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
btn_array = []
for button in args:
btn_array.append(button)
btn_array = [button for button in args]
self.inline_keyboard.append(btn_array)
return self

View file

@ -17,17 +17,18 @@ class InlineQuery(base.TelegramObject):
"""
id: base.String = fields.Field()
from_user: User = fields.Field(alias='from', base=User)
location: Location = fields.Field(base=Location)
query: base.String = fields.Field()
offset: base.String = fields.Field()
chat_type: base.String = fields.Field()
location: Location = fields.Field(base=Location)
async def answer(self,
results: typing.List[InlineQueryResult],
cache_time: typing.Union[base.Integer, None] = None,
is_personal: typing.Union[base.Boolean, None] = None,
next_offset: typing.Union[base.String, None] = None,
switch_pm_text: typing.Union[base.String, None] = None,
switch_pm_parameter: typing.Union[base.String, None] = None):
cache_time: typing.Optional[base.Integer] = None,
is_personal: typing.Optional[base.Boolean] = None,
next_offset: typing.Optional[base.String] = None,
switch_pm_text: typing.Optional[base.String] = None,
switch_pm_parameter: typing.Optional[base.String] = None):
"""
Use this method to send answers to an inline query.
No more than 50 results per query are allowed.
@ -38,22 +39,22 @@ class InlineQuery(base.TelegramObject):
:type results: :obj:`typing.List[types.InlineQueryResult]`
:param cache_time: The maximum amount of time in seconds that the result of the
inline query may be cached on the server. Defaults to 300.
:type cache_time: :obj:`typing.Union[base.Integer, None]`
:type cache_time: :obj:`typing.Optional[base.Integer]`
:param is_personal: Pass True, if results may be cached on the server side only
for the user that sent the query. By default, results may be returned to any user who sends the same query
:type is_personal: :obj:`typing.Union[base.Boolean, None]`
:type is_personal: :obj:`typing.Optional[base.Boolean]`
:param next_offset: Pass the offset that a client should send in the
next query with the same text to receive more results.
Pass an empty string if there are no more results or if you dont support pagination.
Offset length cant exceed 64 bytes.
:type next_offset: :obj:`typing.Union[base.String, None]`
:type next_offset: :obj:`typing.Optional[base.String]`
:param switch_pm_text: If passed, clients will display a button with specified text that
switches the user to a private chat with the bot and sends the bot a start message
with the parameter switch_pm_parameter
:type switch_pm_text: :obj:`typing.Union[base.String, None]`
:type switch_pm_text: :obj:`typing.Optional[base.String]`
:param switch_pm_parameter: Deep-linking parameter for the /start message sent to the bot when
user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed.
:type switch_pm_parameter: :obj:`typing.Union[base.String, None]`
:type switch_pm_parameter: :obj:`typing.Optional[base.String]`
:return: On success, True is returned
:rtype: :obj:`base.Boolean`
"""

View file

@ -4,6 +4,7 @@ from . import base
from . import fields
from .inline_keyboard import InlineKeyboardMarkup
from .input_message_content import InputMessageContent
from .message_entity import MessageEntity
class InlineQueryResult(base.TelegramObject):
@ -83,23 +84,29 @@ class InlineQueryResultPhoto(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
photo_url: base.String,
thumb_url: base.String,
photo_width: typing.Optional[base.Integer] = None,
photo_height: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url,
photo_width=photo_width, photo_height=photo_height, title=title,
description=description, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
photo_url: base.String,
thumb_url: base.String,
photo_width: typing.Optional[base.Integer] = None,
photo_height: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, photo_url=photo_url, thumb_url=thumb_url,
photo_width=photo_width, photo_height=photo_height, title=title,
description=description, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultGif(InlineQueryResult):
@ -123,23 +130,29 @@ class InlineQueryResultGif(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
gif_url: base.String,
gif_width: typing.Optional[base.Integer] = None,
gif_height: typing.Optional[base.Integer] = None,
gif_duration: typing.Optional[base.Integer] = None,
thumb_url: typing.Optional[base.String] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultGif, self).__init__(id=id, gif_url=gif_url, gif_width=gif_width,
gif_height=gif_height, gif_duration=gif_duration,
thumb_url=thumb_url, title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
gif_url: base.String,
gif_width: typing.Optional[base.Integer] = None,
gif_height: typing.Optional[base.Integer] = None,
gif_duration: typing.Optional[base.Integer] = None,
thumb_url: typing.Optional[base.String] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, gif_url=gif_url, gif_width=gif_width, gif_height=gif_height,
gif_duration=gif_duration, thumb_url=thumb_url, title=title,
caption=caption, parse_mode=parse_mode, reply_markup=reply_markup,
caption_entities=caption_entities,
input_message_content=input_message_content,
)
class InlineQueryResultMpeg4Gif(InlineQueryResult):
@ -163,23 +176,30 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
mpeg4_url: base.String,
thumb_url: base.String,
mpeg4_width: typing.Optional[base.Integer] = None,
mpeg4_height: typing.Optional[base.Integer] = None,
mpeg4_duration: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultMpeg4Gif, self).__init__(id=id, mpeg4_url=mpeg4_url, mpeg4_width=mpeg4_width,
mpeg4_height=mpeg4_height, mpeg4_duration=mpeg4_duration,
thumb_url=thumb_url, title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
mpeg4_url: base.String,
thumb_url: base.String,
mpeg4_width: typing.Optional[base.Integer] = None,
mpeg4_height: typing.Optional[base.Integer] = None,
mpeg4_duration: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, mpeg4_url=mpeg4_url, mpeg4_width=mpeg4_width,
mpeg4_height=mpeg4_height, mpeg4_duration=mpeg4_duration,
thumb_url=thumb_url, title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
caption_entities=caption_entities,
input_message_content=input_message_content,
)
class InlineQueryResultVideo(InlineQueryResult):
@ -207,26 +227,32 @@ class InlineQueryResultVideo(InlineQueryResult):
description: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
video_url: base.String,
mime_type: base.String,
thumb_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
video_width: typing.Optional[base.Integer] = None,
video_height: typing.Optional[base.Integer] = None,
video_duration: typing.Optional[base.Integer] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultVideo, self).__init__(id=id, video_url=video_url, mime_type=mime_type,
thumb_url=thumb_url, title=title, caption=caption,
video_width=video_width, video_height=video_height,
video_duration=video_duration, description=description,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
video_url: base.String,
mime_type: base.String,
thumb_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
video_width: typing.Optional[base.Integer] = None,
video_height: typing.Optional[base.Integer] = None,
video_duration: typing.Optional[base.Integer] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, video_url=video_url, mime_type=mime_type, thumb_url=thumb_url,
title=title, caption=caption, video_width=video_width,
video_height=video_height, video_duration=video_duration,
description=description, parse_mode=parse_mode,
reply_markup=reply_markup, caption_entities=caption_entities,
input_message_content=input_message_content,
)
class InlineQueryResultAudio(InlineQueryResult):
@ -248,21 +274,27 @@ class InlineQueryResultAudio(InlineQueryResult):
audio_duration: base.Integer = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
audio_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
performer: typing.Optional[base.String] = None,
audio_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultAudio, self).__init__(id=id, audio_url=audio_url, title=title,
caption=caption, parse_mode=parse_mode,
performer=performer, audio_duration=audio_duration,
reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
audio_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
performer: typing.Optional[base.String] = None,
audio_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, audio_url=audio_url, title=title,
caption=caption, parse_mode=parse_mode,
performer=performer, audio_duration=audio_duration,
reply_markup=reply_markup, caption_entities=caption_entities,
input_message_content=input_message_content,
)
class InlineQueryResultVoice(InlineQueryResult):
@ -285,19 +317,25 @@ class InlineQueryResultVoice(InlineQueryResult):
voice_duration: base.Integer = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
voice_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
voice_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultVoice, self).__init__(id=id, voice_url=voice_url, title=title,
caption=caption, voice_duration=voice_duration,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
voice_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
voice_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, voice_url=voice_url, title=title, caption=caption,
voice_duration=voice_duration, parse_mode=parse_mode,
reply_markup=reply_markup, caption_entities=caption_entities,
input_message_content=input_message_content,
)
class InlineQueryResultDocument(InlineQueryResult):
@ -323,25 +361,31 @@ class InlineQueryResultDocument(InlineQueryResult):
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
def __init__(self, *,
id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
document_url: typing.Optional[base.String] = None,
mime_type: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None):
super(InlineQueryResultDocument, self).__init__(id=id, title=title, caption=caption,
document_url=document_url, mime_type=mime_type,
description=description, reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url, thumb_width=thumb_width,
thumb_height=thumb_height, parse_mode=parse_mode)
def __init__(
self,
*,
id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
document_url: typing.Optional[base.String] = None,
mime_type: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
):
super().__init__(
id=id, title=title, caption=caption, parse_mode=parse_mode,
caption_entities=caption_entities, document_url=document_url,
mime_type=mime_type, description=description, reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url, thumb_width=thumb_width,
thumb_height=thumb_height,
)
class InlineQueryResultLocation(InlineQueryResult):
@ -352,16 +396,16 @@ class InlineQueryResultLocation(InlineQueryResult):
Alternatively, you can use input_message_content to send a message with the specified content
instead of the location.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultlocation
"""
type: base.String = fields.Field(alias='type', default='location')
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
title: base.String = fields.Field()
horizontal_accuracy: typing.Optional[base.Float] = fields.Field()
live_period: base.Integer = fields.Field()
heading: typing.Optional[base.Integer] = fields.Field()
proximity_alert_radius: typing.Optional[base.Integer] = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
@ -372,18 +416,31 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude: base.Float,
longitude: base.Float,
title: base.String,
horizontal_accuracy: typing.Optional[base.Float] = None,
live_period: typing.Optional[base.Integer] = None,
heading: typing.Optional[base.Integer] = None,
proximity_alert_radius: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None):
super(InlineQueryResultLocation, self).__init__(id=id, latitude=latitude, longitude=longitude,
title=title, live_period=live_period,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url, thumb_width=thumb_width,
thumb_height=thumb_height)
thumb_height: typing.Optional[base.Integer] = None,
):
super().__init__(
id=id,
latitude=latitude,
longitude=longitude,
title=title,
horizontal_accuracy=horizontal_accuracy,
live_period=live_period,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height
)
class InlineQueryResultVenue(InlineQueryResult):
@ -404,31 +461,40 @@ class InlineQueryResultVenue(InlineQueryResult):
title: base.String = fields.Field()
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
foursquare_type: base.String = fields.Field()
google_place_id: base.String = fields.Field()
google_place_type: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
foursquare_type: base.String = fields.Field()
def __init__(self, *,
id: base.String,
latitude: base.Float,
longitude: base.Float,
title: base.String,
address: base.String,
foursquare_id: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
foursquare_type: typing.Optional[base.String] = None):
super(InlineQueryResultVenue, self).__init__(id=id, latitude=latitude, longitude=longitude,
title=title, address=address, foursquare_id=foursquare_id,
reply_markup=reply_markup,
input_message_content=input_message_content, thumb_url=thumb_url,
thumb_width=thumb_width, thumb_height=thumb_height,
foursquare_type=foursquare_type)
def __init__(
self,
*,
id: base.String,
latitude: base.Float,
longitude: base.Float,
title: base.String,
address: base.String,
foursquare_id: typing.Optional[base.String] = None,
foursquare_type: typing.Optional[base.String] = None,
google_place_id: typing.Optional[base.String] = None,
google_place_type: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
):
super().__init__(
id=id, latitude=latitude, longitude=longitude, title=title,
address=address, foursquare_id=foursquare_id,
foursquare_type=foursquare_type, google_place_id=google_place_id,
google_place_type=google_place_type, reply_markup=reply_markup,
input_message_content=input_message_content, thumb_url=thumb_url,
thumb_width=thumb_width, thumb_height=thumb_height,
)
class InlineQueryResultContact(InlineQueryResult):
@ -510,19 +576,24 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
photo_file_id: base.String,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedPhoto, self).__init__(id=id, photo_file_id=photo_file_id, title=title,
description=description, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
photo_file_id: base.String,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, photo_file_id=photo_file_id, title=title, description=description,
caption=caption, parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultCachedGif(InlineQueryResult):
@ -541,18 +612,23 @@ class InlineQueryResultCachedGif(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
gif_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedGif, self).__init__(id=id, gif_file_id=gif_file_id,
title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
gif_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, gif_file_id=gif_file_id, title=title, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
@ -571,18 +647,23 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
mpeg4_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedMpeg4Gif, self).__init__(id=id, mpeg4_file_id=mpeg4_file_id,
title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
mpeg4_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, mpeg4_file_id=mpeg4_file_id, title=title, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultCachedSticker(InlineQueryResult):
@ -631,20 +712,25 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
title: base.String,
document_file_id: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedDocument, self).__init__(id=id, title=title,
document_file_id=document_file_id,
description=description, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
title: base.String,
document_file_id: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, title=title, document_file_id=document_file_id,
description=description, caption=caption, parse_mode=parse_mode,
caption_entities=caption_entities, reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedVideo(InlineQueryResult):
@ -664,19 +750,24 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
video_file_id: base.String,
title: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedVideo, self).__init__(id=id, video_file_id=video_file_id, title=title,
description=description, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
video_file_id: base.String,
title: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, video_file_id=video_file_id, title=title, description=description,
caption=caption, parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultCachedVoice(InlineQueryResult):
@ -697,18 +788,23 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
voice_file_id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedVoice, self).__init__(id=id, voice_file_id=voice_file_id,
title=title, caption=caption,
parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
voice_file_id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, voice_file_id=voice_file_id, title=title, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)
class InlineQueryResultCachedAudio(InlineQueryResult):
@ -729,14 +825,19 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(self, *,
id: base.String,
audio_file_id: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultCachedAudio, self).__init__(id=id, audio_file_id=audio_file_id,
caption=caption, parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content)
def __init__(
self,
*,
id: base.String,
audio_file_id: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super().__init__(
id=id, audio_file_id=audio_file_id, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
reply_markup=reply_markup, input_message_content=input_message_content,
)

View file

@ -4,6 +4,8 @@ import io
import logging
import os
import secrets
from pathlib import Path
from typing import Union
import aiohttp
@ -25,7 +27,7 @@ class InputFile(base.TelegramObject):
https://core.telegram.org/bots/api#inputfile
"""
def __init__(self, path_or_bytesio, filename=None, conf=None):
def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path], filename=None, conf=None):
"""
:param path_or_bytesio:
@ -39,12 +41,14 @@ class InputFile(base.TelegramObject):
self._path = path_or_bytesio
if filename is None:
filename = os.path.split(path_or_bytesio)[-1]
elif isinstance(path_or_bytesio, io.IOBase):
self._path = None
self._file = path_or_bytesio
elif isinstance(path_or_bytesio, _WebPipe):
elif isinstance(path_or_bytesio, (io.IOBase, _WebPipe)):
self._path = None
self._file = path_or_bytesio
elif isinstance(path_or_bytesio, Path):
self._file = path_or_bytesio.open("rb")
self._path = path_or_bytesio.resolve()
if filename is None:
filename = path_or_bytesio.name
else:
raise TypeError('Not supported file type.')
@ -166,10 +170,7 @@ class _WebPipe:
def name(self):
if not self._name:
*_, part = self.url.rpartition('/')
if part:
self._name = part
else:
self._name = secrets.token_urlsafe(24)
self._name = part or secrets.token_urlsafe(24)
return self._name
async def open(self):

View file

@ -5,6 +5,7 @@ import typing
from . import base
from . import fields
from .input_file import InputFile
from .message_entity import MessageEntity
ATTACHMENT_PREFIX = 'attach://'
@ -27,6 +28,7 @@ class InputMedia(base.TelegramObject):
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
caption: base.String = fields.Field()
parse_mode: base.String = fields.Field()
caption_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity)
def __init__(self, *args, **kwargs):
self._thumb_file = None
@ -106,28 +108,48 @@ 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.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)
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,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
**kwargs,
):
super().__init__(
type='animation', media=media, thumb=thumb, caption=caption, width=width,
height=height, duration=duration, parse_mode=parse_mode,
caption_entities=caption_entities, conf=kwargs,
)
class InputMediaDocument(InputMedia):
"""
Represents a photo to be sent.
Represents a general file to be sent.
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.String = 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] = None,
caption: base.String = None,
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
disable_content_type_detection: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
type='document', media=media, thumb=thumb, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
disable_content_type_detection=disable_content_type_detection,
conf=kwargs,
)
class InputMediaAudio(InputMedia):
@ -141,17 +163,23 @@ 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,
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, 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,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
**kwargs,
):
super().__init__(
type='audio', media=media, thumb=thumb, caption=caption,
duration=duration, performer=performer, title=title,
parse_mode=parse_mode, caption_entities=caption_entities, conf=kwargs,
)
class InputMediaPhoto(InputMedia):
@ -161,11 +189,18 @@ 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.String = 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,
caption: base.String = None,
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
**kwargs,
):
super().__init__(
type='photo', media=media, caption=caption, parse_mode=parse_mode,
caption_entities=caption_entities, conf=kwargs,
)
class InputMediaVideo(InputMedia):
@ -179,16 +214,25 @@ class InputMediaVideo(InputMedia):
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.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)
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,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
supports_streaming: base.Boolean = None,
**kwargs,
):
super().__init__(
type='video', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, caption_entities=caption_entities,
supports_streaming=supports_streaming, conf=kwargs
)
class MediaGroup(base.TelegramObject):
@ -227,10 +271,10 @@ class MediaGroup(base.TelegramObject):
media = InputMediaPhoto(**media)
elif media_type == 'video':
media = InputMediaVideo(**media)
# elif media_type == 'document':
# media = InputMediaDocument(**media)
# elif media_type == 'audio':
# media = InputMediaAudio(**media)
elif media_type == 'document':
media = InputMediaDocument(**media)
elif media_type == 'audio':
media = InputMediaAudio(**media)
# elif media_type == 'animation':
# media = InputMediaAnimation(**media)
else:
@ -239,8 +283,8 @@ class MediaGroup(base.TelegramObject):
elif not isinstance(media, InputMedia):
raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}")
elif media.type in ['document', 'audio', 'animation']:
raise ValueError(f"This type of media is not supported by media groups!")
elif media.type == 'animation':
raise ValueError("This type of media is not supported by media groups!")
self.media.append(media)
@ -266,78 +310,98 @@ class MediaGroup(base.TelegramObject):
width=width, height=height, duration=duration,
parse_mode=parse_mode)
self.attach(animation)
'''
def attach_audio(self, audio: base.InputFile,
def attach_audio(self, audio: typing.Union[InputMediaAudio, 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):
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None):
"""
Attach animation
Attach audio
:param audio:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param performer:
:param title:
:param parse_mode:
:param caption_entities:
"""
if not isinstance(audio, InputMedia):
audio = InputMediaAudio(media=audio, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
duration=duration,
performer=performer, title=title,
parse_mode=parse_mode)
parse_mode=parse_mode,
caption_entities=caption_entities)
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.String = None):
def attach_document(self, document: typing.Union[InputMediaDocument, base.InputFile],
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
disable_content_type_detection: typing.Optional[base.Boolean] = None):
"""
Attach document
:param parse_mode:
:param document:
:param caption:
:param thumb:
:param document:
:param parse_mode:
:param caption_entities:
:param disable_content_type_detection:
"""
if not isinstance(document, InputMedia):
document = InputMediaDocument(media=document, thumb=thumb, caption=caption, parse_mode=parse_mode)
document = InputMediaDocument(media=document, thumb=thumb, caption=caption,
parse_mode=parse_mode, caption_entities=caption_entities,
disable_content_type_detection=disable_content_type_detection)
self.attach(document)
'''
def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile],
caption: base.String = None):
caption: base.String = None, parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None):
"""
Attach photo
:param photo:
:param caption:
:param parse_mode:
:param caption_entities:
"""
if not isinstance(photo, InputMedia):
photo = InputMediaPhoto(media=photo, caption=caption)
photo = InputMediaPhoto(media=photo, caption=caption, parse_mode=parse_mode,
caption_entities=caption_entities)
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):
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None, parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
supports_streaming: base.Boolean = None):
"""
Attach video
:param video:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param parse_mode:
:param caption_entities:
:param supports_streaming:
"""
if not isinstance(video, InputMedia):
video = InputMediaVideo(media=video, thumb=thumb, caption=caption,
width=width, height=height, duration=duration)
width=width, height=height, duration=duration,
parse_mode=parse_mode, caption_entities=caption_entities,
supports_streaming=supports_streaming)
self.attach(video)
def to_python(self) -> typing.List:

View file

@ -2,6 +2,9 @@ import typing
from . import base
from . import fields
from .message_entity import MessageEntity
from .labeled_price import LabeledPrice
from ..utils.payload import generate_payload
class InputMessageContent(base.TelegramObject):
@ -26,31 +29,112 @@ class InputContactMessageContent(InputMessageContent):
"""
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
vcard: base.String = fields.Field()
last_name: typing.Optional[base.String] = fields.Field()
vcard: typing.Optional[base.String] = fields.Field()
def __init__(self, phone_number: base.String,
first_name: typing.Optional[base.String] = None,
last_name: typing.Optional[base.String] = None):
super(InputContactMessageContent, self).__init__(phone_number=phone_number, first_name=first_name,
last_name=last_name)
def __init__(self,
phone_number: base.String,
first_name: base.String = None,
last_name: typing.Optional[base.String] = None,
vcard: typing.Optional[base.String] = None,
):
super().__init__(
phone_number=phone_number,
first_name=first_name,
last_name=last_name,
vcard=vcard
)
class InputInvoiceMessageContent(InputMessageContent):
"""
Represents the content of an invoice message to be sent as the
result of an inline query.
https://core.telegram.org/bots/api#inputinvoicemessagecontent
"""
title: base.String = fields.Field()
description: base.String = fields.Field()
payload: base.String = fields.Field()
provider_token: base.String = fields.Field()
currency: base.String = fields.Field()
prices: typing.List[LabeledPrice] = fields.ListField(base=LabeledPrice)
max_tip_amount: typing.Optional[base.Integer] = fields.Field()
suggested_tip_amounts: typing.Optional[
typing.List[base.Integer]
] = fields.ListField(base=base.Integer)
provider_data: typing.Optional[base.String] = fields.Field()
photo_url: typing.Optional[base.String] = fields.Field()
photo_size: typing.Optional[base.Integer] = fields.Field()
photo_width: typing.Optional[base.Integer] = fields.Field()
photo_height: typing.Optional[base.Integer] = fields.Field()
need_name: typing.Optional[base.Boolean] = fields.Field()
need_phone_number: typing.Optional[base.Boolean] = fields.Field()
need_email: typing.Optional[base.Boolean] = fields.Field()
need_shipping_address: typing.Optional[base.Boolean] = fields.Field()
send_phone_number_to_provider: typing.Optional[base.Boolean] = fields.Field()
send_email_to_provider: typing.Optional[base.Boolean] = fields.Field()
is_flexible: typing.Optional[base.Boolean] = fields.Field()
def __init__(
self,
title: base.String,
description: base.String,
payload: base.String,
provider_token: base.String,
currency: base.String,
prices: typing.List[LabeledPrice] = None,
max_tip_amount: typing.Optional[base.Integer] = None,
suggested_tip_amounts: typing.Optional[typing.List[base.Integer]] = None,
provider_data: typing.Optional[base.String] = None,
photo_url: typing.Optional[base.String] = None,
photo_size: typing.Optional[base.Integer] = None,
photo_width: typing.Optional[base.Integer] = None,
photo_height: typing.Optional[base.Integer] = None,
need_name: typing.Optional[base.Boolean] = None,
need_phone_number: typing.Optional[base.Boolean] = None,
need_email: typing.Optional[base.Boolean] = None,
need_shipping_address: typing.Optional[base.Boolean] = None,
send_phone_number_to_provider: typing.Optional[base.Boolean] = None,
send_email_to_provider: typing.Optional[base.Boolean] = None,
is_flexible: typing.Optional[base.Boolean] = None,
):
if prices is None:
prices = []
payload = generate_payload(**locals())
super().__init__(**payload)
class InputLocationMessageContent(InputMessageContent):
"""
Represents the content of a location message to be sent as the result of an inline query.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inputlocationmessagecontent
"""
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
horizontal_accuracy: typing.Optional[base.Float] = fields.Field()
live_period: typing.Optional[base.Integer] = fields.Field()
heading: typing.Optional[base.Integer] = fields.Field()
proximity_alert_radius: typing.Optional[base.Integer] = fields.Field()
def __init__(self, latitude: base.Float,
longitude: base.Float):
super(InputLocationMessageContent, self).__init__(latitude=latitude, longitude=longitude)
def __init__(self,
latitude: base.Float,
longitude: base.Float,
horizontal_accuracy: typing.Optional[base.Float] = None,
live_period: typing.Optional[base.Integer] = None,
heading: typing.Optional[base.Integer] = None,
proximity_alert_radius: typing.Optional[base.Integer] = None,
):
super().__init__(
latitude=latitude,
longitude=longitude,
horizontal_accuracy=horizontal_accuracy,
live_period=live_period,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
)
class InputTextMessageContent(InputMessageContent):
@ -60,7 +144,8 @@ class InputTextMessageContent(InputMessageContent):
https://core.telegram.org/bots/api#inputtextmessagecontent
"""
message_text: base.String = fields.Field()
parse_mode: base.String = fields.Field()
parse_mode: typing.Optional[base.String] = fields.Field()
caption_entities: typing.Optional[typing.List[MessageEntity]] = fields.Field()
disable_web_page_preview: base.Boolean = fields.Field()
def safe_get_parse_mode(self):
@ -69,14 +154,22 @@ class InputTextMessageContent(InputMessageContent):
except RuntimeError:
pass
def __init__(self, message_text: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
disable_web_page_preview: typing.Optional[base.Boolean] = None):
def __init__(
self,
message_text: base.String,
parse_mode: typing.Optional[base.String] = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
disable_web_page_preview: typing.Optional[base.Boolean] = None,
):
if parse_mode is None:
parse_mode = self.safe_get_parse_mode()
super(InputTextMessageContent, self).__init__(message_text=message_text, parse_mode=parse_mode,
disable_web_page_preview=disable_web_page_preview)
super().__init__(
message_text=message_text,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_web_page_preview=disable_web_page_preview,
)
class InputVenueMessageContent(InputMessageContent):
@ -92,12 +185,29 @@ class InputVenueMessageContent(InputMessageContent):
longitude: base.Float = fields.Field()
title: base.String = fields.Field()
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
foursquare_id: typing.Optional[base.String] = fields.Field()
foursquare_type: typing.Optional[base.String] = fields.Field()
google_place_id: typing.Optional[base.String] = fields.Field()
google_place_type: typing.Optional[base.String] = fields.Field()
def __init__(self, latitude: typing.Optional[base.Float] = None,
longitude: typing.Optional[base.Float] = None,
title: typing.Optional[base.String] = None,
address: typing.Optional[base.String] = None,
foursquare_id: typing.Optional[base.String] = None):
super(InputVenueMessageContent, self).__init__(latitude=latitude, longitude=longitude, title=title,
address=address, foursquare_id=foursquare_id)
def __init__(
self,
latitude: base.Float,
longitude: base.Float,
title: base.String,
address: base.String,
foursquare_id: typing.Optional[base.String] = None,
foursquare_type: typing.Optional[base.String] = None,
google_place_id: typing.Optional[base.String] = None,
google_place_type: typing.Optional[base.String] = None,
):
super().__init__(
latitude=latitude,
longitude=longitude,
title=title,
address=address,
foursquare_id=foursquare_id,
foursquare_type=foursquare_type,
google_place_id=google_place_id,
google_place_type=google_place_type,
)

View file

@ -1,3 +1,5 @@
import typing
from . import base
from . import fields
@ -10,3 +12,7 @@ class Location(base.TelegramObject):
"""
longitude: base.Float = fields.Field()
latitude: base.Float = fields.Field()
horizontal_accuracy: typing.Optional[base.Float] = fields.Field()
live_period: typing.Optional[base.Integer] = fields.Field()
heading: typing.Optional[base.Integer] = fields.Field()
proximity_alert_radius: typing.Optional[base.Integer] = fields.Field()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
from . import base
from . import fields
class MessageAutoDeleteTimerChanged(base.TelegramObject):
"""
This object represents a service message about a change in auto-delete timer settings.
https://core.telegram.org/bots/api#messageautodeletetimerchanged
"""
message_auto_delete_time: base.Integer = fields.Field()

View file

@ -1,10 +1,9 @@
import sys
from . import base
from . import fields
from .user import User
from ..utils import helper, markdown
from ..utils.deprecated import deprecated
from . import base, fields
from .user import User
class MessageEntity(base.TelegramObject):
@ -13,6 +12,7 @@ class MessageEntity(base.TelegramObject):
https://core.telegram.org/bots/api#messageentity
"""
type: base.String = fields.Field()
offset: base.Integer = fields.Field()
length: base.Integer = fields.Field()
@ -20,6 +20,26 @@ class MessageEntity(base.TelegramObject):
user: User = fields.Field(base=User)
language: base.String = fields.Field()
def __init__(
self,
type: base.String,
offset: base.Integer,
length: base.Integer,
url: base.String = None,
user: User = None,
language: base.String = None,
**kwargs
):
super().__init__(
type=type,
offset=offset,
length=length,
url=url,
user=user,
language=language,
**kwargs
)
def get_text(self, text):
"""
Get value of entity
@ -27,18 +47,18 @@ class MessageEntity(base.TelegramObject):
:param text: full text
:return: part of text
"""
if sys.maxunicode == 0xffff:
return text[self.offset:self.offset + self.length]
if sys.maxunicode == 0xFFFF:
return text[self.offset : self.offset + self.length]
if not isinstance(text, bytes):
entity_text = text.encode('utf-16-le')
else:
entity_text = text
entity_text = (
text.encode("utf-16-le") if not isinstance(text, bytes) else text
)
entity_text = entity_text[self.offset * 2 : (self.offset + self.length) * 2]
return entity_text.decode("utf-16-le")
entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2]
return entity_text.decode('utf-16-le')
@deprecated("This method doesn't work with nested entities and will be removed in aiogram 3.0")
@deprecated(
"This method doesn't work with nested entities and will be removed in aiogram 3.0"
)
def parse(self, text, as_html=True):
"""
Get entity value with markup
@ -95,6 +115,7 @@ class MessageEntityType(helper.Helper):
:key: TEXT_LINK
:key: TEXT_MENTION
"""
mode = helper.HelperMode.snake_case
MENTION = helper.Item() # mention - @username

View file

@ -0,0 +1,10 @@
from . import base, fields
class MessageId(base.TelegramObject):
"""
This object represents a unique message identifier.
https://core.telegram.org/bots/api#messageid
"""
message_id: base.String = fields.Field()

View file

@ -0,0 +1,15 @@
from . import base
from . import fields
from .user import User
class ProximityAlertTriggered(base.TelegramObject):
"""
This object represents the content of a service message, sent whenever a user in
the chat triggers a proximity alert set by another user.
https://core.telegram.org/bots/api#proximityalerttriggered
"""
traveler: User = fields.Field()
watcher: User = fields.Field()
distance: base.Integer = fields.Field()

View file

@ -18,23 +18,32 @@ class KeyboardButtonPollType(base.TelegramObject):
class ReplyKeyboardMarkup(base.TelegramObject):
"""
This object represents a custom keyboard with reply options (see Introduction to bots for details and examples).
This object represents a custom keyboard with reply options
(see https://core.telegram.org/bots#keyboards to bots for details
and examples).
https://core.telegram.org/bots/api#replykeyboardmarkup
"""
keyboard: 'typing.List[typing.List[KeyboardButton]]' = fields.ListOfLists(base='KeyboardButton', default=[])
resize_keyboard: base.Boolean = fields.Field()
one_time_keyboard: base.Boolean = fields.Field()
input_field_placeholder: base.String = fields.Field()
selective: base.Boolean = fields.Field()
def __init__(self, keyboard: 'typing.List[typing.List[KeyboardButton]]' = None,
resize_keyboard: base.Boolean = None,
one_time_keyboard: base.Boolean = None,
input_field_placeholder: base.String = None,
selective: base.Boolean = None,
row_width: base.Integer = 3):
super(ReplyKeyboardMarkup, self).__init__(keyboard=keyboard, resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard, selective=selective,
conf={'row_width': row_width})
super().__init__(
keyboard=keyboard,
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
input_field_placeholder=input_field_placeholder,
selective=selective,
conf={'row_width': row_width},
)
@property
def row_width(self):
@ -58,7 +67,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
if index % self.row_width == 0:
self.keyboard.append(row)
row = []
if len(row) > 0:
if row:
self.keyboard.append(row)
return self
@ -70,9 +79,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
:return: self
:rtype: :obj:`types.ReplyKeyboardMarkup`
"""
btn_array = []
for button in args:
btn_array.append(button)
btn_array = [button for button in args]
self.keyboard.append(btn_array)
return self

View file

@ -3,13 +3,14 @@ from __future__ import annotations
from . import base
from . import fields
from .callback_query import CallbackQuery
from .chat_member_updated import ChatMemberUpdated
from .chosen_inline_result import ChosenInlineResult
from .inline_query import InlineQuery
from .message import Message
from .poll import Poll, PollAnswer
from .pre_checkout_query import PreCheckoutQuery
from .shipping_query import ShippingQuery
from ..utils import helper
from ..utils import helper, deprecated
class Update(base.TelegramObject):
@ -31,6 +32,8 @@ class Update(base.TelegramObject):
pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery)
poll: Poll = fields.Field(base=Poll)
poll_answer: PollAnswer = fields.Field(base=PollAnswer)
my_chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated)
chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated)
def __hash__(self):
return self.update_id
@ -55,9 +58,21 @@ class AllowedUpdates(helper.Helper):
CHANNEL_POST = helper.ListItem() # channel_post
EDITED_CHANNEL_POST = helper.ListItem() # edited_channel_post
INLINE_QUERY = helper.ListItem() # inline_query
CHOSEN_INLINE_QUERY = helper.ListItem() # chosen_inline_result
CHOSEN_INLINE_RESULT = helper.ListItem() # chosen_inline_result
CALLBACK_QUERY = helper.ListItem() # callback_query
SHIPPING_QUERY = helper.ListItem() # shipping_query
PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query
POLL = helper.ListItem() # poll
POLL_ANSWER = helper.ListItem() # poll_answer
MY_CHAT_MEMBER = helper.ListItem() # my_chat_member
CHAT_MEMBER = helper.ListItem() # chat_member
CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar(
"`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. "
"Use `CHOSEN_INLINE_RESULT`",
new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT,
)
@classmethod
def default(cls):
return []

View file

@ -14,3 +14,5 @@ class Venue(base.TelegramObject):
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
foursquare_type: base.String = fields.Field()
google_place_id: base.String = fields.Field()
google_place_type: base.String = fields.Field()

View file

@ -16,5 +16,6 @@ class Video(base.TelegramObject, mixins.Downloadable):
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()

View file

@ -0,0 +1,13 @@
from . import base
from . import fields
from . import mixins
class VoiceChatEnded(base.TelegramObject, mixins.Downloadable):
"""
This object represents a service message about a voice chat ended in the chat.
https://core.telegram.org/bots/api#voicechatended
"""
duration: base.Integer = fields.Field()

View file

@ -0,0 +1,16 @@
import typing
from . import base
from . import fields
from . import mixins
from .user import User
class VoiceChatParticipantsInvited(base.TelegramObject, mixins.Downloadable):
"""
This object represents a service message about new members invited to a voice chat.
https://core.telegram.org/bots/api#voicechatparticipantsinvited
"""
users: typing.List[User] = fields.ListField(base=User)

View file

@ -0,0 +1,15 @@
from datetime import datetime
from . import base
from . import fields
from .user import User
class VoiceChatScheduled(base.TelegramObject):
"""
This object represents a service message about a voice chat scheduled in the chat.
https://core.telegram.org/bots/api#voicechatscheduled
"""
start_date: datetime = fields.DateTimeField()

View file

@ -0,0 +1,12 @@
from . import base
from . import mixins
class VoiceChatStarted(base.TelegramObject, mixins.Downloadable):
"""
This object represents a service message about a voice chat started in the chat.
Currently holds no information.
https://core.telegram.org/bots/api#voicechatstarted
"""
pass

View file

@ -13,6 +13,7 @@ class WebhookInfo(base.TelegramObject):
url: base.String = fields.Field()
has_custom_certificate: base.Boolean = fields.Field()
pending_update_count: base.Integer = fields.Field()
ip_address: base.String = fields.Field()
last_error_date: base.Integer = fields.Field()
last_error_message: base.String = fields.Field()
max_connections: base.Integer = fields.Field()

View file

@ -130,7 +130,6 @@ class CallbackDataFilter(Filter):
if isinstance(value, (list, tuple, set, frozenset)):
if data.get(key) not in value:
return False
else:
if data.get(key) != value:
return False
elif data.get(key) != value:
return False
return {'callback_data': data}

View file

@ -1,10 +1,10 @@
"""
Deep linking
Telegram bots have a deep linking mechanism, that allows for passing additional
parameters to the bot on startup. It could be a command that launches the bot or
an auth token to connect the user's Telegram account to their account on some
external service.
Telegram bots have a deep linking mechanism, that allows for passing
additional parameters to the bot on startup. It could be a command that
launches the bot or an auth token to connect the user's Telegram
account to their account on some external service.
You can read detailed description in the source:
https://core.telegram.org/bots#deep-linking
@ -16,86 +16,123 @@ Basic link example:
.. code-block:: python
from aiogram.utils.deep_linking import get_start_link
link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo'
link = await get_start_link('foo')
# result: 'https://t.me/MyBot?start=foo'
Encoded link example:
.. code-block:: python
from aiogram.utils.deep_linking import get_start_link, decode_payload
link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v'
# and decode it back:
payload = decode_payload('Zm9v') # result: 'foo'
from aiogram.utils.deep_linking import get_start_link
link = await get_start_link('foo', encode=True)
# result: 'https://t.me/MyBot?start=Zm9v'
Decode it back example:
.. code-block:: python
from aiogram.utils.deep_linking import decode_payload
from aiogram.types import Message
@dp.message_handler(commands=["start"])
async def handler(message: Message):
args = message.get_args()
payload = decode_payload(args)
await message.answer(f"Your payload: {payload}")
"""
import re
from base64 import urlsafe_b64decode, urlsafe_b64encode
from ..bot import Bot
BAD_PATTERN = re.compile(r"[^_A-z0-9-]")
async def get_start_link(payload: str, encode=False) -> str:
"""
Use this method to handy get 'start' deep link with your payload.
If you need to encode payload or pass special characters - set encode as True
Get 'start' deep link with your payload.
If you need to encode payload or pass special characters -
set encode as True
:param payload: args passed with /start
:param encode: encode payload with base64url
:return: link
"""
return await _create_link('start', payload, encode)
return await _create_link(
link_type="start",
payload=payload,
encode=encode,
)
async def get_startgroup_link(payload: str, encode=False) -> str:
"""
Use this method to handy get 'startgroup' deep link with your payload.
If you need to encode payload or pass special characters - set encode as True
Get 'startgroup' deep link with your payload.
If you need to encode payload or pass special characters -
set encode as True
:param payload: args passed with /start
:param encode: encode payload with base64url
:return: link
"""
return await _create_link('startgroup', payload, encode)
return await _create_link(
link_type="startgroup",
payload=payload,
encode=encode,
)
async def _create_link(link_type, payload: str, encode=False):
"""
Create deep link.
:param link_type: `start` or `startgroup`
:param payload: any string-convertible data
:param encode: pass True to encode the payload
:return: deeplink
"""
bot = await _get_bot_user()
payload = filter_payload(payload)
if encode:
payload = encode_payload(payload)
return f'https://t.me/{bot.username}?{link_type}={payload}'
def encode_payload(payload: str) -> str:
""" Encode payload with URL-safe base64url. """
from base64 import urlsafe_b64encode
result: bytes = urlsafe_b64encode(payload.encode())
return result.decode()
def decode_payload(payload: str) -> str:
""" Decode payload with URL-safe base64url. """
from base64 import urlsafe_b64decode
result: bytes = urlsafe_b64decode(payload + '=' * (4 - len(payload) % 4))
return result.decode()
def filter_payload(payload: str) -> str:
""" Convert payload to text and search for not allowed symbols. """
import re
# convert to string
if not isinstance(payload, str):
payload = str(payload)
# search for not allowed characters
if re.search(r'[^_A-z0-9-]', payload):
message = ('Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. '
'We recommend to encode parameters with binary and other '
'types of content.')
if encode:
payload = encode_payload(payload)
if re.search(BAD_PATTERN, payload):
message = (
"Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. "
"Pass `encode=True` or encode payload manually."
)
raise ValueError(message)
return payload
if len(payload) > 64:
message = "Payload must be up to 64 characters long."
raise ValueError(message)
return f"https://t.me/{bot.username}?{link_type}={payload}"
def encode_payload(payload: str) -> str:
"""Encode payload with URL-safe base64url."""
payload = str(payload)
bytes_payload: bytes = urlsafe_b64encode(payload.encode())
str_payload = bytes_payload.decode()
return str_payload.replace("=", "")
def decode_payload(payload: str) -> str:
"""Decode payload with URL-safe base64url."""
payload += "=" * (4 - len(payload) % 4)
result: bytes = urlsafe_b64decode(payload)
return result.decode()
async def _get_bot_user():
""" Get current user of bot. """
from ..bot import Bot
"""Get current user of bot."""
bot = Bot.get_current()
return await bot.me

View file

@ -2,7 +2,7 @@ import asyncio
import inspect
import warnings
import functools
from typing import Callable
from typing import Callable, Generic, TypeVar, Type, Optional
def deprecated(reason, stacklevel=2) -> Callable:
@ -129,3 +129,36 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
return wrapped
return decorator
_VT = TypeVar("_VT")
_OwnerCls = TypeVar("_OwnerCls")
class DeprecatedReadOnlyClassVar(Generic[_OwnerCls, _VT]):
"""
DeprecatedReadOnlyClassVar[Owner, ValueType]
:param warning_message: Warning message when getter gets called
:param new_value_getter: Any callable with (owner_class: Type[Owner]) -> ValueType
signature that will be executed
Usage example:
>>> class MyClass:
... some_attribute: DeprecatedReadOnlyClassVar[MyClass, int] = \
... DeprecatedReadOnlyClassVar(
... "Warning message.", lambda owner: 15)
...
>>> MyClass.some_attribute # does warning.warn with `Warning message` and returns 15 in the current case
"""
__slots__ = "_new_value_getter", "_warning_message"
def __init__(self, warning_message: str, new_value_getter: Callable[[_OwnerCls], _VT]):
self._warning_message = warning_message
self._new_value_getter = new_value_getter
def __get__(self, instance: Optional[_OwnerCls], owner: Type[_OwnerCls]):
warn_deprecated(self._warning_message, stacklevel=3)
return self._new_value_getter(owner)

View file

@ -6,11 +6,14 @@
- MessageError
- MessageNotModified
- MessageToForwardNotFound
- MessageIdInvalid
- MessageToDeleteNotFound
- MessageToPinNotFound
- MessageIdentifierNotSpecified
- MessageTextIsEmpty
- MessageCantBeEdited
- MessageCantBeDeleted
- MessageCantBeForwarded
- MessageToEditNotFound
- MessageToReplyNotFound
- ToMuchMessages
@ -37,6 +40,7 @@
- URLHostIsEmpty
- StartParamInvalid
- ButtonDataInvalid
- FileIsTooBig
- WrongFileIdentifier
- GroupDeactivated
- BadWebhook
@ -175,6 +179,11 @@ class MessageToForwardNotFound(MessageError):
match = 'message to forward not found'
class MessageIdInvalid(MessageError):
text = 'Invalid message id'
match = 'message_id_invalid'
class MessageToDeleteNotFound(MessageError):
"""
Will be raised when you try to delete very old or deleted or unknown message.
@ -182,11 +191,18 @@ class MessageToDeleteNotFound(MessageError):
match = 'message to delete not found'
class MessageToPinNotFound(MessageError):
"""
Will be raised when you try to pin deleted or unknown message.
"""
match = 'message to pin 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 = 'Reply message not found'
class MessageIdentifierNotSpecified(MessageError):
@ -205,6 +221,10 @@ class MessageCantBeDeleted(MessageError):
match = 'message can\'t be deleted'
class MessageCantBeForwarded(MessageError):
match = 'message can\'t be forwarded'
class MessageToEditNotFound(MessageError):
match = 'message to edit not found'
@ -339,12 +359,16 @@ class ButtonDataInvalid(BadRequest):
text = 'Button data invalid'
class FileIsTooBig(BadRequest):
match = 'File is too big'
class WrongFileIdentifier(BadRequest):
match = 'wrong file identifier/HTTP URL specified'
class GroupDeactivated(BadRequest):
match = 'group is deactivated'
match = 'Group chat was deactivated'
class PhotoAsInputFileRequired(BadRequest):
@ -470,6 +494,20 @@ class MethodIsNotAvailable(BadRequest):
match = "Method is available only for supergroups"
class CantRestrictChatOwner(BadRequest):
"""
Raises when bot restricts the chat owner
"""
match = 'Can\'t remove chat owner'
class UserIsAnAdministratorOfTheChat(BadRequest):
"""
Raises when bot restricts the chat admin
"""
match = 'User is an administrator of the chat'
class NotFound(TelegramAPIError, _MatchErrorMixin):
__group = True
@ -497,7 +535,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
class BotKicked(Unauthorized):
match = 'bot was kicked from a chat'
match = 'bot was kicked from'
class BotBlocked(Unauthorized):

View file

@ -2,7 +2,7 @@ import asyncio
import datetime
import functools
import secrets
from typing import Callable, Union, Optional, Any
from typing import Callable, Union, Optional, Any, List
from warnings import warn
from aiohttp import web
@ -23,7 +23,8 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
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):
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True,
allowed_updates: Optional[List[str]] = None):
"""
Start bot in long-polling mode
@ -34,11 +35,20 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
:param on_startup:
:param on_shutdown:
:param timeout:
:param relax:
:param fast:
:param allowed_updates:
"""
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, relax=relax, fast=fast)
executor.start_polling(
reset_webhook=reset_webhook,
timeout=timeout,
relax=relax,
fast=fast,
allowed_updates=allowed_updates
)
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
@ -123,13 +133,13 @@ class Executor:
"""
def __init__(self, dispatcher, skip_updates=None, check_ip=False, retry_after=None, loop=None):
if loop is None:
loop = dispatcher.loop
if loop is not None:
self._loop = loop
self.dispatcher = dispatcher
self.skip_updates = skip_updates
self.check_ip = check_ip
self.retry_after = retry_after
self.loop = loop
self._identity = secrets.token_urlsafe(16)
self._web_app = None
@ -145,13 +155,17 @@ class Executor:
Bot.set_current(dispatcher.bot)
Dispatcher.set_current(dispatcher)
@property
def loop(self) -> asyncio.AbstractEventLoop:
return getattr(self, "_loop", asyncio.get_event_loop())
@property
def frozen(self):
return self._freeze
def set_web_app(self, application: web.Application):
"""
Change instance of aiohttp.web.Applicaton
Change instance of aiohttp.web.Application
:param application:
"""
@ -291,7 +305,8 @@ class Executor:
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, relax=0.1, fast=True):
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True,
allowed_updates: Optional[List[str]] = None):
"""
Start bot in long-polling mode
@ -304,7 +319,7 @@ class Executor:
try:
loop.run_until_complete(self._startup_polling())
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
relax=relax, fast=fast))
relax=relax, fast=fast, allowed_updates=allowed_updates))
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
# loop.stop()
@ -350,7 +365,7 @@ class Executor:
self.dispatcher.stop_polling()
await self.dispatcher.storage.close()
await self.dispatcher.storage.wait_closed()
await self.dispatcher.bot.close()
await self.dispatcher.bot.session.close()
async def _startup_polling(self):
await self._welcome()

View file

@ -103,10 +103,7 @@ class HelperMode(Helper):
if symbol == '_' and pos > 0:
need_upper = True
else:
if need_upper:
result += symbol.upper()
else:
result += symbol.lower()
result += symbol.upper() if need_upper else symbol.lower()
need_upper = False
if first_upper:
result = result[0].upper() + result[1:]
@ -201,10 +198,14 @@ 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)
props_keys = [
prop_name
for prop_name in (
name
for name, prop in namespace.items()
if isinstance(prop, (Item, ListItem))
)
]
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)

View file

@ -15,12 +15,13 @@ def split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]:
return [text[i:i + length] for i in range(0, len(text), length)]
def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]:
def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH, split_separator: str = ' ') -> typing.List[str]:
"""
Split long text
:param text:
:param length:
:param split_separator
:return:
"""
# TODO: More informative description
@ -30,7 +31,7 @@ def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[
while temp_text:
if len(temp_text) > length:
try:
split_pos = temp_text[:length].rindex(' ')
split_pos = temp_text[:length].rindex(split_separator)
except ValueError:
split_pos = length
if split_pos < length // 4 * 3:

View file

@ -9,11 +9,11 @@ if TYPE_CHECKING: # pragma: no cover
from aiogram.types import MessageEntity
__all__ = (
"TextDecoration",
"HtmlDecoration",
"MarkdownDecoration",
"html_decoration",
"markdown_decoration",
'HtmlDecoration',
'MarkdownDecoration',
'TextDecoration',
'html_decoration',
'markdown_decoration',
)
@ -55,16 +55,15 @@ class TextDecoration(ABC):
:param entities: Array of MessageEntities
:return:
"""
result = "".join(
return "".join(
self._unparse_entities(
text, sorted(entities, key=lambda item: item.offset) if entities else []
self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else []
)
)
return result
def _unparse_entities(
self,
text: str,
text: bytes,
entities: List[MessageEntity],
offset: Optional[int] = None,
length: Optional[int] = None,
@ -74,15 +73,15 @@ class TextDecoration(ABC):
length = length or len(text)
for index, entity in enumerate(entities):
if entity.offset < offset:
if entity.offset * 2 < offset:
continue
if entity.offset > offset:
yield self.quote(text[offset : entity.offset])
start = entity.offset
offset = entity.offset + entity.length
if entity.offset * 2 > offset:
yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2]))
start = entity.offset * 2
offset = entity.offset * 2 + entity.length * 2
sub_entities = list(
filter(lambda e: e.offset < (offset or 0), entities[index + 1 :])
filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :])
)
yield self.apply_entity(
entity,
@ -94,7 +93,15 @@ class TextDecoration(ABC):
)
if offset < length:
yield self.quote(text[offset:length])
yield self.quote(self._remove_surrogates(text[offset:length]))
@staticmethod
def _add_surrogates(text: str):
return text.encode('utf-16-le')
@staticmethod
def _remove_surrogates(text: bytes):
return text.decode('utf-16-le')
@abstractmethod
def link(self, value: str, link: str) -> str: # pragma: no cover
@ -159,7 +166,7 @@ class HtmlDecoration(TextDecoration):
return f"<s>{value}</s>"
def quote(self, value: str) -> str:
return html.escape(value)
return html.escape(value, quote=False)
class MarkdownDecoration(TextDecoration):
@ -172,19 +179,19 @@ class MarkdownDecoration(TextDecoration):
return f"*{value}*"
def italic(self, value: str) -> str:
return f"_{value}_\r"
return f"_\r{value}_\r"
def code(self, value: str) -> str:
return f"`{value}`"
def pre(self, value: str) -> str:
return f"```{value}```"
return f"```\n{value}\n```"
def pre_language(self, value: str, language: str) -> str:
return f"```{language}\n{value}\n```"
def underline(self, value: str) -> str:
return f"__{value}__"
return f"__\r{value}__\r"
def strikethrough(self, value: str) -> str:
return f"~{value}~"

View file

@ -3,11 +3,10 @@
ujson>=1.35
python-rapidjson>=0.7.0
emoji>=0.5.2
pytest>=5.4
pytest>=6.2.1
pytest-asyncio>=0.10.0
tox>=3.9.0
aresponses>=1.1.1
uvloop>=0.12.2
aioredis>=1.2.0
wheel>=0.31.1
sphinx>=2.0.1
@ -16,3 +15,5 @@ sphinxcontrib-programoutput>=0.14
aiohttp-socks>=0.3.4
rethinkdb>=2.4.1
coverage==4.5.3
motor>=2.2.0
pytest-lazy-fixture==0.6.*

View file

@ -10,7 +10,7 @@ Filter factory greatly simplifies the reuse of filters when registering handlers
Filters factory
===============
.. autoclass:: aiogram.dispatcher.filters.factory.FiltersFactory
.. autoclass:: aiogram.dispatcher.filters.FiltersFactory
:members:
:show-inheritance:
@ -21,28 +21,28 @@ Builtin filters
Command
-------
.. autoclass:: aiogram.dispatcher.filters.builtin.Command
.. autoclass:: aiogram.dispatcher.filters.Command
:members:
:show-inheritance:
CommandStart
------------
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandStart
.. autoclass:: aiogram.dispatcher.filters.CommandStart
:members:
:show-inheritance:
CommandHelp
-----------
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandHelp
.. autoclass:: aiogram.dispatcher.filters.CommandHelp
:members:
:show-inheritance:
CommandSettings
---------------
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandSettings
.. autoclass:: aiogram.dispatcher.filters.CommandSettings
:members:
:show-inheritance:
@ -50,7 +50,7 @@ CommandSettings
CommandPrivacy
--------------
.. autoclass:: aiogram.dispatcher.filters.builtin.CommandPrivacy
.. autoclass:: aiogram.dispatcher.filters.CommandPrivacy
:members:
:show-inheritance:
@ -58,7 +58,7 @@ CommandPrivacy
Text
----
.. autoclass:: aiogram.dispatcher.filters.builtin.Text
.. autoclass:: aiogram.dispatcher.filters.Text
:members:
:show-inheritance:
@ -66,7 +66,7 @@ Text
HashTag
-------
.. autoclass:: aiogram.dispatcher.filters.builtin.HashTag
.. autoclass:: aiogram.dispatcher.filters.HashTag
:members:
:show-inheritance:
@ -74,7 +74,7 @@ HashTag
Regexp
------
.. autoclass:: aiogram.dispatcher.filters.builtin.Regexp
.. autoclass:: aiogram.dispatcher.filters.Regexp
:members:
:show-inheritance:
@ -82,7 +82,7 @@ Regexp
RegexpCommandsFilter
--------------------
.. autoclass:: aiogram.dispatcher.filters.builtin.RegexpCommandsFilter
.. autoclass:: aiogram.dispatcher.filters.RegexpCommandsFilter
:members:
:show-inheritance:
@ -90,21 +90,21 @@ RegexpCommandsFilter
ContentTypeFilter
-----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.ContentTypeFilter
.. autoclass:: aiogram.dispatcher.filters.ContentTypeFilter
:members:
:show-inheritance:
IsSenderContact
---------------
.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact
.. autoclass:: aiogram.dispatcher.filters.IsSenderContact
:members:
:show-inheritance:
StateFilter
-----------
.. autoclass:: aiogram.dispatcher.filters.builtin.StateFilter
.. autoclass:: aiogram.dispatcher.filters.StateFilter
:members:
:show-inheritance:
@ -112,13 +112,13 @@ StateFilter
ExceptionsFilter
----------------
.. autoclass:: aiogram.dispatcher.filters.builtin.ExceptionsFilter
.. autoclass:: aiogram.dispatcher.filters.ExceptionsFilter
:members:
:show-inheritance:
IDFilter
----------------
--------
.. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter
:members:
@ -126,9 +126,9 @@ IDFilter
AdminFilter
----------------
-----------
.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter
.. autoclass:: aiogram.dispatcher.filters.AdminFilter
:members:
:show-inheritance:
@ -136,23 +136,31 @@ AdminFilter
IsReplyFilter
-------------
.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter
.. autoclass:: aiogram.dispatcher.filters.IsReplyFilter
:members:
:show-inheritance:
ForwardedMessageFilter
-------------
----------------------
.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter
.. autoclass:: aiogram.dispatcher.filters.ForwardedMessageFilter
:members:
:show-inheritance:
ChatTypeFilter
--------------
.. autoclass:: aiogram.dispatcher.filters.ChatTypeFilter
:members:
:show-inheritance:
MediaGroupFilter
-------------
.. autoclass:: aiogram.dispatcher.filters.filters.ChatTypeFilter
.. autoclass:: aiogram.dispatcher.filters.MediaGroupFilter
:members:
:show-inheritance:
@ -170,19 +178,19 @@ Own filter can be:
AbstractFilter
--------------
.. autoclass:: aiogram.dispatcher.filters.filters.AbstractFilter
.. autoclass:: aiogram.dispatcher.filters.AbstractFilter
:members:
:show-inheritance:
Filter
------
.. autoclass:: aiogram.dispatcher.filters.filters.Filter
.. autoclass:: aiogram.dispatcher.filters.Filter
:members:
:show-inheritance:
BoundFilter
-----------
.. autoclass:: aiogram.dispatcher.filters.filters.BoundFilter
.. autoclass:: aiogram.dispatcher.filters.BoundFilter
:members:
:show-inheritance:

View file

@ -12,15 +12,29 @@ Coming soon...
Memory storage
~~~~~~~~~~~~~~
Coming soon...
.. autoclass:: aiogram.contrib.fsm_storage.memory.MemoryStorage
:show-inheritance:
Redis storage
~~~~~~~~~~~~~
Coming soon...
.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage
:show-inheritance:
Mongo storage
~~~~~~~~~~~~~
.. autoclass:: aiogram.contrib.fsm_storage.mongo.MongoStorage
:show-inheritance:
Rethink DB storage
~~~~~~~~~~~~~~~~~~
Coming soon...
.. autoclass:: aiogram.contrib.fsm_storage.rethinkdb.RethinkDBStorage
:show-inheritance:
Making own storage's
~~~~~~~~~~~~~~~~~~~~

View file

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

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.593501
=================
Broadcast example
=================

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.558059
===================
Check user language
===================
@ -10,4 +8,3 @@ Babel is required.
:caption: check_user_language.py
:language: python
:linenos:
:lines: 5-

View file

@ -1,8 +1,7 @@
========
Echo bot
========
Very simple example of the bot which will sent text of the received messages to the sender
.. literalinclude:: ../../../examples/echo_bot.py
:caption: echo_bot.py
:language: python

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.595032
============================
Finite state machine example
============================

View file

@ -1,28 +1,8 @@
.. Autogenerated file at 2018-09-08 02:07:37.591007
============
I18n example
============
Internalize your bot
Step 1: extract texts
# pybabel extract i18n_example.py -o locales/mybot.pot
Step 2: create *.po files. For e.g. create en, ru, uk locales.
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
Step 3: translate texts
Step 4: compile translations
# pybabel compile -d locales -D mybot
Step 5: When you change the code of your bot you need to update po & mo files.
Step 5.1: regenerate pot file:
command from step 1
Step 5.2: update po files
# pybabel update -d locales -D mybot -i locales/mybot.pot
Step 5.3: update your translations
Step 5.4: compile mo files
command from step 4
.. literalinclude:: ../../../examples/i18n_example.py
:caption: i18n_example.py
:language: python
:linenos:
:lines: 22-

View file

@ -19,3 +19,4 @@ Examples
payments
broadcast_example
media_group
local_server

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.561907
==========
Inline bot
==========

View file

@ -0,0 +1,8 @@
============
Local server
============
.. literalinclude:: ../../../examples/local_server.py
:caption: local_server.py
:language: python
:linenos:

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.566615
===========
Media group
===========

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.560132
========================
Middleware and antiflood
========================

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.579017
========
Payments
========

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.555359
=================
Proxy and emojize
=================

View file

@ -1,5 +1,3 @@
.. Autogenerated file at 2018-09-08 02:07:37.568530
==============================
Regexp commands filter example
==============================

View file

@ -1,14 +1,8 @@
.. Autogenerated file at 2018-09-08 02:07:37.563878
==================
Throttling example
==================
=================
Throtling example
=================
Example for throttling manager.
You can use that for flood controlling.
.. literalinclude:: ../../../examples/throtling_example.py
:caption: throtling_example.py
.. literalinclude:: ../../../examples/throttling_example.py
:caption: throttling_example.py
:language: python
:linenos:
:lines: 7-

View file

@ -1,13 +1,8 @@
.. Autogenerated file at 2018-10-28 19:31:48.341172
===============
Webhook example
===============
Example outdated
.. literalinclude:: ../../../examples/webhook_example.py
:caption: webhook_example.py
:language: python
:linenos:
:lines: 5-

View file

@ -1,10 +1,8 @@
.. Autogenerated file at 2018-09-08 02:07:37.576034
===================
Webhook example old
===================
=================
Webhook example 2
=================
.. literalinclude:: ../../../examples/webhook_example_2.py
.. literalinclude:: ../../../examples/webhook_example_old.py
:caption: webhook_example_2.py
:language: python
:linenos:

View file

@ -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.9-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -13,9 +13,13 @@ Using Pipenv
$ pipenv install aiogram
Using AUR
Using Pacman
---------
*aiogram* is also available in Arch User Repository, so you can install this framework on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install the `python-aiogram <https://aur.archlinux.org/packages/python-aiogram/>`_ package.
*aiogram* is also available in Arch Linux Repository, so you can install this framework on any Arch-based distribution like Arch Linux, Antergos, Manjaro, etc. To do this, just use pacman to install the `python-aiogram <https://archlinux.org/packages/community/any/python-aiogram/>`_ package:
.. code-block:: bash
$ pacman -S python-aiogram
From sources
------------
@ -28,7 +32,7 @@ From sources
$ cd aiogram
$ python setup.py install
Or if you want to install stable version (The same with version form PyPi):
Or if you want to install stable version (The same with version from PyPi):
.. code-block:: bash

View file

@ -24,17 +24,17 @@ Next step: interaction with bots starts with one command. Register your first co
:language: python
:lines: 20-25
If you want to handle all messages in the chat simply add handler without filters:
If you want to handle all text messages in the chat simply add handler without filters:
.. literalinclude:: ../../examples/echo_bot.py
:language: python
:lines: 35-37
:lines: 44-49
Last step: run long polling.
.. literalinclude:: ../../examples/echo_bot.py
:language: python
:lines: 40-41
:lines: 52-53
Summary
-------
@ -42,4 +42,4 @@ Summary
.. literalinclude:: ../../examples/echo_bot.py
:language: python
:linenos:
:lines: -19,27-
:lines: -27,43-

View file

@ -13,3 +13,4 @@ Utils
parts
json
emoji
deep_linking

View file

@ -113,7 +113,7 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: typing.Dict
@dp.errors_handler(exception=MessageNotModified)
async def message_not_modified_handler(update, error):
return True
return True # errors_handler must return True if error was handled correctly
if __name__ == '__main__':

View file

@ -62,7 +62,7 @@ async def callback_vote_action(query: types.CallbackQuery, callback_data: typing
@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises
async def message_not_modified_handler(update, error):
return True
return True # errors_handler must return True if error was handled correctly
if __name__ == '__main__':

View file

@ -19,12 +19,12 @@ bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.CHANNEL])
@dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.SUPERGROUP])
async def send_welcome(message: types.Message):
"""
This handler will be called when user sends `/start` or `/help` command
This handler will be called when user sends message in private chat or supergroup
"""
await message.reply("Hi!\nI'm hearing your messages in private chats and channels")
await message.reply("Hi!\nI'm hearing your messages in private chats and supergroups")
# propagate message to the next handler
raise SkipHandler
@ -33,7 +33,7 @@ async def send_welcome(message: types.Message):
@dp.message_handler(chat_type=ChatType.PRIVATE)
async def send_welcome(message: types.Message):
"""
This handler will be called when user sends `/start` or `/help` command
This handler will be called when user sends message in private chat
"""
await message.reply("Hi!\nI'm hearing your messages only in private chats")

View file

@ -1,8 +1,8 @@
"""
Internalize your bot
Internationalize your bot
Step 1: extract texts
# pybabel extract i18n_example.py -o locales/mybot.pot
# pybabel extract --input-dirs=. -o locales/mybot.pot
Some useful options:
- Extract texts with pluralization support
@ -16,9 +16,14 @@ Step 1: extract texts
- Set version
# --version=2.2
Step 2: create *.po files. For e.g. create en, ru, uk locales.
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
Step 3: translate texts
Step 2: create *.po files. E.g. create en, ru, uk locales.
# pybabel init -i locales/mybot.pot -d locales -D mybot -l en
# pybabel init -i locales/mybot.pot -d locales -D mybot -l ru
# pybabel init -i locales/mybot.pot -d locales -D mybot -l uk
Step 3: translate texts located in locales/{language}/LC_MESSAGES/mybot.po
To open .po file you can use basic text editor or any PO editor, e.g. https://poedit.net/
Step 4: compile translations
# pybabel compile -d locales -D mybot
@ -27,7 +32,8 @@ Step 5: When you change the code of your bot you need to update po & mo files.
command from step 1
Step 5.2: update po files
# pybabel update -d locales -D mybot -i locales/mybot.pot
Step 5.3: update your translations
Step 5.3: update your translations
location and tools you know from step 3
Step 5.4: compile mo files
command from step 4
"""
@ -92,5 +98,6 @@ async def cmd_like(message: types.Message, locale):
# NOTE: This is comment for a translator
await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes))
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

27
examples/local_server.py Normal file
View file

@ -0,0 +1,27 @@
import logging
from aiogram import Bot, Dispatcher, executor, types
from aiogram.bot.api import TelegramAPIServer
from aiogram.types import ContentType
API_TOKEN = 'BOT TOKEN HERE'
# Configure logging
logging.basicConfig(level=logging.INFO)
# Create private Bot API server endpoints wrapper
local_server = TelegramAPIServer.from_base('http://localhost')
# Initialize bot with using local server
bot = Bot(token=API_TOKEN, server=local_server)
# ... and dispatcher
dp = Dispatcher(bot)
@dp.message_handler(content_types=ContentType.ANY)
async def echo(message: types.Message):
await message.copy_to(message.chat.id)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

View file

@ -0,0 +1,34 @@
# NOTE: This is an example of an integration between
# externally created Application object and the aiogram's dispatcher
# This can be used for a custom route, for instance
from aiogram import Bot, Dispatcher, types
from aiogram.dispatcher.webhook import configure_app
from aiohttp import web
bot = Bot(token=config.bot_token)
dp = Dispatcher(bot)
@dp.message_handler(commands=["start"])
async def cmd_start(message: types.Message):
await message.reply("start!")
# handle /api route
async def api_handler(request):
return web.json_response({"status": "OK"}, status=200)
app = web.Application()
# add a custom route
app.add_routes([web.post('/api', api_handler)])
# every request to /bot route will be retransmitted to dispatcher to be handled
# as a bot update
configure_app(dp, app, "/bot")
if __name__ == '__main__':
web.run_app(app, port=9000)

Some files were not shown because too many files have changed in this diff Show more