From 99f5a89f702ce56727d7a8898ec3040bedf78b47 Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Sat, 17 Feb 2018 22:25:23 +0500 Subject: [PATCH 01/12] Using connection pool in RethinkDB driver --- aiogram/contrib/fsm_storage/rethinkdb.py | 97 +++++++++++++++++------- 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index cb84a59f..092c3056 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -1,5 +1,6 @@ import asyncio import typing +import weakref import rethinkdb as r @@ -47,77 +48,110 @@ class RethinkDBStorage(BaseStorage): self._timeout = timeout self._ssl = ssl or {} - self._connection: r.Connection = None + self._queue = asyncio.Queue() + self._outstanding_connections = weakref.WeakSet() self._loop = loop or asyncio.get_event_loop() - self._lock = asyncio.Lock(loop=self._loop) - async def connection(self): + async def get_connection(self): """ Get or create connection. """ - async with self._lock: # thread-safe - if not self._connection: - self._connection = await r.connect(host=self._host, port=self._port, db=self._db, - auth_key=self._auth_key, user=self._user, - password=self._password, timeout=self._timeout, ssl=self._ssl, - io_loop=self._loop) - return self._connection + try: + while True: + conn: r.Connection = self._queue.get_nowait() + if conn.is_open(): + break + try: + await conn.close() + except r.ReqlError: + raise ConnectionNotClosed('Exception was caught while closing connection') + except asyncio.QueueEmpty: + conn = await r.connect(host=self._host, port=self._port, db=self._db, + auth_key=self._auth_key, user=self._user, password=self._password, timeout=self._timeout, + ssl=self._ssl) + + self._outstanding_connections.add(conn) + return conn + + async def put_connection(self, conn): + """ + Return connection to pool. + """ + self._queue.put_nowait(conn) + self._outstanding_connections.remove(conn) async def close(self): """ - Close connection. + Close all connections. """ - if self._connection and self._connection.is_open(): - await self._connection.close() - self._connection = None + while True: + try: + conn: r.Connection = self._queue.get_nowait() + except asyncio.QueueEmpty: + break + + self._outstanding_connections.add(conn) + + for conn in self._outstanding_connections: + try: + await conn.close() + except r.ReqlError: + raise ConnectionNotClosed('Exception was caught while closing connection') def wait_closed(self): """ Checks if connection is closed. """ - if self._connection: + if len(self._outstanding_connections) != 0 and self._queue.qsize() != 0: raise ConnectionNotClosed return True async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() - return await r.table(self._table).get(chat)[user]['state'].default(default or '').run(conn) + conn = await self.get_connection() + result = await r.table(self._table).get(chat)[user]['state'].default(default or '').run(conn) + await self.put_connection(conn) + return result async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() - return await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn) + conn = await self.get_connection() + result = await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn) + await self.put_connection(conn) + return result async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, state: typing.Optional[typing.AnyStr] = None): chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() + conn = await self.get_connection() if await r.table(self._table).get(chat).run(conn): await r.table(self._table).get(chat).update({user: {'state': state}}).run(conn) else: await r.table(self._table).insert({'id': chat, user: {'state': state}}).run(conn) + await self.put_connection(conn) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() + conn = await self.get_connection() if await r.table(self._table).get(chat).run(conn): await r.table(self._table).get(chat).update({user: {'data': r.literal(data)}}).run(conn) else: await r.table(self._table).insert({'id': chat, user: {'data': data}}).run(conn) + await self.put_connection(conn) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None, **kwargs): chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() + conn = await self.get_connection() if await r.table(self._table).get(chat).run(conn): await r.table(self._table).get(chat).update({user: {'data': data}}).run(conn) else: await r.table(self._table).insert({'id': chat, user: {'data': data}}).run(conn) + await self.put_connection(conn) def has_bucket(self): return True @@ -125,27 +159,31 @@ class RethinkDBStorage(BaseStorage): async def get_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[dict] = None) -> typing.Dict: chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() - return await r.table(self._table).get(chat)[user]['bucket'].default(default or {}).run(conn) + conn = await self.get_connection() + result = await r.table(self._table).get(chat)[user]['bucket'].default(default or {}).run(conn) + await self.put_connection(conn) + return result async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, bucket: typing.Dict = None): chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() + conn = await self.get_connection() if await r.table(self._table).get(chat).run(conn): await r.table(self._table).get(chat).update({user: {'bucket': r.literal(bucket)}}).run(conn) else: await r.table(self._table).insert({'id': chat, user: {'bucket': bucket}}).run(conn) + await self.put_connection(conn) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, bucket: typing.Dict = None, **kwargs): chat, user = map(str, self.check_address(chat=chat, user=user)) - conn = await self.connection() + conn = await self.get_connection() if await r.table(self._table).get(chat).run(conn): await r.table(self._table).get(chat).update({user: {'bucket': bucket}}).run(conn) else: await r.table(self._table).insert({'id': chat, user: {'bucket': bucket}}).run(conn) + await self.put_connection(conn) async def get_states_list(self) -> typing.List[typing.Tuple[int]]: """ @@ -153,7 +191,7 @@ class RethinkDBStorage(BaseStorage): :return: list of tuples where first element is chat id and second is user id """ - conn = await self.connection() + conn = await self.get_connection() result = [] items = (await r.table(self._table).run(conn)).items @@ -164,11 +202,14 @@ class RethinkDBStorage(BaseStorage): user = int(key) result.append((chat, user)) + await self.put_connection(conn) + return result async def reset_all(self): """ Reset states in DB """ - conn = await self.connection() + conn = await self.get_connection() await r.table(self._table).delete().run(conn) + await self.put_connection(conn) From 4f2cb40aeaf5c4c4a5127ab0ee088c51a266e331 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 22 Feb 2018 01:56:44 +0200 Subject: [PATCH 02/12] Implemented auth widget object and auth data validator. --- aiogram/bot/base.py | 4 +++ aiogram/types/__init__.py | 2 ++ aiogram/types/auth_widget_data.py | 44 +++++++++++++++++++++++++++++++ aiogram/utils/auth_widget.py | 25 ++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 aiogram/types/auth_widget_data.py create mode 100644 aiogram/utils/auth_widget.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 2aa6816c..84b1342c 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -7,6 +7,7 @@ import aiohttp from . import api from ..types import ParseMode, base from ..utils import json +from ..utils.auth_widget import check_token class BaseBot: @@ -248,3 +249,6 @@ class BaseBot: @parse_mode.deleter def parse_mode(self): self.parse_mode = None + + def check_auth_widget(self, data): + return check_token(data, self.__token) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 7b56c835..cc7abb11 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -2,6 +2,7 @@ from . import base from . import fields from .animation import Animation from .audio import Audio +from .auth_widget_data import AuthWidgetData from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType @@ -56,6 +57,7 @@ __all__ = ( 'AllowedUpdates', 'Animation', 'Audio', + 'AuthWidgetData', 'CallbackGame', 'CallbackQuery', 'Chat', diff --git a/aiogram/types/auth_widget_data.py b/aiogram/types/auth_widget_data.py new file mode 100644 index 00000000..64066437 --- /dev/null +++ b/aiogram/types/auth_widget_data.py @@ -0,0 +1,44 @@ +from aiohttp import web + +from . import base +from . import fields + + +class AuthWidgetData(base.TelegramObject): + id: base.Integer = fields.Field() + first_name: base.String = fields.Field() + last_name: base.String = fields.Field() + username: base.String = fields.Field() + photo_url: base.String = fields.Field() + auth_date: base.String = fields.DateTimeField() + hash: base.String = fields.Field() + + @classmethod + def parse(cls, request: web.Request) -> 'AuthWidgetData': + """ + Parse request as Telegram auth widget data. + + :param request: + :return: :obj:`AuthWidgetData` + :raise :obj:`aiohttp.web.HTTPBadRequest` + """ + try: + query = dict(request.query) + query['id'] = int(query['id']) + query['auth_date'] = int(query['auth_date']) + widget = AuthWidgetData(**query) + except (ValueError, KeyError): + raise web.HTTPBadRequest(text='Invalid auth data') + else: + return widget + + def validate(self): + return self.bot.check_auth_widget(self.to_python()) + + @property + def full_name(self): + result = self.first_name + if self.last_name: + result += ' ' + result += self.last_name + return result diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py new file mode 100644 index 00000000..b5cce802 --- /dev/null +++ b/aiogram/utils/auth_widget.py @@ -0,0 +1,25 @@ +import collections +import hashlib +import hmac + + +def check_token(data, token): + """ + Validate auth token + https://core.telegram.org/widgets/login#checking-authorization + + Source: https://gist.github.com/xen/e4bea72487d34caa28c762776cf655a3 + + :param data: + :param token: + :return: + """ + secret = hashlib.sha256() + secret.update(token.encode('utf-8')) + sorted_params = collections.OrderedDict(sorted(data.items())) + param_hash = sorted_params.pop('hash', '') or '' + msg = "\n".join(["{}={}".format(k, v) for k, v in sorted_params.items()]) + + if param_hash == hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest(): + return True + return False From f3f9b3c27a140c5ce2ba70dfc57ce6d33b8d3049 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 22 Feb 2018 02:11:29 +0200 Subject: [PATCH 03/12] Check python version in `setup.py` --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 1d999d2e..be9694df 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,19 @@ #!/usr/bin/env python3 +import sys from distutils.core import setup +from warnings import warn from pip.req import parse_requirements from setuptools import PackageFinder from aiogram import Stage, VERSION +MINIMAL_PY_VERSION = (3, 6) + +if sys.version_info < MINIMAL_PY_VERSION: + warn('aiogram works only with Python {}+'.format('.'.join(map(str, MINIMAL_PY_VERSION)), RuntimeWarning)) + def get_description(): """ From 9ef7ef46431bbf369522dfd99dc314e408d7ac58 Mon Sep 17 00:00:00 2001 From: Arslan Sakhapov Date: Thu, 22 Feb 2018 21:20:01 +0500 Subject: [PATCH 04/12] Add max_conn param --- aiogram/contrib/fsm_storage/rethinkdb.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 092c3056..8c6d24ae 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -37,7 +37,7 @@ class RethinkDBStorage(BaseStorage): """ def __init__(self, host='localhost', port=28015, db='aiogram', table='aiogram', auth_key=None, - user=None, password=None, timeout=20, ssl=None, loop=None): + user=None, password=None, timeout=20, ssl=None, max_conn=10, loop=None): self._host = host self._port = port self._db = db @@ -48,7 +48,7 @@ class RethinkDBStorage(BaseStorage): self._timeout = timeout self._ssl = ssl or {} - self._queue = asyncio.Queue() + self._queue = asyncio.Queue(max_conn) self._outstanding_connections = weakref.WeakSet() self._loop = loop or asyncio.get_event_loop() @@ -66,9 +66,12 @@ class RethinkDBStorage(BaseStorage): except r.ReqlError: raise ConnectionNotClosed('Exception was caught while closing connection') except asyncio.QueueEmpty: - conn = await r.connect(host=self._host, port=self._port, db=self._db, - auth_key=self._auth_key, user=self._user, password=self._password, timeout=self._timeout, - ssl=self._ssl) + if len(self._outstanding_connections) < self._queue.maxsize: + conn = await r.connect(host=self._host, port=self._port, db=self._db, + auth_key=self._auth_key, user=self._user, password=self._password, + timeout=self._timeout, ssl=self._ssl) + else: + conn = await self._queue.get() self._outstanding_connections.add(conn) return conn @@ -185,7 +188,7 @@ class RethinkDBStorage(BaseStorage): await r.table(self._table).insert({'id': chat, user: {'bucket': bucket}}).run(conn) await self.put_connection(conn) - async def get_states_list(self) -> typing.List[typing.Tuple[int]]: + async def get_states_list(self) -> typing.List[typing.Tuple[int, int]]: """ Get list of all stored chat's and user's From 531b2a4df799b10f24205f7432f20488fa9be534 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 23 Feb 2018 13:19:23 +0200 Subject: [PATCH 05/12] Hashable TelegramObject --- aiogram/types/audio.py | 8 -------- aiogram/types/auth_widget_data.py | 3 +++ aiogram/types/base.py | 25 +++++++++++++++++++++++++ aiogram/types/callback_query.py | 3 +++ aiogram/types/chat.py | 3 +++ aiogram/types/chat_photo.py | 3 +++ aiogram/types/contact.py | 3 +++ aiogram/types/message_entity.py | 4 ++-- aiogram/types/mixins.py | 3 +++ aiogram/types/sticker.py | 11 ++--------- aiogram/types/user.py | 5 +---- aiogram/types/video.py | 11 ++--------- aiogram/types/video_note.py | 11 ++--------- aiogram/types/voice.py | 11 ++--------- 14 files changed, 54 insertions(+), 50 deletions(-) diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 7946c28a..ed323f81 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -15,11 +15,3 @@ class Audio(base.TelegramObject, mixins.Downloadable): title: base.String = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() - - def __hash__(self): - return hash(self.file_id) + \ - self.duration + \ - hash(self.performer) + \ - hash(self.title) + \ - hash(self.mime_type) + \ - self.file_size diff --git a/aiogram/types/auth_widget_data.py b/aiogram/types/auth_widget_data.py index 64066437..c117c92e 100644 --- a/aiogram/types/auth_widget_data.py +++ b/aiogram/types/auth_widget_data.py @@ -42,3 +42,6 @@ class AuthWidgetData(base.TelegramObject): result += ' ' result += self.last_name return result + + def __hash__(self): + return self.id diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 60a26485..166a9848 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -244,3 +244,28 @@ class TelegramObject(metaclass=MetaTelegramObject): """ for _, value in self: yield value + + def __hash__(self): + def _hash(obj): + buf = 0 + if isinstance(obj, list): + for item in obj: + buf += _hash(item) + elif isinstance(obj, dict): + for dict_key, dict_value in obj.items(): + buf += hash(dict_key) + _hash(dict_value) + else: + try: + buf += hash(obj) + except TypeError: # Skip unhashable objects + pass + return buf + + result = 0 + for key, value in sorted(self.values.items()): + result += hash(key) + _hash(value) + + return result + + def __eq__(self, other): + return isinstance(other, self.__class__) and hash(other) == hash(self) diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 72ef1604..51ba1f17 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -56,3 +56,6 @@ class CallbackQuery(base.TelegramObject): :rtype: :obj:`base.Boolean`""" await self.bot.answer_callback_query(callback_query_id=self.id, text=text, show_alert=show_alert, url=url, cache_time=cache_time) + + def __hash__(self): + return hash(self.id) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index a696e90e..1d6db5a9 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -28,6 +28,9 @@ class Chat(base.TelegramObject): sticker_set_name: base.String = fields.Field() can_set_sticker_set: base.Boolean = fields.Field() + def __hash__(self): + return self.id + @property def full_name(self): if self.type == ChatType.PRIVATE: diff --git a/aiogram/types/chat_photo.py b/aiogram/types/chat_photo.py index daac874b..08775d93 100644 --- a/aiogram/types/chat_photo.py +++ b/aiogram/types/chat_photo.py @@ -73,3 +73,6 @@ class ChatPhoto(base.TelegramObject): async def get_big_file(self): return await self.bot.get_file(self.big_file_id) + + def __hash__(self): + return hash(self.small_file_id) + hash(self.big_file_id) diff --git a/aiogram/types/contact.py b/aiogram/types/contact.py index 5f0eed35..842d6044 100644 --- a/aiogram/types/contact.py +++ b/aiogram/types/contact.py @@ -19,3 +19,6 @@ class Contact(base.TelegramObject): if self.last_name is not None: name += ' ' + self.last_name return name + + def __hash__(self): + return hash(self.phone_number) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 46733f3a..24e6da5f 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -18,8 +18,8 @@ class MessageEntity(base.TelegramObject): def _apply(self, text, func): return text[:self.offset] + \ - func(text[self.offset:self.offset + self.length]) + \ - text[self.offset + self.length:] + func(text[self.offset:self.offset + self.length]) + \ + text[self.offset + self.length:] def apply_md(self, text): """ diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index 0d0a42f9..396633ad 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -44,3 +44,6 @@ class Downloadable: return self else: return await self.bot.get_file(self.file_id) + + def __hash__(self): + return hash(self.file_id) diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 5b68d458..b2fd7ef6 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -1,10 +1,11 @@ from . import base from . import fields +from . import mixins from .mask_position import MaskPosition from .photo_size import PhotoSize -class Sticker(base.TelegramObject): +class Sticker(base.TelegramObject, mixins.Downloadable): """ This object represents a sticker. @@ -18,11 +19,3 @@ class Sticker(base.TelegramObject): set_name: base.String = fields.Field() mask_position: MaskPosition = fields.Field(base=MaskPosition) file_size: base.Integer = fields.Field() - - def __hash__(self): - return self.file_id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.file_id == self.file_id - return self.file_id == other diff --git a/aiogram/types/user.py b/aiogram/types/user.py index c8864f8d..40e1f7bc 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -78,10 +78,7 @@ class User(base.TelegramObject): return await self.bot.get_user_profile_photos(self.id, offset, limit) def __hash__(self): - return self.id + \ - hash(self.is_bot) + \ - hash(self.full_name) + \ - (hash(self.username) if self.username else 0) + return self.id def __int__(self): return self.id diff --git a/aiogram/types/video.py b/aiogram/types/video.py index 36fc0f90..bf5187cd 100644 --- a/aiogram/types/video.py +++ b/aiogram/types/video.py @@ -1,9 +1,10 @@ from . import base from . import fields +from . import mixins from .photo_size import PhotoSize -class Video(base.TelegramObject): +class Video(base.TelegramObject, mixins.Downloadable): """ This object represents a video file. @@ -16,11 +17,3 @@ class Video(base.TelegramObject): thumb: PhotoSize = fields.Field(base=PhotoSize) mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() - - def __hash__(self): - return self.file_id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.file_id == self.file_id - return self.file_id == other diff --git a/aiogram/types/video_note.py b/aiogram/types/video_note.py index 286ca762..9665b6bc 100644 --- a/aiogram/types/video_note.py +++ b/aiogram/types/video_note.py @@ -1,9 +1,10 @@ from . import base from . import fields +from . import mixins from .photo_size import PhotoSize -class VideoNote(base.TelegramObject): +class VideoNote(base.TelegramObject, mixins.Downloadable): """ This object represents a video message (available in Telegram apps as of v.4.0). @@ -14,11 +15,3 @@ class VideoNote(base.TelegramObject): duration: base.Integer = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) file_size: base.Integer = fields.Field() - - def __hash__(self): - return self.file_id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.file_id == self.file_id - return self.file_id == other diff --git a/aiogram/types/voice.py b/aiogram/types/voice.py index ba006bec..621f2247 100644 --- a/aiogram/types/voice.py +++ b/aiogram/types/voice.py @@ -1,8 +1,9 @@ from . import base from . import fields +from . import mixins -class Voice(base.TelegramObject): +class Voice(base.TelegramObject, mixins.Downloadable): """ This object represents a voice note. @@ -12,11 +13,3 @@ class Voice(base.TelegramObject): duration: base.Integer = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() - - def __hash__(self): - return self.file_id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.file_id == self.file_id - return self.file_id == other From 7cab32b1cca545325b447de9675894a14fdd1e10 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 23 Feb 2018 13:56:35 +0200 Subject: [PATCH 06/12] Fix `Fatal Python error: PyImport_GetModuleDict: no module dictionary!` --- aiogram/bot/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 84b1342c..57dd5895 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -68,17 +68,17 @@ class BaseBot: self.parse_mode = parse_mode def __del__(self): - self.close() + asyncio.ensure_future(self.close()) - def close(self): + async def close(self): """ Close all client sessions """ + if self.session and not self.session.closed: + await self.session.close() for session in self._temp_sessions: if not session.closed: - session.close() - if self.session and not self.session.closed: - self.session.close() + await session.close() def create_temp_session(self, limit: base.Integer = 1, force_close: base.Boolean = False) -> aiohttp.ClientSession: """ From a3856e33bd757737b30bbe638f58c950d4b263c0 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 23 Feb 2018 14:08:02 +0200 Subject: [PATCH 07/12] Default value for `state` argument in `FSMContext.set_state` --- aiogram/dispatcher/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index b1e6377f..e50b8de7 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -286,7 +286,7 @@ class FSMContext: async def update_data(self, data: typing.Dict = None, **kwargs): await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs) - async def set_state(self, state: typing.Union[typing.AnyStr, None]): + async def set_state(self, state: typing.Union[typing.AnyStr, None] = None): await self.storage.set_state(chat=self.chat, user=self.user, state=state) async def set_data(self, data: typing.Dict = None): From dfcc59d349e5387ff59ead32bd8f20d4ae064568 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 23 Feb 2018 14:11:35 +0200 Subject: [PATCH 08/12] Change version number --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index d9d32776..02f83130 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 1, 1, stage=Stage.DEV, build=0) +VERSION = Version(1, 2, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From 5de860af6e66332f6f2e2aa798257ea5c57acff5 Mon Sep 17 00:00:00 2001 From: Pryanik <34004367+h0n3yc4k3@users.noreply.github.com> Date: Fri, 23 Feb 2018 22:33:25 +0300 Subject: [PATCH 09/12] Fixed minor typo --- examples/adwanced_executor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/adwanced_executor_example.py b/examples/adwanced_executor_example.py index 9dd873b8..eb4694a7 100644 --- a/examples/adwanced_executor_example.py +++ b/examples/adwanced_executor_example.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -II this example used ArgumentParser for configuring Your bot. +In this example used ArgumentParser for configuring Your bot. Provided to start bot with webhook: python adwanced_executor_example.py \ From f185f69e8dccddb4d973979d1f2871fd6a9c5de2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 1 Mar 2018 19:38:14 +0200 Subject: [PATCH 10/12] Lost `Message.reply_sticker` & `reply=True` by default in `Message.reply` --- aiogram/types/message.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 5b267a6b..fe9e87d9 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -181,7 +181,7 @@ class Message(base.TelegramObject): return text async def reply(self, text, parse_mode=None, disable_web_page_preview=None, - disable_notification=None, reply_markup=None, reply=False) -> 'Message': + disable_notification=None, reply_markup=None, reply=True) -> 'Message': """ Reply to this message @@ -630,6 +630,30 @@ class Message(base.TelegramObject): """ return await self.bot.delete_message(self.chat.id, self.message_id) + async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, reply=True) -> 'Message': + """ + Use this method to send .webp stickers. + + Source: https://core.telegram.org/bots/api#sendsticker + + :param sticker: Sticker to send. + :type sticker: :obj:`typing.Union[base.InputFile, base.String]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def pin(self, disable_notification: bool = False): """ Pin message From 8433c4cc4aa26bacccf51e39b411cde7bea79e3e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 1 Mar 2018 19:39:52 +0200 Subject: [PATCH 11/12] Auto close bot HTTP-connections. --- aiogram/utils/executor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index ed8947eb..686bc7ea 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -39,6 +39,8 @@ async def _shutdown(dispatcher: Dispatcher, callback=None): await dispatcher.storage.close() await dispatcher.storage.wait_closed() + await dispatcher.bot.close() + async def _wh_shutdown(app): callback = app.get('_shutdown_callback', None) From a8c6d164725c63090492bcf302ecad218a0058ab Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 1 Mar 2018 19:41:02 +0200 Subject: [PATCH 12/12] Change version number. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 02f83130..f7c17691 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 2, stage=Stage.FINAL, build=0) +VERSION = Version(1, 2, 1, stage=Stage.DEV, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version