Merge branch 'dev'

# Conflicts:
#	aiogram/__init__.py
This commit is contained in:
Alex Root Junior 2017-09-16 20:01:23 +03:00
commit 4b5269aeab
19 changed files with 668 additions and 76 deletions

View file

@ -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

View file

@ -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 {}))

View file

@ -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)

View file

@ -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],

View file

@ -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

View file

@ -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))

View file

@ -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 = []

View file

@ -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

View file

@ -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
View 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
View 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)

View file

@ -11,6 +11,13 @@ MD_SYMBOLS = (
('<pre>', '</pre>'),
)
HTML_QUOTES_MAP = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
}
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 &lt;, > with &gt; and & with &amp;).
: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
View 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]

View file

@ -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:

View file

@ -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:

View file

@ -1,7 +1,6 @@
name: py36
channels:
- conda-forge
- default
dependencies:
- python=3.6
- sphinx=1.5.3

View 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)

View file

@ -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
View 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()
)