mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-11 09:55:21 +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 .bot import Bot
|
||||||
from .utils.versions import Version, Stage
|
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
|
__version__ = VERSION.version
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ from http import HTTPStatus
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from ..utils import json
|
from ..utils import json
|
||||||
from ..utils.exceptions import ValidationError, TelegramAPIError, BadRequest, Unauthorized, NetworkError, RetryAfter, \
|
from ..utils.exceptions import BadRequest, ConflictError, MigrateToChat, NetworkError, RetryAfter, TelegramAPIError, \
|
||||||
MigrateToChat, ConflictError
|
Unauthorized, ValidationError
|
||||||
from ..utils.helper import Helper, HelperMode, Item
|
from ..utils.helper import Helper, HelperMode, Item
|
||||||
|
|
||||||
# Main aiogram logger
|
# 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
|
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
|
:param token: BOT token
|
||||||
|
:type token: :obj:`str`
|
||||||
:param method: API method
|
:param method: API method
|
||||||
|
:type method: :obj:`str`
|
||||||
:param data: request payload
|
:param data: request payload
|
||||||
|
:type data: :obj:`dict`
|
||||||
:param files: files
|
:param files: files
|
||||||
|
:type files: :obj:`dict`
|
||||||
:param continue_retry:
|
: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(
|
log.debug("Make request: '{0}' with data: {1} and files {2}".format(
|
||||||
method, data or {}, files or {}))
|
method, data or {}, files or {}))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
from typing import Union, TypeVar, List, Dict, Optional
|
from typing import Dict, List, Optional, TypeVar, Union
|
||||||
|
|
||||||
import aiohttp
|
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
|
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
|
:param token: token from @BotFather
|
||||||
|
:type token: :obj:`str`
|
||||||
:param loop: event loop
|
:param loop: event loop
|
||||||
|
:type loop: Optional Union :obj:`asyncio.BaseEventLoop`, :obj:`asyncio.AbstractEventLoop`
|
||||||
:param connections_limit: connections limit for aiohttp.ClientSession
|
:param connections_limit: connections limit for aiohttp.ClientSession
|
||||||
|
:type connections_limit: :obj:`int`
|
||||||
:param proxy: HTTP proxy URL
|
: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
|
: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.__token = token
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
self.proxy_auth = proxy_auth
|
self.proxy_auth = proxy_auth
|
||||||
|
|
@ -56,8 +62,6 @@ class BaseBot:
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
When bot object is deleting - need close all sessions
|
When bot object is deleting - need close all sessions
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
for session in self._temp_sessions:
|
for session in self._temp_sessions:
|
||||||
if not session.closed:
|
if not session.closed:
|
||||||
|
|
@ -65,15 +69,18 @@ class BaseBot:
|
||||||
if self.session and not self.session.closed:
|
if self.session and not self.session.closed:
|
||||||
self.session.close()
|
self.session.close()
|
||||||
|
|
||||||
def create_temp_session(self) -> aiohttp.ClientSession:
|
def create_temp_session(self, limit: int = 1) -> aiohttp.ClientSession:
|
||||||
"""
|
"""
|
||||||
Create temporary session
|
Create temporary session
|
||||||
|
|
||||||
:return:
|
:param limit: Limit of connections
|
||||||
|
:type limit: :obj:`int`
|
||||||
|
:return: New session
|
||||||
|
:rtype: :obj:`aiohttp.TCPConnector`
|
||||||
"""
|
"""
|
||||||
session = aiohttp.ClientSession(
|
session = aiohttp.ClientSession(
|
||||||
connector=aiohttp.TCPConnector(limit=1, force_close=True),
|
connector=aiohttp.TCPConnector(limit=limit, force_close=True),
|
||||||
loop=self.loop)
|
loop=self.loop, json_serialize=json.dumps)
|
||||||
self._temp_sessions.append(session)
|
self._temp_sessions.append(session)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
@ -81,8 +88,8 @@ class BaseBot:
|
||||||
"""
|
"""
|
||||||
Destroy temporary session
|
Destroy temporary session
|
||||||
|
|
||||||
:param session:
|
:param session: target session
|
||||||
:return:
|
:type session: :obj:`aiohttp.ClientSession`
|
||||||
"""
|
"""
|
||||||
if not session.closed:
|
if not session.closed:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
@ -98,10 +105,14 @@ class BaseBot:
|
||||||
https://core.telegram.org/bots/api#making-requests
|
https://core.telegram.org/bots/api#making-requests
|
||||||
|
|
||||||
:param method: API method
|
:param method: API method
|
||||||
|
:type method: :obj:`str`
|
||||||
:param data: request parameters
|
:param data: request parameters
|
||||||
|
:type data: :obj:`dict`
|
||||||
:param files: files
|
:param files: files
|
||||||
:return: Union[List, Dict]
|
:type files: :obj:`dict`
|
||||||
:raise: :class:`aiogram.exceptions.TelegramApiError`
|
:return: result
|
||||||
|
:rtype: Union[List, Dict]
|
||||||
|
:raise: :obj:`aiogram.exceptions.TelegramApiError`
|
||||||
"""
|
"""
|
||||||
return await api.request(self.session, self.__token, method, data, files,
|
return await api.request(self.session, self.__token, method, data, files,
|
||||||
proxy=self.proxy, proxy_auth=self.proxy_auth,
|
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
|
if You want to automatically create destination (:class:`io.BytesIO`) use default
|
||||||
value of destination and handle result of this method.
|
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 destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
|
||||||
:param timeout: Integer
|
:param timeout: Integer
|
||||||
:param chunk_size: Integer
|
:param chunk_size: Integer
|
||||||
|
|
@ -129,7 +141,7 @@ class BaseBot:
|
||||||
destination = io.BytesIO()
|
destination = io.BytesIO()
|
||||||
|
|
||||||
session = self.create_temp_session()
|
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')
|
dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb')
|
||||||
try:
|
try:
|
||||||
|
|
@ -153,10 +165,10 @@ class BaseBot:
|
||||||
https://core.telegram.org/bots/api#inputfile
|
https://core.telegram.org/bots/api#inputfile
|
||||||
|
|
||||||
:param file_type: field name
|
:param file_type: field name
|
||||||
:param method: API metod
|
:param method: API method
|
||||||
:param file: String or io.IOBase
|
:param file: String or io.IOBase
|
||||||
:param payload: request payload
|
:param payload: request payload
|
||||||
:return: resonse
|
:return: response
|
||||||
"""
|
"""
|
||||||
if file is None:
|
if file is None:
|
||||||
files = {}
|
files = {}
|
||||||
|
|
@ -164,8 +176,6 @@ class BaseBot:
|
||||||
# You can use file ID or URL in the most of requests
|
# You can use file ID or URL in the most of requests
|
||||||
payload[file_type] = file
|
payload[file_type] = file
|
||||||
files = None
|
files = None
|
||||||
elif isinstance(file, (io.IOBase, io.FileIO)):
|
|
||||||
files = {file_type: file.read()}
|
|
||||||
else:
|
else:
|
||||||
files = {file_type: file}
|
files = {file_type: file}
|
||||||
|
|
||||||
|
|
@ -435,7 +445,8 @@ class BaseBot:
|
||||||
disable_notification: Optional[Boolean] = None,
|
disable_notification: Optional[Boolean] = None,
|
||||||
reply_to_message_id: Optional[Integer] = None,
|
reply_to_message_id: Optional[Integer] = None,
|
||||||
reply_markup: Optional[Union[
|
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.
|
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.
|
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)
|
:param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional)
|
||||||
- Additional interface options. A JSON-serialized object for an inline keyboard,
|
- 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.
|
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.
|
:return: On success, the sent Message is returned.
|
||||||
"""
|
"""
|
||||||
reply_markup = prepare_arg(reply_markup)
|
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)
|
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_to_message_id: Optional[Integer] = None,
|
||||||
reply_markup: Optional[Union[
|
reply_markup: Optional[Union[
|
||||||
types.InlineKeyboardMarkup,
|
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.
|
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.
|
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)
|
:param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional)
|
||||||
- Additional interface options. A JSON-serialized object for an inline keyboard,
|
- 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.
|
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)
|
:return: On success, the sent Message is returned. (serialized)
|
||||||
"""
|
"""
|
||||||
raw = super(Bot, self).send_document(chat_id=chat_id, document=document, caption=caption,
|
raw = super(Bot, self).send_document(chat_id=chat_id, document=document, caption=caption,
|
||||||
disable_notification=disable_notification,
|
disable_notification=disable_notification,
|
||||||
reply_to_message_id=reply_to_message_id,
|
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))
|
return self.prepare_object(types.Message.deserialize(await raw))
|
||||||
|
|
||||||
async def send_video(self, chat_id: Union[Integer, String],
|
async def send_video(self, chat_id: Union[Integer, String],
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ class RedisStorage(BaseStorage):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
chat, user = self.check_address(chat=chat, user=user)
|
chat, user = self.check_address(chat=chat, user=user)
|
||||||
addr = f"{chat}:{user}"
|
addr = f"fsm:{chat}:{user}"
|
||||||
|
|
||||||
conn = await self.redis
|
conn = await self.redis
|
||||||
data = await conn.execute('GET', addr)
|
data = await conn.execute('GET', addr)
|
||||||
|
|
@ -104,7 +104,7 @@ class RedisStorage(BaseStorage):
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
chat, user = self.check_address(chat=chat, user=user)
|
chat, user = self.check_address(chat=chat, user=user)
|
||||||
addr = f"{chat}:{user}"
|
addr = f"fsm:{chat}:{user}"
|
||||||
|
|
||||||
record = {'state': state, 'data': data}
|
record = {'state': state, 'data': data}
|
||||||
|
|
||||||
|
|
@ -138,3 +138,19 @@ class RedisStorage(BaseStorage):
|
||||||
data = []
|
data = []
|
||||||
data.update(data, **kwargs)
|
data.update(data, **kwargs)
|
||||||
await self.set_data(chat=chat, user=user, data=data)
|
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 logging
|
||||||
import typing
|
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 .handler import Handler
|
||||||
from .storage import DisabledStorage, BaseStorage, FSMContext
|
from .storage import BaseStorage, DisabledStorage, FSMContext
|
||||||
from .webhook import BaseResponse
|
from .webhook import BaseResponse
|
||||||
from ..bot import Bot
|
from ..bot import Bot
|
||||||
from ..types.message import ContentType
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MODE = 'MODE'
|
||||||
|
LONG_POOLING = 'long-pooling'
|
||||||
|
|
||||||
|
|
||||||
class Dispatcher:
|
class Dispatcher:
|
||||||
"""
|
"""
|
||||||
|
|
@ -48,6 +53,8 @@ class Dispatcher:
|
||||||
|
|
||||||
self.updates_handler.register(self.process_update)
|
self.updates_handler.register(self.process_update)
|
||||||
|
|
||||||
|
self.errors_handlers = Handler(self, once=False)
|
||||||
|
|
||||||
self._pooling = False
|
self._pooling = False
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
|
@ -79,7 +86,7 @@ class Dispatcher:
|
||||||
"""
|
"""
|
||||||
tasks = []
|
tasks = []
|
||||||
for update in updates:
|
for update in updates:
|
||||||
tasks.append(self.updates_handler.notify(update))
|
tasks.append(self.process_update(update))
|
||||||
return await asyncio.gather(*tasks)
|
return await asyncio.gather(*tasks)
|
||||||
|
|
||||||
async def process_update(self, update):
|
async def process_update(self, update):
|
||||||
|
|
@ -89,25 +96,64 @@ class Dispatcher:
|
||||||
:param update:
|
:param update:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
self.last_update_id = update.update_id
|
self.last_update_id = update.update_id
|
||||||
|
has_context = context.check_configured()
|
||||||
if update.message:
|
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)
|
return await self.message_handlers.notify(update.message)
|
||||||
if update.edited_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)
|
return await self.edited_message_handlers.notify(update.edited_message)
|
||||||
if update.channel_post:
|
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)
|
return await self.channel_post_handlers.notify(update.channel_post)
|
||||||
if update.edited_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)
|
return await self.edited_channel_post_handlers.notify(update.edited_channel_post)
|
||||||
if update.inline_query:
|
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)
|
return await self.inline_query_handlers.notify(update.inline_query)
|
||||||
if update.chosen_inline_result:
|
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)
|
return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result)
|
||||||
if update.callback_query:
|
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)
|
return await self.callback_query_handlers.notify(update.callback_query)
|
||||||
if update.shipping_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)
|
return await self.shipping_query_handlers.notify(update.shipping_query)
|
||||||
if update.pre_checkout_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)
|
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):
|
async def start_pooling(self, timeout=20, relax=0.1, limit=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -121,6 +167,7 @@ class Dispatcher:
|
||||||
if self._pooling:
|
if self._pooling:
|
||||||
raise RuntimeError('Pooling already started')
|
raise RuntimeError('Pooling already started')
|
||||||
log.info('Start pooling.')
|
log.info('Start pooling.')
|
||||||
|
context.set_value(MODE, LONG_POOLING)
|
||||||
|
|
||||||
self._pooling = True
|
self._pooling = True
|
||||||
offset = None
|
offset = None
|
||||||
|
|
@ -150,9 +197,8 @@ class Dispatcher:
|
||||||
:param updates: list of updates.
|
:param updates: list of updates.
|
||||||
"""
|
"""
|
||||||
need_to_call = []
|
need_to_call = []
|
||||||
for update in await self.process_updates(updates):
|
for response in await self.process_updates(updates):
|
||||||
for responses in update:
|
for response in response:
|
||||||
for response in responses:
|
|
||||||
if not isinstance(response, BaseResponse):
|
if not isinstance(response, BaseResponse):
|
||||||
continue
|
continue
|
||||||
need_to_call.append(response.execute_response(self.bot))
|
need_to_call.append(response.execute_response(self.bot))
|
||||||
|
|
@ -709,6 +755,35 @@ class Dispatcher:
|
||||||
|
|
||||||
return decorator
|
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, *,
|
def current_state(self, *,
|
||||||
chat: typing.Union[str, int, None] = None,
|
chat: typing.Union[str, int, None] = None,
|
||||||
user: typing.Union[str, int, None] = None) -> FSMContext:
|
user: typing.Union[str, int, None] = None) -> FSMContext:
|
||||||
|
|
@ -730,6 +805,7 @@ class Dispatcher:
|
||||||
:param func:
|
:param func:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_response(task):
|
def process_response(task):
|
||||||
response = task.result()
|
response = task.result()
|
||||||
self.loop.create_task(response.execute_response(self.bot))
|
self.loop.create_task(response.execute_response(self.bot))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from aiogram.utils import context
|
||||||
from ..utils.helper import Helper, HelperMode, Item
|
from ..utils.helper import Helper, HelperMode, Item
|
||||||
|
|
||||||
|
USER_STATE = 'USER_STATE'
|
||||||
|
|
||||||
|
|
||||||
async def check_filter(filter_, args, kwargs):
|
async def check_filter(filter_, args, kwargs):
|
||||||
if not callable(filter_):
|
if not callable(filter_):
|
||||||
|
|
@ -102,6 +105,10 @@ class StateFilter(AsyncFilter):
|
||||||
if self.state == '*':
|
if self.state == '*':
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
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)
|
chat, user = self.get_target(obj)
|
||||||
|
|
||||||
if chat or user:
|
if chat or user:
|
||||||
|
|
@ -118,6 +125,19 @@ class StatesListFilter(StateFilter):
|
||||||
return False
|
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):
|
def generate_default_filters(dispatcher, *args, **kwargs):
|
||||||
filters_set = []
|
filters_set = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
from aiogram.utils.deprecated import deprecated
|
||||||
from . import Handler
|
from . import Handler
|
||||||
from .handler import SkipHandler
|
from .handler import SkipHandler
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
class Middleware:
|
class Middleware:
|
||||||
def __init__(self, handler, filters=None):
|
def __init__(self, handler, filters=None):
|
||||||
self.handler: Handler = handler
|
self.handler: Handler = handler
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ import asyncio.tasks
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import typing
|
import typing
|
||||||
from typing import Union, Dict, Optional
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .. import types
|
from .. import types
|
||||||
from ..bot import api
|
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 import json
|
||||||
from ..utils.deprecated import warn_deprecated as warn
|
from ..utils.deprecated import warn_deprecated as warn
|
||||||
from ..utils.exceptions import TimeoutWarning
|
from ..utils.exceptions import TimeoutWarning
|
||||||
|
|
@ -20,6 +21,10 @@ BOT_DISPATCHER_KEY = 'BOT_DISPATCHER'
|
||||||
|
|
||||||
RESPONSE_TIMEOUT = 55
|
RESPONSE_TIMEOUT = 55
|
||||||
|
|
||||||
|
WEBHOOK = 'webhook'
|
||||||
|
WEBHOOK_CONNECTION = 'WEBHOOK_CONNECTION'
|
||||||
|
WEBHOOK_REQUEST = 'WEBHOOK_REQUEST'
|
||||||
|
|
||||||
|
|
||||||
class WebhookRequestHandler(web.View):
|
class WebhookRequestHandler(web.View):
|
||||||
"""
|
"""
|
||||||
|
|
@ -71,6 +76,11 @@ class WebhookRequestHandler(web.View):
|
||||||
|
|
||||||
:return: :class:`aiohttp.web.Response`
|
:return: :class:`aiohttp.web.Response`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
context.update_state({'CALLER': WEBHOOK,
|
||||||
|
WEBHOOK_CONNECTION: True,
|
||||||
|
WEBHOOK_REQUEST: self.request})
|
||||||
|
|
||||||
dispatcher = self.get_dispatcher()
|
dispatcher = self.get_dispatcher()
|
||||||
update = await self.parse_update(dispatcher.bot)
|
update = await self.parse_update(dispatcher.bot)
|
||||||
|
|
||||||
|
|
@ -113,6 +123,7 @@ class WebhookRequestHandler(web.View):
|
||||||
if fut.done():
|
if fut.done():
|
||||||
return fut.result()
|
return fut.result()
|
||||||
else:
|
else:
|
||||||
|
context.set_value(WEBHOOK_CONNECTION, False)
|
||||||
fut.remove_done_callback(cb)
|
fut.remove_done_callback(cb)
|
||||||
fut.add_done_callback(self.respond_via_request)
|
fut.add_done_callback(self.respond_via_request)
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -253,7 +264,7 @@ class ReplyToMixin:
|
||||||
class SendMessage(BaseResponse, ReplyToMixin):
|
class SendMessage(BaseResponse, ReplyToMixin):
|
||||||
"""
|
"""
|
||||||
You can send message with webhook by using this instance of this object.
|
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',
|
__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>'),
|
('<pre>', '</pre>'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HTML_QUOTES_MAP = {
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'&': '&',
|
||||||
|
'"': '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _join(*content, sep=' '):
|
def _join(*content, sep=' '):
|
||||||
return sep.join(map(str, content))
|
return sep.join(map(str, content))
|
||||||
|
|
@ -27,6 +34,22 @@ def _md(string, symbols=('', '')):
|
||||||
return start + string + end
|
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=' '):
|
def text(*content, sep=' '):
|
||||||
"""
|
"""
|
||||||
Join all elements with separator
|
Join all elements with separator
|
||||||
|
|
@ -57,7 +80,7 @@ def hbold(*content, sep=' '):
|
||||||
:param sep:
|
:param sep:
|
||||||
:return:
|
: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=' '):
|
def italic(*content, sep=' '):
|
||||||
|
|
@ -79,7 +102,7 @@ def hitalic(*content, sep=' '):
|
||||||
:param sep:
|
:param sep:
|
||||||
:return:
|
: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=' '):
|
def code(*content, sep=' '):
|
||||||
|
|
@ -101,7 +124,7 @@ def hcode(*content, sep=' '):
|
||||||
:param sep:
|
:param sep:
|
||||||
:return:
|
: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'):
|
def pre(*content, sep='\n'):
|
||||||
|
|
@ -123,7 +146,7 @@ def hpre(*content, sep='\n'):
|
||||||
:param sep:
|
:param sep:
|
||||||
:return:
|
: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):
|
def link(title, url):
|
||||||
|
|
@ -134,7 +157,7 @@ def link(title, url):
|
||||||
:param url:
|
:param url:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return "[{0}]({1})".format(_escape(title), url)
|
return "[{0}]({1})".format(title, url)
|
||||||
|
|
||||||
|
|
||||||
def hlink(title, url):
|
def hlink(title, url):
|
||||||
|
|
@ -145,7 +168,7 @@ def hlink(title, url):
|
||||||
:param url:
|
:param url:
|
||||||
:return:
|
: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=' '):
|
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.
|
This class is base of bot. In BaseBot implemented all available methods of Telegram Bot API.
|
||||||
|
|
||||||
.. autoclass:: aiogram.bot.base.BaseBot
|
.. autoclass:: aiogram.bot.base.BaseBot
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,5 @@ 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`
|
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
|
name: py36
|
||||||
channels:
|
channels:
|
||||||
- conda-forge
|
- conda-forge
|
||||||
- default
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- python=3.6
|
- python=3.6
|
||||||
- sphinx=1.5.3
|
- 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())
|
loop.run_until_complete(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
loop.stop()
|
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 distutils.core import setup
|
||||||
|
|
||||||
from setuptools import PackageFinder
|
from setuptools import PackageFinder
|
||||||
|
|
||||||
from aiogram import __version__ as version
|
from aiogram import __version__ as version
|
||||||
|
|
||||||
|
ALLOWED_SYMBOLS = string.ascii_letters + string.digits + '_-'
|
||||||
|
|
||||||
|
|
||||||
def get_description():
|
def get_description():
|
||||||
|
"""
|
||||||
|
Read full description from 'README.rst'
|
||||||
|
|
||||||
|
:return: description
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
with open('README.rst', encoding='utf-8') as f:
|
with open('README.rst', encoding='utf-8') as f:
|
||||||
return f.read()
|
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(
|
setup(
|
||||||
name='aiogram',
|
name='aiogram',
|
||||||
version=version,
|
version=version,
|
||||||
|
|
@ -18,15 +47,15 @@ setup(
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Alex Root Junior',
|
author='Alex Root Junior',
|
||||||
author_email='jroot.junior@gmail.com',
|
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(),
|
long_description=get_description(),
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'Framework :: AsyncIO',
|
'Framework :: AsyncIO',
|
||||||
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
],
|
],
|
||||||
install_requires=['aiohttp']
|
install_requires=get_requirements()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue