mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-09 17:33:44 +00:00
Merge branch 'dev'
# Conflicts: # aiogram/__init__.py
This commit is contained in:
commit
4b5269aeab
19 changed files with 668 additions and 76 deletions
|
|
@ -1,6 +1,6 @@
|
|||
from .bot import Bot
|
||||
from .utils.versions import Version, Stage
|
||||
|
||||
VERSION = Version(0, 4, 1, stage=Stage.FINAL, build=0)
|
||||
VERSION = Version(0, 4, 2, stage=Stage.FINAL, build=0)
|
||||
|
||||
__version__ = VERSION.version
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from http import HTTPStatus
|
|||
import aiohttp
|
||||
|
||||
from ..utils import json
|
||||
from ..utils.exceptions import ValidationError, TelegramAPIError, BadRequest, Unauthorized, NetworkError, RetryAfter, \
|
||||
MigrateToChat, ConflictError
|
||||
from ..utils.exceptions import BadRequest, ConflictError, MigrateToChat, NetworkError, RetryAfter, TelegramAPIError, \
|
||||
Unauthorized, ValidationError
|
||||
from ..utils.helper import Helper, HelperMode, Item
|
||||
|
||||
# Main aiogram logger
|
||||
|
|
@ -129,13 +129,20 @@ async def request(session, token, method, data=None, files=None, continue_retry=
|
|||
|
||||
https://core.telegram.org/bots/api#making-requests
|
||||
|
||||
:param session: :class:`aiohttp.ClientSession`
|
||||
:param session: HTTP Client session
|
||||
:type session: :obj:`aiohttp.ClientSession`
|
||||
:param token: BOT token
|
||||
:type token: :obj:`str`
|
||||
:param method: API method
|
||||
:type method: :obj:`str`
|
||||
:param data: request payload
|
||||
:type data: :obj:`dict`
|
||||
:param files: files
|
||||
:type files: :obj:`dict`
|
||||
:param continue_retry:
|
||||
:return: bool or dict
|
||||
:type continue_retry: :obj:`dict`
|
||||
:return: result
|
||||
:rtype :obj:`bool` or :obj:`dict`
|
||||
"""
|
||||
log.debug("Make request: '{0}' with data: {1} and files {2}".format(
|
||||
method, data or {}, files or {}))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import io
|
||||
from typing import Union, TypeVar, List, Dict, Optional
|
||||
from typing import Dict, List, Optional, TypeVar, Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
|
@ -31,13 +31,19 @@ class BaseBot:
|
|||
Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot
|
||||
|
||||
:param token: token from @BotFather
|
||||
:type token: :obj:`str`
|
||||
:param loop: event loop
|
||||
:type loop: Optional Union :obj:`asyncio.BaseEventLoop`, :obj:`asyncio.AbstractEventLoop`
|
||||
:param connections_limit: connections limit for aiohttp.ClientSession
|
||||
:type connections_limit: :obj:`int`
|
||||
:param proxy: HTTP proxy URL
|
||||
:param proxy_auth: :obj:`aiohttp.BasicAuth`
|
||||
:type proxy: :obj:`str`
|
||||
:param proxy_auth: Authentication information
|
||||
:type proxy_auth: Optional :obj:`aiohttp.BasicAuth`
|
||||
:param continue_retry: automatic retry sent request when flood control exceeded
|
||||
:type continue_retry: :obj:`bool`
|
||||
:raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError`
|
||||
"""
|
||||
|
||||
self.__token = token
|
||||
self.proxy = proxy
|
||||
self.proxy_auth = proxy_auth
|
||||
|
|
@ -56,8 +62,6 @@ class BaseBot:
|
|||
def __del__(self):
|
||||
"""
|
||||
When bot object is deleting - need close all sessions
|
||||
|
||||
:return:
|
||||
"""
|
||||
for session in self._temp_sessions:
|
||||
if not session.closed:
|
||||
|
|
@ -65,15 +69,18 @@ class BaseBot:
|
|||
if self.session and not self.session.closed:
|
||||
self.session.close()
|
||||
|
||||
def create_temp_session(self) -> aiohttp.ClientSession:
|
||||
def create_temp_session(self, limit: int = 1) -> aiohttp.ClientSession:
|
||||
"""
|
||||
Create temporary session
|
||||
|
||||
:return:
|
||||
:param limit: Limit of connections
|
||||
:type limit: :obj:`int`
|
||||
:return: New session
|
||||
:rtype: :obj:`aiohttp.TCPConnector`
|
||||
"""
|
||||
session = aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(limit=1, force_close=True),
|
||||
loop=self.loop)
|
||||
connector=aiohttp.TCPConnector(limit=limit, force_close=True),
|
||||
loop=self.loop, json_serialize=json.dumps)
|
||||
self._temp_sessions.append(session)
|
||||
return session
|
||||
|
||||
|
|
@ -81,8 +88,8 @@ class BaseBot:
|
|||
"""
|
||||
Destroy temporary session
|
||||
|
||||
:param session:
|
||||
:return:
|
||||
:param session: target session
|
||||
:type session: :obj:`aiohttp.ClientSession`
|
||||
"""
|
||||
if not session.closed:
|
||||
session.close()
|
||||
|
|
@ -98,10 +105,14 @@ class BaseBot:
|
|||
https://core.telegram.org/bots/api#making-requests
|
||||
|
||||
:param method: API method
|
||||
:type method: :obj:`str`
|
||||
:param data: request parameters
|
||||
:type data: :obj:`dict`
|
||||
:param files: files
|
||||
:return: Union[List, Dict]
|
||||
:raise: :class:`aiogram.exceptions.TelegramApiError`
|
||||
:type files: :obj:`dict`
|
||||
:return: result
|
||||
:rtype: Union[List, Dict]
|
||||
:raise: :obj:`aiogram.exceptions.TelegramApiError`
|
||||
"""
|
||||
return await api.request(self.session, self.__token, method, data, files,
|
||||
proxy=self.proxy, proxy_auth=self.proxy_auth,
|
||||
|
|
@ -118,7 +129,8 @@ class BaseBot:
|
|||
if You want to automatically create destination (:class:`io.BytesIO`) use default
|
||||
value of destination and handle result of this method.
|
||||
|
||||
:param file_path: String
|
||||
:param file_path: file path on telegram server (You can get it from :obj:`aiogram.types.File`)
|
||||
:type file_path: :obj:`str`
|
||||
:param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||
:param timeout: Integer
|
||||
:param chunk_size: Integer
|
||||
|
|
@ -129,7 +141,7 @@ class BaseBot:
|
|||
destination = io.BytesIO()
|
||||
|
||||
session = self.create_temp_session()
|
||||
url = api.FILE_URL.format(token=self.__token, path=file_path)
|
||||
url = api.Methods.file_url(token=self.__token, path=file_path)
|
||||
|
||||
dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb')
|
||||
try:
|
||||
|
|
@ -153,10 +165,10 @@ class BaseBot:
|
|||
https://core.telegram.org/bots/api#inputfile
|
||||
|
||||
:param file_type: field name
|
||||
:param method: API metod
|
||||
:param method: API method
|
||||
:param file: String or io.IOBase
|
||||
:param payload: request payload
|
||||
:return: resonse
|
||||
:return: response
|
||||
"""
|
||||
if file is None:
|
||||
files = {}
|
||||
|
|
@ -164,8 +176,6 @@ class BaseBot:
|
|||
# You can use file ID or URL in the most of requests
|
||||
payload[file_type] = file
|
||||
files = None
|
||||
elif isinstance(file, (io.IOBase, io.FileIO)):
|
||||
files = {file_type: file.read()}
|
||||
else:
|
||||
files = {file_type: file}
|
||||
|
||||
|
|
@ -435,7 +445,8 @@ class BaseBot:
|
|||
disable_notification: Optional[Boolean] = None,
|
||||
reply_to_message_id: Optional[Integer] = None,
|
||||
reply_markup: Optional[Union[
|
||||
types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None) -> Dict:
|
||||
types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None,
|
||||
filename: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
Use this method to send general files. On success, the sent Message is returned.
|
||||
Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future.
|
||||
|
|
@ -456,10 +467,13 @@ class BaseBot:
|
|||
:param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional)
|
||||
- Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
|
||||
:param filename: Set file name
|
||||
:return: On success, the sent Message is returned.
|
||||
"""
|
||||
reply_markup = prepare_arg(reply_markup)
|
||||
payload = generate_payload(**locals(), exclude=['document'])
|
||||
if filename:
|
||||
document = (filename, document)
|
||||
payload = generate_payload(**locals(), exclude=['document', 'filename'])
|
||||
|
||||
return await self.send_file('document', api.Methods.SEND_DOCUMENT, document, payload)
|
||||
|
||||
|
|
|
|||
|
|
@ -272,7 +272,8 @@ class Bot(BaseBot):
|
|||
reply_to_message_id: Optional[Integer] = None,
|
||||
reply_markup: Optional[Union[
|
||||
types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup, Dict, String]] = None) -> types.Message:
|
||||
types.ReplyKeyboardMarkup, Dict, String]] = None,
|
||||
filename: Optional[str] = None) -> types.Message:
|
||||
"""
|
||||
Use this method to send general files. On success, the sent Message is returned.
|
||||
Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future.
|
||||
|
|
@ -293,12 +294,14 @@ class Bot(BaseBot):
|
|||
:param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional)
|
||||
- Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
|
||||
:param filename: Set file name
|
||||
:return: On success, the sent Message is returned. (serialized)
|
||||
"""
|
||||
raw = super(Bot, self).send_document(chat_id=chat_id, document=document, caption=caption,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
reply_markup=reply_markup)
|
||||
reply_markup=reply_markup,
|
||||
filename=filename)
|
||||
return self.prepare_object(types.Message.deserialize(await raw))
|
||||
|
||||
async def send_video(self, chat_id: Union[Integer, String],
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class RedisStorage(BaseStorage):
|
|||
:return:
|
||||
"""
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
addr = f"{chat}:{user}"
|
||||
addr = f"fsm:{chat}:{user}"
|
||||
|
||||
conn = await self.redis
|
||||
data = await conn.execute('GET', addr)
|
||||
|
|
@ -104,7 +104,7 @@ class RedisStorage(BaseStorage):
|
|||
data = {}
|
||||
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
addr = f"{chat}:{user}"
|
||||
addr = f"fsm:{chat}:{user}"
|
||||
|
||||
record = {'state': state, 'data': data}
|
||||
|
||||
|
|
@ -138,3 +138,19 @@ class RedisStorage(BaseStorage):
|
|||
data = []
|
||||
data.update(data, **kwargs)
|
||||
await self.set_data(chat=chat, user=user, data=data)
|
||||
|
||||
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
|
||||
"""
|
||||
Get list of all stored chat's and user's
|
||||
|
||||
:return: list of tuples where first element is chat id and second is user id
|
||||
"""
|
||||
conn = await self.redis
|
||||
result = []
|
||||
|
||||
keys = await conn.execute('KEYS', 'fsm:*')
|
||||
for item in keys:
|
||||
*_, chat, user = item.decode('utf-8').split(':')
|
||||
result.append((chat, user))
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@ import functools
|
|||
import logging
|
||||
import typing
|
||||
|
||||
from .filters import CommandsFilter, RegexpFilter, ContentTypeFilter, generate_default_filters
|
||||
from .filters import CommandsFilter, ContentTypeFilter, RegexpFilter, USER_STATE, generate_default_filters, \
|
||||
ExceptionsFilter
|
||||
from .handler import Handler
|
||||
from .storage import DisabledStorage, BaseStorage, FSMContext
|
||||
from .storage import BaseStorage, DisabledStorage, FSMContext
|
||||
from .webhook import BaseResponse
|
||||
from ..bot import Bot
|
||||
from ..types.message import ContentType
|
||||
from ..utils.exceptions import TelegramAPIError, NetworkError
|
||||
from ..utils import context
|
||||
from ..utils.exceptions import NetworkError, TelegramAPIError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MODE = 'MODE'
|
||||
LONG_POOLING = 'long-pooling'
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
"""
|
||||
|
|
@ -48,6 +53,8 @@ class Dispatcher:
|
|||
|
||||
self.updates_handler.register(self.process_update)
|
||||
|
||||
self.errors_handlers = Handler(self, once=False)
|
||||
|
||||
self._pooling = False
|
||||
|
||||
def __del__(self):
|
||||
|
|
@ -79,7 +86,7 @@ class Dispatcher:
|
|||
"""
|
||||
tasks = []
|
||||
for update in updates:
|
||||
tasks.append(self.updates_handler.notify(update))
|
||||
tasks.append(self.process_update(update))
|
||||
return await asyncio.gather(*tasks)
|
||||
|
||||
async def process_update(self, update):
|
||||
|
|
@ -89,25 +96,64 @@ class Dispatcher:
|
|||
:param update:
|
||||
:return:
|
||||
"""
|
||||
self.last_update_id = update.update_id
|
||||
if update.message:
|
||||
return await self.message_handlers.notify(update.message)
|
||||
if update.edited_message:
|
||||
return await self.edited_message_handlers.notify(update.edited_message)
|
||||
if update.channel_post:
|
||||
return await self.channel_post_handlers.notify(update.channel_post)
|
||||
if update.edited_channel_post:
|
||||
return await self.edited_channel_post_handlers.notify(update.edited_channel_post)
|
||||
if update.inline_query:
|
||||
return await self.inline_query_handlers.notify(update.inline_query)
|
||||
if update.chosen_inline_result:
|
||||
return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result)
|
||||
if update.callback_query:
|
||||
return await self.callback_query_handlers.notify(update.callback_query)
|
||||
if update.shipping_query:
|
||||
return await self.shipping_query_handlers.notify(update.shipping_query)
|
||||
if update.pre_checkout_query:
|
||||
return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query)
|
||||
try:
|
||||
self.last_update_id = update.update_id
|
||||
has_context = context.check_configured()
|
||||
if update.message:
|
||||
if has_context:
|
||||
state = self.storage.get_state(chat=update.message.chat.id,
|
||||
user=update.message.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.message_handlers.notify(update.message)
|
||||
if update.edited_message:
|
||||
if has_context:
|
||||
state = self.storage.get_state(chat=update.edited_message.chat.id,
|
||||
user=update.edited_message.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.edited_message_handlers.notify(update.edited_message)
|
||||
if update.channel_post:
|
||||
if has_context:
|
||||
state = self.storage.get_state(chat=update.message.chat.id,
|
||||
user=update.message.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.channel_post_handlers.notify(update.channel_post)
|
||||
if update.edited_channel_post:
|
||||
if has_context:
|
||||
state = self.storage.get_state(chat=update.edited_channel_post.chat.id,
|
||||
user=update.edited_channel_post.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.edited_channel_post_handlers.notify(update.edited_channel_post)
|
||||
if update.inline_query:
|
||||
if has_context:
|
||||
state = self.storage.get_state(user=update.inline_query.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.inline_query_handlers.notify(update.inline_query)
|
||||
if update.chosen_inline_result:
|
||||
if has_context:
|
||||
state = self.storage.get_state(user=update.chosen_inline_result.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result)
|
||||
if update.callback_query:
|
||||
if has_context:
|
||||
state = self.storage.get_state(chat=update.callback_query.message.chat.id,
|
||||
user=update.callback_query.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.callback_query_handlers.notify(update.callback_query)
|
||||
if update.shipping_query:
|
||||
if has_context:
|
||||
state = self.storage.get_state(user=update.shipping_query.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.shipping_query_handlers.notify(update.shipping_query)
|
||||
if update.pre_checkout_query:
|
||||
if has_context:
|
||||
state = self.storage.get_state(user=update.pre_checkout_query.from_user.id)
|
||||
context.set_value(USER_STATE, await state)
|
||||
return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query)
|
||||
except Exception as e:
|
||||
err = await self.errors_handlers.notify(self, update, e)
|
||||
if err:
|
||||
return err
|
||||
raise
|
||||
|
||||
async def start_pooling(self, timeout=20, relax=0.1, limit=None):
|
||||
"""
|
||||
|
|
@ -121,6 +167,7 @@ class Dispatcher:
|
|||
if self._pooling:
|
||||
raise RuntimeError('Pooling already started')
|
||||
log.info('Start pooling.')
|
||||
context.set_value(MODE, LONG_POOLING)
|
||||
|
||||
self._pooling = True
|
||||
offset = None
|
||||
|
|
@ -150,12 +197,11 @@ class Dispatcher:
|
|||
:param updates: list of updates.
|
||||
"""
|
||||
need_to_call = []
|
||||
for update in await self.process_updates(updates):
|
||||
for responses in update:
|
||||
for response in responses:
|
||||
if not isinstance(response, BaseResponse):
|
||||
continue
|
||||
need_to_call.append(response.execute_response(self.bot))
|
||||
for response in await self.process_updates(updates):
|
||||
for response in response:
|
||||
if not isinstance(response, BaseResponse):
|
||||
continue
|
||||
need_to_call.append(response.execute_response(self.bot))
|
||||
if need_to_call:
|
||||
try:
|
||||
asyncio.gather(*need_to_call)
|
||||
|
|
@ -709,6 +755,35 @@ class Dispatcher:
|
|||
|
||||
return decorator
|
||||
|
||||
def register_errors_handler(self, callback, *, func=None, exception=None):
|
||||
"""
|
||||
Register errors handler
|
||||
|
||||
:param callback:
|
||||
:param func:
|
||||
:param exception: you can make handler for specific errors type
|
||||
"""
|
||||
filters_set = []
|
||||
if func is not None:
|
||||
filters_set.append(func)
|
||||
if exception is not None:
|
||||
filters_set.append(ExceptionsFilter(exception))
|
||||
self.errors_handlers.register(callback, filters_set)
|
||||
|
||||
def errors_handler(self, *, func=None, exception=None):
|
||||
"""
|
||||
Decorator for registering errors handler
|
||||
|
||||
:param func:
|
||||
:param exception: you can make handler for specific errors type
|
||||
:return:
|
||||
"""
|
||||
def decorator(callback):
|
||||
self.register_errors_handler(callback, func=func, exception=exception)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
def current_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None) -> FSMContext:
|
||||
|
|
@ -730,6 +805,7 @@ class Dispatcher:
|
|||
:param func:
|
||||
:return:
|
||||
"""
|
||||
|
||||
def process_response(task):
|
||||
response = task.result()
|
||||
self.loop.create_task(response.execute_response(self.bot))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import inspect
|
||||
import re
|
||||
|
||||
from aiogram.utils import context
|
||||
from ..utils.helper import Helper, HelperMode, Item
|
||||
|
||||
USER_STATE = 'USER_STATE'
|
||||
|
||||
|
||||
async def check_filter(filter_, args, kwargs):
|
||||
if not callable(filter_):
|
||||
|
|
@ -102,10 +105,14 @@ class StateFilter(AsyncFilter):
|
|||
if self.state == '*':
|
||||
return True
|
||||
|
||||
chat, user = self.get_target(obj)
|
||||
if context.check_value(USER_STATE):
|
||||
context_state = context.get_value(USER_STATE)
|
||||
return self.state == context_state
|
||||
else:
|
||||
chat, user = self.get_target(obj)
|
||||
|
||||
if chat or user:
|
||||
return await self.dispatcher.storage.get_state(chat=chat, user=user) == self.state
|
||||
if chat or user:
|
||||
return await self.dispatcher.storage.get_state(chat=chat, user=user) == self.state
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -118,6 +125,19 @@ class StatesListFilter(StateFilter):
|
|||
return False
|
||||
|
||||
|
||||
class ExceptionsFilter(Filter):
|
||||
def __init__(self, exception):
|
||||
self.exception = exception
|
||||
|
||||
def check(self, dispatcher, update, exception):
|
||||
try:
|
||||
raise exception
|
||||
except self.exception:
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def generate_default_filters(dispatcher, *args, **kwargs):
|
||||
filters_set = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from aiogram.utils.deprecated import deprecated
|
||||
from . import Handler
|
||||
from .handler import SkipHandler
|
||||
|
||||
|
||||
@deprecated
|
||||
class Middleware:
|
||||
def __init__(self, handler, filters=None):
|
||||
self.handler: Handler = handler
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import asyncio.tasks
|
|||
import datetime
|
||||
import functools
|
||||
import typing
|
||||
from typing import Union, Dict, Optional
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .. import types
|
||||
from ..bot import api
|
||||
from ..bot.base import Integer, String, Boolean, Float
|
||||
from ..bot.base import Boolean, Float, Integer, String
|
||||
from ..utils import context
|
||||
from ..utils import json
|
||||
from ..utils.deprecated import warn_deprecated as warn
|
||||
from ..utils.exceptions import TimeoutWarning
|
||||
|
|
@ -20,6 +21,10 @@ BOT_DISPATCHER_KEY = 'BOT_DISPATCHER'
|
|||
|
||||
RESPONSE_TIMEOUT = 55
|
||||
|
||||
WEBHOOK = 'webhook'
|
||||
WEBHOOK_CONNECTION = 'WEBHOOK_CONNECTION'
|
||||
WEBHOOK_REQUEST = 'WEBHOOK_REQUEST'
|
||||
|
||||
|
||||
class WebhookRequestHandler(web.View):
|
||||
"""
|
||||
|
|
@ -71,6 +76,11 @@ class WebhookRequestHandler(web.View):
|
|||
|
||||
:return: :class:`aiohttp.web.Response`
|
||||
"""
|
||||
|
||||
context.update_state({'CALLER': WEBHOOK,
|
||||
WEBHOOK_CONNECTION: True,
|
||||
WEBHOOK_REQUEST: self.request})
|
||||
|
||||
dispatcher = self.get_dispatcher()
|
||||
update = await self.parse_update(dispatcher.bot)
|
||||
|
||||
|
|
@ -113,6 +123,7 @@ 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)
|
||||
finally:
|
||||
|
|
@ -253,7 +264,7 @@ class ReplyToMixin:
|
|||
class SendMessage(BaseResponse, ReplyToMixin):
|
||||
"""
|
||||
You can send message with webhook by using this instance of this object.
|
||||
All arguments is equal with :method:`Bot.send_message` method.
|
||||
All arguments is equal with Bot.send_message method.
|
||||
"""
|
||||
|
||||
__slots__ = ('chat_id', 'text', 'parse_mode',
|
||||
|
|
|
|||
115
aiogram/utils/context.py
Normal file
115
aiogram/utils/context.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""
|
||||
Need setup task factory:
|
||||
>>> from aiogram.utils import context
|
||||
>>> loop = asyncio.get_event_loop()
|
||||
>>> loop.set_task_factory(context.task_factory)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import typing
|
||||
|
||||
CONFIGURED = '@CONFIGURED_TASK_FACTORY'
|
||||
|
||||
|
||||
def task_factory(loop: asyncio.BaseEventLoop, coro: typing.Coroutine):
|
||||
"""
|
||||
Task factory for implementing context processor
|
||||
|
||||
:param loop:
|
||||
:param coro:
|
||||
:return: new task
|
||||
:rtype: :obj:`asyncio.Task`
|
||||
"""
|
||||
# Is not allowed when loop is closed.
|
||||
loop._check_closed()
|
||||
|
||||
task = asyncio.Task(coro, loop=loop)
|
||||
|
||||
# Hide factory
|
||||
if task._source_traceback:
|
||||
del task._source_traceback[-1]
|
||||
|
||||
try:
|
||||
task.context = asyncio.Task.current_task().context
|
||||
except AttributeError:
|
||||
task.context = {CONFIGURED: True}
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def get_current_state() -> typing.Dict:
|
||||
"""
|
||||
Get current execution context from task
|
||||
|
||||
:return: context
|
||||
:rtype: :obj:`dict`
|
||||
"""
|
||||
task = asyncio.Task.current_task()
|
||||
context = getattr(task, 'context', None)
|
||||
if context is None:
|
||||
context = task.context = {}
|
||||
return context
|
||||
|
||||
|
||||
def get_value(key, default=None):
|
||||
"""
|
||||
Get value from task
|
||||
|
||||
:param key:
|
||||
:param default:
|
||||
:return: value
|
||||
"""
|
||||
return get_current_state().get(key, default)
|
||||
|
||||
|
||||
def check_value(key):
|
||||
"""
|
||||
Key in context?
|
||||
|
||||
:param key:
|
||||
:return:
|
||||
"""
|
||||
return key in get_current_state()
|
||||
|
||||
|
||||
def set_value(key, value):
|
||||
"""
|
||||
Set value
|
||||
|
||||
:param key:
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
get_current_state()[key] = value
|
||||
|
||||
|
||||
def del_value(key):
|
||||
"""
|
||||
Remove value from context
|
||||
|
||||
:param key:
|
||||
:return:
|
||||
"""
|
||||
del get_current_state()[key]
|
||||
|
||||
|
||||
def update_state(data=None, **kwargs):
|
||||
"""
|
||||
Update multiple state items
|
||||
|
||||
:param data:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
state = get_current_state()
|
||||
state.update(data, **kwargs)
|
||||
|
||||
|
||||
def check_configured():
|
||||
"""
|
||||
Check loop is configured
|
||||
:return:
|
||||
"""
|
||||
return get_value(CONFIGURED)
|
||||
77
aiogram/utils/executor.py
Normal file
77
aiogram/utils/executor.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from aiogram.bot.api import log
|
||||
from aiogram.dispatcher import Dispatcher
|
||||
from aiogram.dispatcher.webhook import BOT_DISPATCHER_KEY, get_new_configured_app
|
||||
from aiogram.utils import context
|
||||
|
||||
|
||||
async def _startup(dispatcher: Dispatcher, skip_updates=False, callback=None):
|
||||
user = await dispatcher.bot.me
|
||||
log.info(f"Bot: {user.full_name} [@{user.username}]")
|
||||
|
||||
if callable(callback):
|
||||
await callback(dispatcher)
|
||||
|
||||
if skip_updates:
|
||||
count = await dispatcher.skip_updates()
|
||||
if count:
|
||||
log.warning(f"Skipped {count} updates.")
|
||||
|
||||
|
||||
async def _wh_startup(app):
|
||||
callback = app.get('_startup_callback', None)
|
||||
dispatcher = app.get(BOT_DISPATCHER_KEY, None)
|
||||
skip_updates = app.get('_skip_updates', False)
|
||||
await _startup(dispatcher, skip_updates=skip_updates, callback=callback)
|
||||
|
||||
|
||||
async def _shutdown(dispatcher: Dispatcher, callback=None):
|
||||
if callable(callback):
|
||||
await callback(dispatcher)
|
||||
|
||||
dispatcher.storage.close()
|
||||
await dispatcher.storage.wait_closed()
|
||||
|
||||
|
||||
async def _wh_shutdown(app):
|
||||
callback = app.get('_shutdown_callback', None)
|
||||
dispatcher = app.get(BOT_DISPATCHER_KEY, None)
|
||||
await _shutdown(dispatcher, callback=callback)
|
||||
|
||||
|
||||
def start_pooling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, on_shutdown=None):
|
||||
log.warning('Start bot with long-pooling.')
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
loop.set_task_factory(context.task_factory)
|
||||
|
||||
loop.create_task(dispatcher.start_pooling())
|
||||
try:
|
||||
loop.run_until_complete(_startup(dispatcher, skip_updates=skip_updates, callback=on_startup))
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
loop.run_until_complete(_shutdown(dispatcher, callback=on_shutdown))
|
||||
log.warning("Goodbye!")
|
||||
|
||||
|
||||
def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None,
|
||||
**kwargs):
|
||||
log.warning('Start bot with webhook.')
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
app = get_new_configured_app(dispatcher, webhook_path)
|
||||
app['_startup_callback'] = on_startup
|
||||
app['_shutdown_callback'] = on_shutdown
|
||||
app['_skip_updates'] = skip_updates
|
||||
|
||||
app.on_startup.append(_wh_startup)
|
||||
app.on_shutdown.append(_wh_shutdown)
|
||||
|
||||
web.run_app(app, loop=loop, **kwargs)
|
||||
|
|
@ -11,6 +11,13 @@ MD_SYMBOLS = (
|
|||
('<pre>', '</pre>'),
|
||||
)
|
||||
|
||||
HTML_QUOTES_MAP = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
}
|
||||
|
||||
|
||||
def _join(*content, sep=' '):
|
||||
return sep.join(map(str, content))
|
||||
|
|
@ -27,6 +34,22 @@ def _md(string, symbols=('', '')):
|
|||
return start + string + end
|
||||
|
||||
|
||||
def quote_html(content):
|
||||
"""
|
||||
Quote HTML symbols
|
||||
|
||||
All <, > and & symbols that are not a part of a tag or an HTML entity
|
||||
must be replaced with the corresponding HTML entities (< with <, > with > and & with &).
|
||||
|
||||
:param content: str
|
||||
:return: str
|
||||
"""
|
||||
new_content = ''
|
||||
for symbol in content:
|
||||
new_content += HTML_QUOTES_MAP[symbol] if symbol in '<>&"' else symbol
|
||||
return new_content
|
||||
|
||||
|
||||
def text(*content, sep=' '):
|
||||
"""
|
||||
Join all elements with separator
|
||||
|
|
@ -57,7 +80,7 @@ def hbold(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[4])
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[4])
|
||||
|
||||
|
||||
def italic(*content, sep=' '):
|
||||
|
|
@ -79,7 +102,7 @@ def hitalic(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[5])
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[5])
|
||||
|
||||
|
||||
def code(*content, sep=' '):
|
||||
|
|
@ -101,7 +124,7 @@ def hcode(*content, sep=' '):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[6])
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[6])
|
||||
|
||||
|
||||
def pre(*content, sep='\n'):
|
||||
|
|
@ -123,7 +146,7 @@ def hpre(*content, sep='\n'):
|
|||
:param sep:
|
||||
:return:
|
||||
"""
|
||||
return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[7])
|
||||
return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[7])
|
||||
|
||||
|
||||
def link(title, url):
|
||||
|
|
@ -134,7 +157,7 @@ def link(title, url):
|
|||
:param url:
|
||||
:return:
|
||||
"""
|
||||
return "[{0}]({1})".format(_escape(title), url)
|
||||
return "[{0}]({1})".format(title, url)
|
||||
|
||||
|
||||
def hlink(title, url):
|
||||
|
|
@ -145,7 +168,7 @@ def hlink(title, url):
|
|||
:param url:
|
||||
:return:
|
||||
"""
|
||||
return "<a href=\"{0}\">{1}</a>".format(url, _escape(title))
|
||||
return "<a href=\"{0}\">{1}</a>".format(url, quote_html(title))
|
||||
|
||||
|
||||
def escape_md(*content, sep=' '):
|
||||
|
|
|
|||
59
aiogram/utils/parts.py
Normal file
59
aiogram/utils/parts.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import typing
|
||||
|
||||
MAX_MESSAGE_LENGTH = 4096
|
||||
|
||||
|
||||
def split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]:
|
||||
"""
|
||||
Split long text
|
||||
|
||||
:param text:
|
||||
:param length:
|
||||
:return: list of parts
|
||||
:rtype: :obj:`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]:
|
||||
"""
|
||||
Split long text
|
||||
|
||||
:param text:
|
||||
:param length:
|
||||
:return:
|
||||
"""
|
||||
# TODO: More informative description
|
||||
|
||||
temp_text = text
|
||||
parts = []
|
||||
while temp_text:
|
||||
if len(temp_text) > length:
|
||||
try:
|
||||
split_pos = temp_text[:length].rindex(' ')
|
||||
except ValueError:
|
||||
split_pos = length
|
||||
if split_pos < length // 4 * 3:
|
||||
split_pos = length
|
||||
parts.append(temp_text[:split_pos])
|
||||
temp_text = temp_text[split_pos:].lstrip()
|
||||
else:
|
||||
parts.append(temp_text)
|
||||
break
|
||||
return parts
|
||||
|
||||
|
||||
def paginate(data: typing.Iterable, page: int = 0, limit: int = 10) -> typing.Iterable:
|
||||
"""
|
||||
Slice data over pages
|
||||
|
||||
:param data: any iterable object
|
||||
:type data: :obj:`typing.Iterable`
|
||||
:param page: number of page
|
||||
:type page: :obj:`int`
|
||||
:param limit: items per page
|
||||
:type limit: :obj:`int`
|
||||
:return: sliced object
|
||||
:rtype: :obj:`typing.Iterable`
|
||||
"""
|
||||
return data[page * limit:page * limit + limit]
|
||||
|
|
@ -4,3 +4,5 @@ BaseBot
|
|||
This class is base of bot. In BaseBot implemented all available methods of Telegram Bot API.
|
||||
|
||||
.. autoclass:: aiogram.bot.base.BaseBot
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ Bot object
|
|||
That is extended (and recommended for usage) bot class based on BaseBot class.
|
||||
You can use instance of that bot in :obj:`aiogram.dispatcher.Dispatcher`
|
||||
|
||||
.. autoclass:: aiogram.bot.bot.Bot
|
||||
.. autoclass:: aiogram.bot.bot.Bot
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
name: py36
|
||||
channels:
|
||||
- conda-forge
|
||||
- default
|
||||
dependencies:
|
||||
- python=3.6
|
||||
- sphinx=1.5.3
|
||||
|
|
|
|||
133
examples/adwanced_executor_example.py
Normal file
133
examples/adwanced_executor_example.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
II this example used ArgumentParser for configuring Your bot.
|
||||
|
||||
Provided to start bot with webhook:
|
||||
python adwanced_executor_example.py \
|
||||
--token TOKEN_HERE \
|
||||
--host 0.0.0.0 \
|
||||
--port 8084 \
|
||||
--host-name example.com \
|
||||
--webhook-port 443
|
||||
|
||||
Or long pooling:
|
||||
python adwanced_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.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher import Dispatcher
|
||||
from aiogram.dispatcher.webhook import *
|
||||
from aiogram.utils.executor import start_pooling, start_webhook
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Configure arguments parser.
|
||||
parser = argparse.ArgumentParser(description='Python telegram bot')
|
||||
parser.add_argument('--token', '-t', nargs='?', type=str, default=None, help='Set working directory')
|
||||
parser.add_argument('--sock', help='UNIX Socket path')
|
||||
parser.add_argument('--host', help='Webserver host')
|
||||
parser.add_argument('--port', type=int, help='Webserver port')
|
||||
parser.add_argument('--cert', help='Path to SSL certificate')
|
||||
parser.add_argument('--pkey', help='Path to SSL private key')
|
||||
parser.add_argument('--host-name', help='Set webhook host name')
|
||||
parser.add_argument('--webhook-port', type=int, help='Port for webhook (default=port)')
|
||||
parser.add_argument('--webhook-path', default='/webhook', help='Port for webhook (default=port)')
|
||||
|
||||
|
||||
async def cmd_start(message: types.Message):
|
||||
return SendMessage(message.chat.id, f"Hello, {message.from_user.full_name}!")
|
||||
|
||||
|
||||
def setup_handlers(dispatcher: Dispatcher):
|
||||
# This example has only one messages handler
|
||||
dispatcher.register_message_handler(cmd_start, commands=['start', 'welcome'])
|
||||
|
||||
|
||||
async def on_startup(dispatcher, url=None, cert=None):
|
||||
setup_handlers(dispatcher)
|
||||
|
||||
bot = dispatcher.bot
|
||||
|
||||
# Get current webhook status
|
||||
webhook = await bot.get_webhook_info()
|
||||
|
||||
if url:
|
||||
# If URL is bad
|
||||
if webhook.url != url:
|
||||
# If URL doesnt match with by current remove webhook
|
||||
if not webhook.url:
|
||||
await bot.delete_webhook()
|
||||
|
||||
# Set new URL for webhook
|
||||
if cert:
|
||||
with open(cert, 'rb') as cert_file:
|
||||
await bot.set_webhook(url, certificate=cert_file)
|
||||
else:
|
||||
await bot.set_webhook(url)
|
||||
elif webhook.url:
|
||||
# Otherwise remove webhook.
|
||||
await bot.delete_webhook()
|
||||
|
||||
|
||||
async def on_shutdown(dispatcher):
|
||||
print('Shutdown.')
|
||||
|
||||
|
||||
def main(arguments):
|
||||
args = parser.parse_args(arguments)
|
||||
token = args.token
|
||||
sock = args.sock
|
||||
host = args.host
|
||||
port = args.port
|
||||
cert = args.cert
|
||||
pkey = args.pkey
|
||||
host_name = args.host_name or host
|
||||
webhook_port = args.webhook_port or port
|
||||
webhook_path = args.webhook_path
|
||||
|
||||
# Fi webhook path
|
||||
if not webhook_path.startswith('/'):
|
||||
webhook_path = '/' + webhook_path
|
||||
|
||||
# Generate webhook URL
|
||||
webhook_url = f"https://{host_name}:{webhook_port}{webhook_path}"
|
||||
|
||||
# Create bot & dispatcher instances.
|
||||
bot = Bot(token)
|
||||
dispatcher = Dispatcher(bot)
|
||||
|
||||
if (sock or host) and host_name:
|
||||
if cert and pkey:
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||
ssl_context.load_cert_chain(cert, pkey)
|
||||
else:
|
||||
ssl_context = None
|
||||
|
||||
start_webhook(dispatcher, webhook_path,
|
||||
on_startup=functools.partial(on_startup, url=webhook_url, cert=cert),
|
||||
on_shutdown=on_shutdown,
|
||||
host=host, port=port, path=sock, ssl_context=ssl_context)
|
||||
else:
|
||||
start_pooling(dispatcher, on_startup=on_startup, on_shutdown=on_shutdown)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argv = sys.argv[1:]
|
||||
|
||||
if not len(argv):
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
main(argv)
|
||||
|
|
@ -41,3 +41,7 @@ if __name__ == '__main__':
|
|||
loop.run_until_complete(main())
|
||||
except KeyboardInterrupt:
|
||||
loop.stop()
|
||||
|
||||
# Also you can use another execution method
|
||||
# >>> from aiogram.utils.executor import start_pooling
|
||||
# >>> start_pooling(dp, loop=loop, on_startup=main, on_shutdown=shutdown)
|
||||
|
|
|
|||
35
setup.py
Normal file → Executable file
35
setup.py
Normal file → Executable file
|
|
@ -1,15 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import string
|
||||
from distutils.core import setup
|
||||
|
||||
from setuptools import PackageFinder
|
||||
|
||||
from aiogram import __version__ as version
|
||||
|
||||
ALLOWED_SYMBOLS = string.ascii_letters + string.digits + '_-'
|
||||
|
||||
|
||||
def get_description():
|
||||
"""
|
||||
Read full description from 'README.rst'
|
||||
|
||||
:return: description
|
||||
:rtype: str
|
||||
"""
|
||||
with open('README.rst', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def get_requirements():
|
||||
"""
|
||||
Read requirements from 'requirements txt'
|
||||
|
||||
:return: requirements
|
||||
:rtype: list
|
||||
"""
|
||||
requirements = []
|
||||
with open('requirements.txt', 'r') as file:
|
||||
for line in file.readlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
requirements.append(line)
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
setup(
|
||||
name='aiogram',
|
||||
version=version,
|
||||
|
|
@ -18,15 +47,15 @@ setup(
|
|||
license='MIT',
|
||||
author='Alex Root Junior',
|
||||
author_email='jroot.junior@gmail.com',
|
||||
description='Telegram bot API framework based on asyncio',
|
||||
description='Is are pretty simple and fully asynchronously library for Telegram Bot API',
|
||||
long_description=get_description(),
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Environment :: Console',
|
||||
'Framework :: AsyncIO',
|
||||
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
install_requires=['aiohttp']
|
||||
install_requires=get_requirements()
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue