From e5ec44def279108ff1c91b175e6da96f1b7ee558 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 29 Nov 2017 13:51:48 +0200 Subject: [PATCH 01/44] Change version. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ac6b1c96..f7ec1957 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -10,7 +10,7 @@ except ImportError as e: from .utils.versions import Stage, Version -VERSION = Version(1, 0, 2, stage=Stage.DEV, build=0) +VERSION = Version(1, 0, 2, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 5) __version__ = VERSION.version From 3e0d7e1ef5355a4761074dc5db7fb230653d6625 Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Tue, 9 Jan 2018 23:10:22 +0500 Subject: [PATCH 02/44] Fix error replacing 'to_json' by 'to_python' --- aiogram/types/inline_keyboard.py | 2 +- aiogram/types/reply_keyboard.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 636f101c..76c47e98 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -54,7 +54,7 @@ class InlineKeyboardMarkup(base.TelegramObject): """ btn_array = [] for button in args: - btn_array.append(button.to_json()) + btn_array.append(button.to_python()) self.inline_keyboard.append(btn_array) return self diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index c16bc496..27a595c2 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -41,7 +41,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): elif isinstance(button, bytes): row.append({'text': button.decode('utf-8')}) else: - row.append(button.to_json()) + row.append(button.to_python()) if i % self.row_width == 0: self.keyboard.append(row) row = [] @@ -55,7 +55,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): if isinstance(button, str): btn_array.append({'text': button}) else: - btn_array.append(button.to_json()) + btn_array.append(button.to_python()) self.keyboard.append(btn_array) return self From 2656cab2f1876d8ab6a6a4c0f9c374aedfa47cb2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 10 Jan 2018 19:38:41 +0200 Subject: [PATCH 03/44] TelegramObject is iterable. --- aiogram/types/base.py | 47 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index e291afe5..2770fd84 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -4,7 +4,6 @@ from typing import TypeVar from .fields import BaseField from ..utils import json -from ..utils.context import get_value PROPS_ATTR_NAME = '_props' VALUES_ATTR_NAME = '_values' @@ -187,15 +186,61 @@ class TelegramObject(metaclass=MetaTelegramObject): return self.as_json() def __getitem__(self, item): + """ + Item getter (by key) + + :param item: + :return: + """ if item in self.props: return self.props[item].get_value(self) raise KeyError(item) def __setitem__(self, key, value): + """ + Item setter (by key) + + :param key: + :param value: + :return: + """ if key in self.props: return self.props[key].set_value(self, value, self.conf.get('parent', None)) raise KeyError(key) def __contains__(self, item): + """ + Check key contains in that object + + :param item: + :return: + """ self.clean() return item in self.values + + def __iter__(self): + """ + Iterate over items + + :return: + """ + for item in self.to_python().items(): + yield item + + def iter_keys(self): + """ + Iterate over keys + + :return: + """ + for key, _ in self: + yield key + + def iter_values(self): + """ + Iterate over values + + :return: + """ + for _, value in self: + yield value From 0144d54e811ffcfe4ef079bad7cbc60a7ec86a40 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 10 Jan 2018 20:41:19 +0200 Subject: [PATCH 04/44] Change version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index e7a1292d..6af7fd8c 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -19,7 +19,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 0, 3, stage=Stage.FINAL, build=0) +VERSION = Version(1, 0, 4, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 5) __version__ = VERSION.version From 9b383a8c34be77f9e53c07f411ceceb0cd270bfa Mon Sep 17 00:00:00 2001 From: Sergey Baboshin Date: Sat, 13 Jan 2018 18:42:21 +0300 Subject: [PATCH 05/44] Fixed code example --- docs/source/quick_start.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index a9ff0890..c5ff7e9c 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -48,5 +48,9 @@ Summary bot = Bot(token='BOT TOKEN HERE') dp = Dispatcher(bot) + @dp.message_handler(commands=['start', 'help']) + async def send_welcome(message: types.Message): + await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") + if __name__ == '__main__': executor.start_polling(dp) From 020332ad3ad93c6d73279bbb1359ac8a1457c426 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 15 Jan 2018 14:34:35 +0200 Subject: [PATCH 06/44] Send files with correct names. --- aiogram/bot/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1498f2a3..3f41a167 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -101,7 +101,7 @@ def _compose_data(params=None, files=None): :param files: :return: """ - data = aiohttp.formdata.FormData() + data = aiohttp.formdata.FormData(quote_fields=False) if params: for key, value in params.items(): From 51b84cd78e4dbf847de8264288cc3ec9883d86ff Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 16 Jan 2018 04:24:38 +0200 Subject: [PATCH 07/44] Fixed attachment of media from dict. (InputMedia, MediaGroup) --- aiogram/types/input_media.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 12c9e8ca..259a2b18 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -46,8 +46,8 @@ class InputMediaPhoto(InputMedia): https://core.telegram.org/bots/api#inputmediaphoto """ - def __init__(self, media: base.InputFile, caption: base.String = None): - super(InputMediaPhoto, self).__init__(type='photo', media=media, caption=caption) + def __init__(self, media: base.InputFile, caption: base.String = None, **kwargs): + super(InputMediaPhoto, self).__init__(type='photo', media=media, caption=caption, conf=kwargs) if isinstance(media, (io.IOBase, InputFile)): self.file = media @@ -64,9 +64,9 @@ class InputMediaVideo(InputMedia): duration: base.Integer = fields.Field() def __init__(self, media: base.InputFile, caption: base.String = None, - width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None): + width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, **kwargs): super(InputMediaVideo, self).__init__(type='video', media=media, caption=caption, - width=width, height=height, duration=duration) + width=width, height=height, duration=duration, conf=kwargs) if isinstance(media, (io.IOBase, InputFile)): self.file = media @@ -82,7 +82,7 @@ class MediaGroup(base.TelegramObject): self.media = [] if medias: - self.attach_many(medias) + self.attach_many(*medias) def attach_many(self, *medias: typing.Union[InputMedia, typing.Dict]): """ From 44e61fc6adf9cc9aaad0602fc4a9ec5173247780 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 18 Jan 2018 17:36:24 +0200 Subject: [PATCH 08/44] Set default parse mode. --- aiogram/bot/base.py | 28 +++++++++++++++++++++++++++- aiogram/bot/bot.py | 6 ++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 0617dee9..baa74b84 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional, Union import aiohttp +from aiogram.types import ParseMode from . import api from ..types import base from ..utils import json @@ -18,7 +19,8 @@ class BaseBot: loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None, connections_limit: Optional[base.Integer] = 10, proxy: str = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, - validate_token: Optional[bool] = True): + validate_token: Optional[bool] = True, + parse_mode=None): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot @@ -34,6 +36,8 @@ class BaseBot: :type proxy_auth: Optional :obj:`aiohttp.BasicAuth` :param validate_token: Validate token. :type validate_token: :obj:`bool` + :param parse_mode: You can set default parse mode + :type parse_mode: :obj:`str` :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ # Authentication @@ -61,6 +65,8 @@ class BaseBot: # Data stored in bot instance self._data = {} + self.parse_mode = parse_mode + def __del__(self): self.close() @@ -223,3 +229,23 @@ class BaseBot: :return: value or default value """ return self._data.get(key, default) + + @property + def parse_mode(self): + return getattr(self, '_parse_mode', None) + + @parse_mode.setter + def parse_mode(self, value): + if value is None: + setattr(self, '_parse_mode', None) + else: + if not isinstance(value, str): + raise TypeError(f"Parse mode must be an 'str' not {type(value)}") + value = value.lower() + if value not in ParseMode.all(): + raise ValueError(f"Parse mode must be one of {ParseMode.all()}") + setattr(self, '_parse_mode', value) + + @parse_mode.deleter + def parse_mode(self): + self.parse_mode = None diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index bdc38f94..392ef179 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -197,6 +197,9 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.parse_mode: + payload.setdefault('parse_mode', self.parse_mode) + result = await self.request(api.Methods.SEND_MESSAGE, payload) return types.Message(**result) @@ -1245,6 +1248,9 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) + if self.parse_mode: + payload.setdefault('parse_mode', self.parse_mode) + result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload) if isinstance(result, bool): From d1234880fa4adfb3517b32892eae15a3b83e2756 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 20 Jan 2018 02:12:33 +0300 Subject: [PATCH 09/44] Added Regexp Commands Filter --- aiogram/dispatcher/filters.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index 599b4aef..e2d61027 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -132,6 +132,32 @@ class RegexpFilter(Filter): return bool(self.regexp.search(message.text)) +class RegexpCommandsFilter(AsyncFilter): + """ + Check commands by regexp in message + """ + + def __init__(self, regexp_commands): + self.regexp_commands = [re.compile(command, flags=re.IGNORECASE | re.MULTILINE) for command in regexp_commands] + + async def check(self, message): + if not message.is_command(): + return False + + command = message.text.split()[0][1:] + command, _, mention = command.partition('@') + + if mention and mention != (await message.bot.me).username: + return False + + for command in self.regexp_commands: + search = command.search(message.text) + if search: + message.conf['regexp_command'] = search + return True + return False + + class ContentTypeFilter(Filter): """ Check message content type From 08b7021ca080d160de841e92c9b3e228822f4158 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 20 Jan 2018 02:18:19 +0300 Subject: [PATCH 10/44] Create regexp_commands_filter_example.py --- examples/regexp_commands_filter_example.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/regexp_commands_filter_example.py diff --git a/examples/regexp_commands_filter_example.py b/examples/regexp_commands_filter_example.py new file mode 100644 index 00000000..3cd859db --- /dev/null +++ b/examples/regexp_commands_filter_example.py @@ -0,0 +1,16 @@ +from aiogram import Bot, types +from aiogram.dispatcher import Dispatcher, filters +from aiogram.utils import executor + +bot = Bot(token='TOKEN') +dp = Dispatcher(bot) + + +@dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)'])) +async def send_welcome(message: types.Message): + regexp_command = message.conf['regexp_command'] + await message.reply("You have requested an item with number: {}".format(regexp_command.group(1))) + + +if __name__ == '__main__': + executor.start_polling(dp) From c70789eadbade5c43fe7179c7d3c6233ca515ebc Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 12:48:09 +0300 Subject: [PATCH 11/44] Typos fix in quick_start.rst --- docs/source/quick_start.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index c5ff7e9c..9f35c45a 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -4,15 +4,15 @@ Quick start Simple template --------------- -By first step you need import all modules +At first you have to import all necessary modules .. code-block:: python3 - from aiogram import Bot + from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.utils import executor -In next step you you can initialize bot and dispatcher instances. +Then you have to initialize bot and dispatcher instances. Bot token you can get from `@BotFather `_ @@ -21,7 +21,7 @@ Bot token you can get from `@BotFather `_ bot = Bot(token='BOT TOKEN HERE') dp = Dispatcher(bot) -And next: all bots is needed command for starting interaction with bot. Register first command handler: +Next step: interaction with bots starts with one command. Register your first command handler: .. code-block:: python3 @@ -29,7 +29,7 @@ And next: all bots is needed command for starting interaction with bot. Registe async def send_welcome(message: types.Message): await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") -And last step - run long polling. +Last step: run long polling. .. code-block:: python3 @@ -41,7 +41,7 @@ Summary .. code-block:: python3 - from aiogram import Bot + from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.utils import executor From f708f0c930a28866a5378a32d69f50774ecb89bf Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 13:12:41 +0300 Subject: [PATCH 12/44] Typos fix in docs/source/bot/* --- docs/source/bot/base.rst | 2 +- docs/source/bot/extended.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/bot/base.rst b/docs/source/bot/base.rst index 5fe71588..2c5943fd 100644 --- a/docs/source/bot/base.rst +++ b/docs/source/bot/base.rst @@ -1,7 +1,7 @@ BaseBot ======= -This class is base of bot. In BaseBot implemented only methods for interactions with Telegram Bot API. +This class is the base class for bot. BaseBot implements only methods for interaction with Telegram Bot API. .. autoclass:: aiogram.bot.base.BaseBot :members: diff --git a/docs/source/bot/extended.rst b/docs/source/bot/extended.rst index 71ed12ea..b689f24f 100644 --- a/docs/source/bot/extended.rst +++ b/docs/source/bot/extended.rst @@ -1,8 +1,8 @@ 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` +This is extended (and recommended for use) bot class based on BaseBot class. +You can use an instance of this bot in :obj:`aiogram.dispatcher.Dispatcher` .. autoclass:: aiogram.bot.bot.Bot :members: From ad651a8425a56453e00e5ca02a11af0796b70c73 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 13:20:36 +0300 Subject: [PATCH 13/44] Added info about RethinkDB storage (not sure if it has to be done manually) --- docs/source/contrib/storages.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/contrib/storages.rst b/docs/source/contrib/storages.rst index 6d6c8508..a4f17085 100644 --- a/docs/source/contrib/storages.rst +++ b/docs/source/contrib/storages.rst @@ -16,3 +16,10 @@ Redis storage .. automodule:: aiogram.contrib.fsm_storage.redis :members: :show-inheritance: + +RethinkDB storage +----------------- + +.. automodule:: aiogram.contrib.fsm_storage.rethinkdb + :members: + :show-inheritance: From 5c6008ac6b39ee13761935868e5be6e68f764069 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 13:25:10 +0300 Subject: [PATCH 14/44] Typos fix in docs/source/dispatcher/* --- docs/source/dispatcher/filters.rst | 2 +- docs/source/dispatcher/storage.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index 54b45d15..f01965b3 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -1,7 +1,7 @@ Filters ------- -In this module stored builtin filters for dispatcher. +This module stores builtin filters for dispatcher. .. automodule:: aiogram.dispatcher.filters :members: diff --git a/docs/source/dispatcher/storage.rst b/docs/source/dispatcher/storage.rst index 6551978f..7ab4b5c1 100644 --- a/docs/source/dispatcher/storage.rst +++ b/docs/source/dispatcher/storage.rst @@ -1,7 +1,7 @@ Storages -------- -In this module stored base of storage's for finite-state machine. +This module stores storage base for finite-state machine. .. automodule:: aiogram.dispatcher.storage :members: From 7f169ad7b975f1afa88dcc2c2cd772aec476963b Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 13:37:17 +0300 Subject: [PATCH 15/44] Typos fix in docs/source --- docs/source/index.rst | 14 +++++++------- docs/source/install.rst | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 527a4409..18152c49 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,7 +27,7 @@ Welcome to aiogram's documentation! :alt: MIT License -**aiogram** is are pretty simple and fully asynchronously library for `Telegram Bot API `_ written in Python 3.6 with `asyncio `_ and `aiohttp `_. It helps to make your bots more faster and simpler. +**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.6 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. Official aiogram resources @@ -45,17 +45,17 @@ Features -------- - Asynchronous -- Be awesome -- Make things faster -- Have `FSM `_ -- Can reply into webhook +- Awesome +- Makes things faster +- Has `FSM `_ +- Supports webhook Contribute ---------- -- `Issue Tracker `_ -- `Source Code `_ +- `Issue Tracker `_ +- `Source Code `_ Contents diff --git a/docs/source/install.rst b/docs/source/install.rst index 9d11a6ea..016cba94 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -1,8 +1,8 @@ Installation Guide ================== -From PIP --------- +Using PIP +--------- .. code-block:: bash $ pip install -U aiogram @@ -11,5 +11,6 @@ From sources ------------ .. code-block:: bash - $ git clone https://bitbucket.org/illemius/aiogram.git + $ git clone https://github.com/aiogram/aiogram.git + $ cd aiogram $ python setup.py install From f68629f68bb326f09849916aaed648aaf298f5c6 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 13:50:37 +0300 Subject: [PATCH 16/44] Typo fix, PEP8 formatting fix --- aiogram/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8f9a0594..8ea10ddb 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -4,7 +4,8 @@ try: from .bot import Bot except ImportError as e: if e.name == 'aiohttp': - warnings.warn('Dependencies is not installed!', category=ImportWarning) + warnings.warn('Dependencies are not installed!', + category=ImportWarning) else: raise From 72db167fa32eb12f23503caa4d53fff3f8f465aa Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 15:48:21 +0300 Subject: [PATCH 17/44] Minor aiogram/utils/* typos fixes + PEP8 formatting fixes + super minor code change --- aiogram/utils/context.py | 2 +- aiogram/utils/executor.py | 9 +++++---- aiogram/utils/helper.py | 7 ++++--- aiogram/utils/markdown.py | 13 ++++++++----- aiogram/utils/versions.py | 6 ++++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/aiogram/utils/context.py b/aiogram/utils/context.py index dfef6882..b31c71c0 100644 --- a/aiogram/utils/context.py +++ b/aiogram/utils/context.py @@ -1,5 +1,5 @@ """ -Need setup task factory: +You need to setup task factory: >>> from aiogram.utils import context >>> loop = asyncio.get_event_loop() >>> loop.set_task_factory(context.task_factory) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index b1fec35e..ed8947eb 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -51,7 +51,8 @@ def start_pooling(*args, **kwargs): return start_polling(*args, **kwargs) -def start_polling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, on_shutdown=None): +def start_polling(dispatcher, *, loop=None, skip_updates=False, + on_startup=None, on_shutdown=None): log.warning('Start bot with long-polling.') if loop is None: loop = dispatcher.loop @@ -59,7 +60,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, loop.set_task_factory(context.task_factory) try: - loop.run_until_complete(_startup(dispatcher, skip_updates=skip_updates, callback=on_startup)) + loop.run_until_complete(_startup(dispatcher, skip_updates, on_startup)) loop.create_task(dispatcher.start_polling(reset_webhook=True)) loop.run_forever() except (KeyboardInterrupt, SystemExit): @@ -69,8 +70,8 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, log.warning("Goodbye!") -def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None, - check_ip=False, **kwargs): +def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, + on_startup=None, on_shutdown=None, check_ip=False, **kwargs): log.warning('Start bot with webhook.') if loop is None: loop = dispatcher.loop diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 5b708f18..eeabca7c 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -137,7 +137,8 @@ class Item: """ Helper item - If value is not configured it will be generated automatically based on variable name + If a value is not provided, + it will be automatically generated based on a variable's name """ def __init__(self, value=None): @@ -156,7 +157,7 @@ class Item: class ListItem(Item): """ - This item always is list + This item is always a list You can use &, | and + operators for that. """ @@ -179,7 +180,7 @@ class ItemsList(list): """ Patch for default list - This class provide +, &, |, +=, &=, |= operators for extending the list + This class provides +, &, |, +=, &=, |= operators for extending the list """ def __init__(self, *seq): diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index da08a400..1e64c106 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -18,6 +18,8 @@ HTML_QUOTES_MAP = { '"': '"' } +_HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS + def _join(*content, sep=' '): return sep.join(map(str, content)) @@ -38,21 +40,22 @@ 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 &). + 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 > & 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 + new_content += HTML_QUOTES_MAP[symbol] if symbol in _HQS else symbol return new_content def text(*content, sep=' '): """ - Join all elements with separator + Join all elements with a separator :param content: :param sep: @@ -168,7 +171,7 @@ def hlink(title, url): :param url: :return: """ - return "{1}".format(url, quote_html(title)) + return '{1}'.format(url, quote_html(title)) def escape_md(*content, sep=' '): diff --git a/aiogram/utils/versions.py b/aiogram/utils/versions.py index f1eb00c3..b621bc62 100644 --- a/aiogram/utils/versions.py +++ b/aiogram/utils/versions.py @@ -9,7 +9,8 @@ from .helper import Helper, HelperMode, Item class Version: - def __init__(self, major=0, minor=0, maintenance=0, stage='final', build=0): + def __init__(self, major=0, minor=0, + maintenance=0, stage='final', build=0): self.__raw_version = None self.__version = None @@ -86,7 +87,8 @@ class Version: if git_changeset: sub = '.dev{0}'.format(git_changeset) elif version[3] != Stage.FINAL: - mapping = {Stage.ALPHA: 'a', Stage.BETA: 'b', Stage.RC: 'rc', Stage.DEV: 'dev'} + mapping = {Stage.ALPHA: 'a', Stage.BETA: 'b', + Stage.RC: 'rc', Stage.DEV: 'dev'} sub = mapping[version[3]] + str(version[4]) return str(main + sub) From 3a7cb02acdbd44d3d2aa8993723dea546b009c2e Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 17:02:52 +0300 Subject: [PATCH 18/44] Is a pretty simple... --- README.md | 2 +- README.rst | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 47fce350..1bc5260f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) -**aiogram** is are pretty simple and fully asynchronously library for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.6 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps to make your bots more faster and simpler. +**aiogram** is a pretty simple and fully asynchronous library for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.6 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler. You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). diff --git a/README.rst b/README.rst index 4500460b..57a18bc7 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ AIOGramBot :alt: MIT License -**aiogram** is are pretty simple and fully asynchronously library for `Telegram Bot API `_ written in Python 3.6 with `asyncio `_ and `aiohttp `_. It helps to make your bots more faster and simpler. +**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.6 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. You can `read the docs here `_. diff --git a/setup.py b/setup.py index 9200f803..04da67d2 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( license='MIT', author='Alex Root Junior', author_email='jroot.junior@gmail.com', - description='Is are pretty simple and fully asynchronously library for Telegram Bot API', + description='Is a pretty simple and fully asynchronous library for Telegram Bot API', long_description=get_description(), classifiers=[ VERSION.pypi_development_status, # Automated change classifier by build stage From 57327679e21e37689cd9a309de4222c25a655cea Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Tue, 23 Jan 2018 17:51:59 +0300 Subject: [PATCH 19/44] Very bad mistake fix --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 18152c49..e3f1c8b5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -48,7 +48,7 @@ Features - Awesome - Makes things faster - Has `FSM `_ -- Supports webhook +- Can reply into webhook. (In other words `make requests in response to updates `_) Contribute From 84903060961804b99797a613a81625754203f206 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Jan 2018 02:29:48 +0200 Subject: [PATCH 20/44] Refactor types. --- aiogram/bot/api.py | 2 +- aiogram/types/animation.py | 11 +- aiogram/types/audio.py | 15 +- aiogram/types/base.py | 4 +- aiogram/types/callback_query.py | 35 ++- aiogram/types/chat.py | 271 ++++++++++++++++++++- aiogram/types/chat_member.py | 10 +- aiogram/types/chat_photo.py | 63 +++++ aiogram/types/contact.py | 7 + aiogram/types/document.py | 11 +- aiogram/types/file.py | 23 +- aiogram/types/inline_keyboard.py | 13 +- aiogram/types/inline_query.py | 8 - aiogram/types/input_file.py | 102 ++------ aiogram/types/input_media.py | 11 +- aiogram/types/message.py | 403 +++++++++++++++++++++++++++++-- aiogram/types/mixins.py | 46 ++++ aiogram/types/photo_size.py | 3 +- aiogram/types/reply_keyboard.py | 41 ++-- aiogram/types/update.py | 5 - aiogram/types/user.py | 15 +- 21 files changed, 889 insertions(+), 210 deletions(-) create mode 100644 aiogram/types/mixins.py diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 3f41a167..1b4728c1 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -115,7 +115,7 @@ def _compose_data(params=None, files=None): else: raise ValueError('Tuple must have exactly 2 elements: filename, fileobj') elif isinstance(f, types.InputFile): - filename, fileobj = f.get_filename(), f.get_file() + filename, fileobj = f.filename, f.file else: filename, fileobj = _guess_filename(f) or key, f diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index dcdd4b18..fd470b38 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -1,9 +1,10 @@ from . import base from . import fields +from . import mixins from .photo_size import PhotoSize -class Animation(base.TelegramObject): +class Animation(base.TelegramObject, mixins.Downloadable): """ You can provide an animation for your game so that it looks stylish in chats (check out Lumberjack for an example). @@ -17,11 +18,3 @@ class Animation(base.TelegramObject): file_name: base.String = 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 diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 96a85fc1..8615cfdd 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -1,8 +1,9 @@ from . import base from . import fields +from . import mixins -class Audio(base.TelegramObject): +class Audio(base.TelegramObject, mixins.Downloadable): """ This object represents an audio file to be treated as music by the Telegram clients. @@ -16,9 +17,9 @@ class Audio(base.TelegramObject): 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 + 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/base.py b/aiogram/types/base.py index 2770fd84..60a26485 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -5,12 +5,12 @@ from typing import TypeVar from .fields import BaseField from ..utils import json +__all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean') + PROPS_ATTR_NAME = '_props' VALUES_ATTR_NAME = '_values' ALIASES_ATTR_NAME = '_aliases' -__all__ = ('MetaTelegramObject', 'TelegramObject') - # Binding of builtin types InputFile = TypeVar('InputFile', 'InputFile', io.BytesIO, io.FileIO, str) String = TypeVar('String', bound=str) diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 7ad12271..72ef1604 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -1,3 +1,5 @@ +import typing + from . import base from . import fields from .message import Message @@ -26,10 +28,31 @@ class CallbackQuery(base.TelegramObject): data: base.String = fields.Field() game_short_name: base.String = fields.Field() - def __hash__(self): - return self.id + async def answer(self, text: typing.Union[base.String, None] = None, + show_alert: typing.Union[base.Boolean, None] = None, + url: typing.Union[base.String, None] = None, + cache_time: typing.Union[base.Integer, None] = None): + """ + Use this method to send answers to callback queries sent from inline keyboards. + The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. - def __eq__(self, other): - if isinstance(other, type(self)): - return other.id == self.id - return self.id == other + Alternatively, the user can be redirected to the specified Game URL. + For this option to work, you must first create a game for your bot via @Botfather and accept the terms. + Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter. + + Source: https://core.telegram.org/bots/api#answercallbackquery + + :param text: Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters + :type text: :obj:`typing.Union[base.String, None]` + :param show_alert: If true, an alert will be shown by the client instead of a notification + at the top of the chat screen. Defaults to false. + :type show_alert: :obj:`typing.Union[base.Boolean, None]` + :param url: URL that will be opened by the user's client. + :type url: :obj:`typing.Union[base.String, None]` + :param cache_time: The maximum amount of time in seconds that the + result of the callback query may be cached client-side. + :type cache_time: :obj:`typing.Union[base.Integer, None]` + :return: On success, True is returned. + :rtype: :obj:`base.Boolean`""" + await self.bot.answer_callback_query(callback_query_id=self.id, text=text, + show_alert=show_alert, url=url, cache_time=cache_time) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 392de77b..e86936d4 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -1,4 +1,5 @@ import asyncio +import typing from . import base from . import fields @@ -62,45 +63,303 @@ class Chat(base.TelegramObject): return markdown.link(name, self.user_url) async def set_photo(self, photo): + """ + Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Source: https://core.telegram.org/bots/api#setchatphoto + + :param photo: New chat photo, uploaded using multipart/form-data + :type photo: :obj:`base.InputFile` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.set_chat_photo(self.id, photo) async def delete_photo(self): + """ + Use this method to delete a chat photo. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Source: https://core.telegram.org/bots/api#deletechatphoto + + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.delete_chat_photo(self.id) async def set_title(self, title): + """ + Use this method to change the title of a chat. Titles can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Source: https://core.telegram.org/bots/api#setchattitle + + :param title: New chat title, 1-255 characters + :type title: :obj:`base.String` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.set_chat_title(self.id, title) async def set_description(self, description): + """ + Use this method to change the description of a supergroup or a channel. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Source: https://core.telegram.org/bots/api#setchatdescription + + :param description: New chat description, 0-255 characters + :type description: :obj:`typing.Union[base.String, None]` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.delete_chat_description(self.id, description) + async def kick(self, user_id: base.Integer, + until_date: typing.Union[base.Integer, None] = None): + """ + Use this method to kick a user from a group, a supergroup or a channel. + In the case of supergroups and channels, the user will not be able to return to the group + on their own using invite links, etc., unless unbanned first. + + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ setting + is off in the target group. + Otherwise members may only be removed by the group's creator or by the member that added them. + + Source: https://core.telegram.org/bots/api#kickchatmember + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`base.Integer` + :param until_date: Date when the user will be unbanned, unix time. + :type until_date: :obj:`typing.Union[base.Integer, None]` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date) + + async def unban(self, user_id: base.Integer): + """ + Use this method to unban a previously kicked user in a supergroup or channel. ` + The user will not return to the group or channel automatically, but will be able to join via link, etc. + + The bot must be an administrator for this to work. + + Source: https://core.telegram.org/bots/api#unbanchatmember + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`base.Integer` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + return await self.bot.unban_chat_member(self.id, user_id=user_id) + + async def restrict(self, user_id: base.Integer, + until_date: typing.Union[base.Integer, None] = None, + can_send_messages: typing.Union[base.Boolean, None] = None, + can_send_media_messages: typing.Union[base.Boolean, None] = None, + can_send_other_messages: typing.Union[base.Boolean, None] = None, + can_add_web_page_previews: typing.Union[base.Boolean, None] = None) -> base.Boolean: + """ + Use this method to restrict a user in a supergroup. + The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. + Pass True for all boolean parameters to lift restrictions from a user. + + Source: https://core.telegram.org/bots/api#restrictchatmember + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`base.Integer` + :param until_date: Date when restrictions will be lifted for the user, unix time. + :type until_date: :obj:`typing.Union[base.Integer, None]` + :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues + :type can_send_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos, + video notes and voice notes, implies can_send_messages + :type can_send_media_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_send_other_messages: Pass True, if the user can send animations, games, stickers and + use inline bots, implies can_send_media_messages + :type can_send_other_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, + implies can_send_media_messages + :type can_add_web_page_previews: :obj:`typing.Union[base.Boolean, None]` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + return self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date, + can_send_messages=can_send_messages, + can_send_media_messages=can_send_media_messages, + can_send_other_messages=can_send_other_messages, + can_add_web_page_previews=can_add_web_page_previews) + + async def promote(self, user_id: base.Integer, + can_change_info: typing.Union[base.Boolean, None] = None, + can_post_messages: typing.Union[base.Boolean, None] = None, + can_edit_messages: typing.Union[base.Boolean, None] = None, + can_delete_messages: typing.Union[base.Boolean, None] = None, + can_invite_users: typing.Union[base.Boolean, None] = None, + can_restrict_members: typing.Union[base.Boolean, None] = None, + can_pin_messages: typing.Union[base.Boolean, None] = None, + can_promote_members: typing.Union[base.Boolean, None] = None) -> base.Boolean: + """ + Use this method to promote or demote a user in a supergroup or a channel. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Pass False for all boolean parameters to demote a user. + + Source: https://core.telegram.org/bots/api#promotechatmember + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`base.Integer` + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings + :type can_change_info: :obj:`typing.Union[base.Boolean, None]` + :param can_post_messages: Pass True, if the administrator can create channel posts, channels only + :type can_post_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only + :type can_edit_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_delete_messages: Pass True, if the administrator can delete messages of other users + :type can_delete_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_invite_users: Pass True, if the administrator can invite new users to the chat + :type can_invite_users: :obj:`typing.Union[base.Boolean, None]` + :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members + :type can_restrict_members: :obj:`typing.Union[base.Boolean, None]` + :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only + :type can_pin_messages: :obj:`typing.Union[base.Boolean, None]` + :param can_promote_members: Pass True, if the administrator can add new administrators + with a subset of his own privileges or demote administrators that he has promoted, + directly or indirectly (promoted by administrators that were appointed by him) + :type can_promote_members: :obj:`typing.Union[base.Boolean, None]` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + return self.bot.promote_chat_member(self.id, + can_change_info=can_change_info, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_invite_users=can_invite_users, + can_restrict_members=can_restrict_members, + can_pin_messages=can_pin_messages, + can_promote_members=can_promote_members) + async def pin_message(self, message_id: int, disable_notification: bool = False): + """ + Use this method to pin a message in a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Source: https://core.telegram.org/bots/api#pinchatmessage + + :param message_id: Identifier of a message to pin + :type message_id: :obj:`base.Integer` + :param disable_notification: Pass True, if it is not necessary to send a notification to + all group members about the new pinned message + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.pin_chat_message(self.id, message_id, disable_notification) async def unpin_message(self): + """ + Use this method to unpin a message in a supergroup chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Source: https://core.telegram.org/bots/api#unpinchatmessage + + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.unpin_chat_message(self.id) async def leave(self): + """ + Use this method for your bot to leave a group, supergroup or channel. + + Source: https://core.telegram.org/bots/api#leavechat + + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.leave_chat(self.id) async def get_administrators(self): + """ + Use this method to get a list of administrators in a chat. + + Source: https://core.telegram.org/bots/api#getchatadministrators + + :return: On success, returns an Array of ChatMember objects that contains information about all + chat administrators except other bots. + If the chat is a group or a supergroup and no administrators were appointed, + only the creator will be returned. + :rtype: :obj:`typing.List[types.ChatMember]` + """ return await self.bot.get_chat_administrators(self.id) async def get_members_count(self): + """ + Use this method to get the number of members in a chat. + + Source: https://core.telegram.org/bots/api#getchatmemberscount + + :return: Returns Int on success. + :rtype: :obj:`base.Integer` + """ return await self.bot.get_chat_members_count(self.id) async def get_member(self, user_id): + """ + Use this method to get information about a member of a chat. + + Source: https://core.telegram.org/bots/api#getchatmember + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`base.Integer` + :return: Returns a ChatMember object on success. + :rtype: :obj:`types.ChatMember` + """ return await self.bot.get_chat_member(self.id, user_id) async def do(self, action): + """ + Use this method when you need to tell the user that something is happening on the bot's side. + The status is set for 5 seconds or less + (when a message arrives from your bot, Telegram clients clear its typing status). + + We only recommend using this method when a response from the bot will take + a noticeable amount of time to arrive. + + Source: https://core.telegram.org/bots/api#sendchataction + + :param action: Type of action to broadcast. + :type action: :obj:`base.String` + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ return await self.bot.send_chat_action(self.id, action) - def __hash__(self): - return self.id + async def export_invite_link(self): + """ + Use this method to export an invite link to a supergroup or a channel. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. - def __eq__(self, other): - if isinstance(other, type(self)): - return other.id == self.id - return self.id == other + Source: https://core.telegram.org/bots/api#exportchatinvitelink + + :return: Returns exported invite link as String on success. + :rtype: :obj:`base.String` + """ + if self.invite_link: + return self.invite_link + return await self.bot.export_chat_invite_link(self.id) def __int__(self): return self.id diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index f445d69a..321d77db 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -29,13 +29,11 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() - def __hash__(self): - return self.user.id + def is_admin(self): + return ChatMemberStatus.is_admin(self.status) - def __eq__(self, other): - if isinstance(other, type(self)): - return other.user.id == self.user.id - return self.user.id == other + def is_member(self): + return ChatMemberStatus.is_member(self.status) def __int__(self): return self.user.id diff --git a/aiogram/types/chat_photo.py b/aiogram/types/chat_photo.py index 393682c4..daac874b 100644 --- a/aiogram/types/chat_photo.py +++ b/aiogram/types/chat_photo.py @@ -1,3 +1,6 @@ +import os +import pathlib + from . import base from . import fields @@ -10,3 +13,63 @@ class ChatPhoto(base.TelegramObject): """ small_file_id: base.String = fields.Field() big_file_id: base.String = fields.Field() + + async def download_small(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True): + """ + Download file + + :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` + :param timeout: Integer + :param chunk_size: Integer + :param seek: Boolean - go to start of file when downloading is finished. + :param make_dirs: Make dirs if not exist + :return: destination + """ + file = await self.get_small_file() + + is_path = True + if destination is None: + destination = file.file_path + elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): + os.path.join(destination, file.file_path) + else: + is_path = False + + if is_path and make_dirs: + os.makedirs(os.path.dirname(destination), exist_ok=True) + + return await self.bot.download_file(file_path=file.file_path, destination=destination, timeout=timeout, + chunk_size=chunk_size, seek=seek) + + async def download_big(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True): + """ + Download file + + :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` + :param timeout: Integer + :param chunk_size: Integer + :param seek: Boolean - go to start of file when downloading is finished. + :param make_dirs: Make dirs if not exist + :return: destination + """ + file = await self.get_big_file() + + is_path = True + if destination is None: + destination = file.file_path + elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): + os.path.join(destination, file.file_path) + else: + is_path = False + + if is_path and make_dirs: + os.makedirs(os.path.dirname(destination), exist_ok=True) + + return await self.bot.download_file(file_path=file.file_path, destination=destination, timeout=timeout, + chunk_size=chunk_size, seek=seek) + + async def get_small_file(self): + return await self.bot.get_file(self.small_file_id) + + async def get_big_file(self): + return await self.bot.get_file(self.big_file_id) diff --git a/aiogram/types/contact.py b/aiogram/types/contact.py index c992ea60..5f0eed35 100644 --- a/aiogram/types/contact.py +++ b/aiogram/types/contact.py @@ -12,3 +12,10 @@ class Contact(base.TelegramObject): first_name: base.String = fields.Field() last_name: base.String = fields.Field() user_id: base.Integer = fields.Field() + + @property + def full_name(self): + name = self.first_name + if self.last_name is not None: + name += ' ' + self.last_name + return name diff --git a/aiogram/types/document.py b/aiogram/types/document.py index 8caa6799..32d943d8 100644 --- a/aiogram/types/document.py +++ b/aiogram/types/document.py @@ -1,9 +1,10 @@ from . import base from . import fields +from . import mixins from .photo_size import PhotoSize -class Document(base.TelegramObject): +class Document(base.TelegramObject, mixins.Downloadable): """ This object represents a general file (as opposed to photos, voice messages and audio files). @@ -14,11 +15,3 @@ class Document(base.TelegramObject): file_name: base.String = 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 diff --git a/aiogram/types/file.py b/aiogram/types/file.py index 01436666..f3269f29 100644 --- a/aiogram/types/file.py +++ b/aiogram/types/file.py @@ -1,8 +1,9 @@ from . import base from . import fields +from . import mixins -class File(base.TelegramObject): +class File(base.TelegramObject, mixins.Downloadable): """ This object represents a file ready to be downloaded. @@ -18,23 +19,3 @@ class File(base.TelegramObject): file_id: base.String = fields.Field() file_size: base.Integer = fields.Field() file_path: base.String = fields.Field() - - async def download(self, destination=None, timeout=30, chunk_size=65536, seek=True): - """ - Download file by file_path to destination - - :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` - :param timeout: Integer - :param chunk_size: Integer - :param seek: Boolean - go to start of file when downloading is finished. - :return: destination - """ - return await self.bot.download_file(self.file_path, destination, timeout, chunk_size, seek) - - 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/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 76c47e98..7c859e6e 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -54,10 +54,21 @@ class InlineKeyboardMarkup(base.TelegramObject): """ btn_array = [] for button in args: - btn_array.append(button.to_python()) + btn_array.append(button) self.inline_keyboard.append(btn_array) return self + def insert(self, button): + """ + Insert button to last row + + :param button: + """ + if self.inline_keyboard and len(self.inline_keyboard[-1] < self.row_width): + self.inline_keyboard[-1].append(button) + else: + self.add(button) + class InlineKeyboardButton(base.TelegramObject): """ diff --git a/aiogram/types/inline_query.py b/aiogram/types/inline_query.py index c4050153..a6332990 100644 --- a/aiogram/types/inline_query.py +++ b/aiogram/types/inline_query.py @@ -17,11 +17,3 @@ class InlineQuery(base.TelegramObject): location: Location = fields.Field(base=Location) query: base.String = fields.Field() offset: base.String = fields.Field() - - def __hash__(self): - return self.id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.id == self.id - return self.id == other diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index 3c69b26a..ff5ab1f0 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -1,10 +1,6 @@ import io import logging import os -import tempfile -import time - -import aiohttp from . import base from ..bot import api @@ -36,11 +32,11 @@ class InputFile(base.TelegramObject): self._path = path_or_bytesio if filename is None: filename = os.path.split(path_or_bytesio)[-1] - else: - # As io.BytesIO - assert isinstance(path_or_bytesio, io.IOBase) + elif isinstance(path_or_bytesio, io.IOBase): self._path = None self._file = path_or_bytesio + else: + raise TypeError('Not supported file type.') self._filename = filename @@ -48,14 +44,17 @@ class InputFile(base.TelegramObject): """ Close file descriptor """ - if not hasattr(self, '_file'): - return self._file.close() - del self._file - if self.conf.get('downloaded') and self.conf.get('temp'): - log.debug(f"Unlink file '{self._path}'") - os.unlink(self._path) + @property + def filename(self): + if self._filename is None: + self._filename = api._guess_filename(self._file) + return self._filename + + @filename.setter + def filename(self, value): + self._filename = value def get_filename(self) -> str: """ @@ -63,9 +62,11 @@ class InputFile(base.TelegramObject): :return: name """ - if self._filename is None: - self._filename = api._guess_filename(self._file) - return self._filename + return self.filename + + @property + def file(self): + return self._file def get_file(self): """ @@ -73,74 +74,7 @@ class InputFile(base.TelegramObject): :return: """ - return self._file - - @classmethod - async def from_url(cls, url, filename=None, temp_file=False, chunk_size=65536): - """ - Download file from URL - - Manually is not required action. You can send urls instead! - - :param url: target URL - :param filename: optional. set custom file name - :param temp_file: use temporary file - :param chunk_size: - - :return: InputFile - """ - conf = { - 'downloaded': True, - 'url': url - } - - # Let's do magic with the filename - if filename: - filename_prefix, _, ext = filename.rpartition('.') - file_suffix = '.' + ext if ext else '' - else: - filename_prefix, _, ext = url.rpartition('/')[-1].rpartition('.') - file_suffix = '.' + ext if ext else '' - filename = filename_prefix + file_suffix - - async with aiohttp.ClientSession() as session: - start = time.time() - async with session.get(url) as response: - if temp_file: - # Create temp file - fd, path = tempfile.mkstemp(suffix=file_suffix, prefix=filename_prefix + '_') - file = conf['temp'] = path - - # Save file in temp directory - with open(fd, 'wb') as f: - await cls._process_stream(response, f, chunk_size=chunk_size) - else: - # Save file in memory - file = await cls._process_stream(response, io.BytesIO(), chunk_size=chunk_size) - - log.debug(f"File successful downloaded at {round(time.time() - start, 2)} seconds from '{url}'") - return cls(file, filename, conf=conf) - - @classmethod - async def _process_stream(cls, response, writer, chunk_size=65536): - """ - Transfer data - - :param response: - :param writer: - :param chunk_size: - :return: - """ - while True: - chunk = await response.content.read(chunk_size) - if not chunk: - break - writer.write(chunk) - - if writer.seekable(): - writer.seek(0) - - return writer + return self.file def to_python(self): raise TypeError('Object of this type is not exportable!') diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 259a2b18..e7981462 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -30,13 +30,16 @@ class InputMedia(base.TelegramObject): @file.setter def file(self, file: io.IOBase): setattr(self, '_file', file) - self.media = ATTACHMENT_PREFIX + secrets.token_urlsafe(16) + attachment_key = self.attachment_key = secrets.token_urlsafe(16) + self.media = ATTACHMENT_PREFIX + attachment_key @property def attachment_key(self): - if self.media.startswith(ATTACHMENT_PREFIX): - return self.media[len(ATTACHMENT_PREFIX):] - return None + return self.conf.get('attachment_key', None) + + @attachment_key.setter + def attachment_key(self, value): + self.conf['attachment_key'] = value class InputMediaPhoto(InputMedia): diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 01543962..453ee3eb 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -21,7 +21,6 @@ from .video import Video from .video_note import VideoNote from .voice import Voice from ..utils import helper -from ..utils.payload import generate_payload class Message(base.TelegramObject): @@ -177,7 +176,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) -> 'Message': + disable_notification=None, reply_markup=None, reply=False) -> 'Message': """ Reply to this message @@ -186,10 +185,360 @@ class Message(base.TelegramObject): :param disable_web_page_preview: bool :param disable_notification: bool :param reply_markup: - :return: :class:`aoigram.types.Message` + :param reply: fill 'reply_to_message_id' + :return: :class:`aiogram.types.Message` """ - return await self.bot.send_message(self.chat.id, text, parse_mode, disable_web_page_preview, - disable_notification, self.message_id, reply_markup) + return await self.bot.send_message(self.chat.id, text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_photo(self, photo: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, reply=True) -> 'Message': + """ + Use this method to send photos. + + Source: https://core.telegram.org/bots/api#sendphoto + + :param photo: Photo to send. + :type photo: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Photo caption (may also be used when resending photos by file_id), 0-200 characters + :type caption: :obj:`typing.Union[base.String, None]` + :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_photo(self.chat.id, photo=photo, caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_audio(self, audio: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + performer: typing.Union[base.String, None] = None, + title: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send audio files, if you want Telegram clients to display them in the music player. + Your audio must be in the .mp3 format. + + For sending voice messages, use the sendVoice method instead. + + Source: https://core.telegram.org/bots/api#sendaudio + + :param audio: Audio file to send. + :type audio: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Audio caption, 0-200 characters + :type caption: :obj:`typing.Union[base.String, None]` + :param duration: Duration of the audio in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param performer: Performer + :type performer: :obj:`typing.Union[base.String, None]` + :param title: Track name + :type title: :obj:`typing.Union[base.String, None]` + :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_audio(self.chat.id, + audio=audio, + caption=caption, + duration=duration, + performer=performer, + title=title, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_document(self, document: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send general files. + + Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + + Source: https://core.telegram.org/bots/api#senddocument + + :param document: File to send. + :type document: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Document caption (may also be used when resending documents by file_id), 0-200 characters + :type caption: :obj:`typing.Union[base.String, None]` + :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_document(self.chat.id, + document=document, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_video(self, video: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send video files, Telegram clients support mp4 videos + (other formats may be sent as Document). + + Source: https://core.telegram.org/bots/api#sendvideo + + :param video: Video to send. + :type video: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param width: Video width + :type width: :obj:`typing.Union[base.Integer, None]` + :param height: Video height + :type height: :obj:`typing.Union[base.Integer, None]` + :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters + :type caption: :obj:`typing.Union[base.String, None]` + :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_video(self.chat.id, + video=video, + duration=duration, + width=width, + height=height, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_voice(self, voice: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send audio files, if you want Telegram clients to display the file + as a playable voice message. + + For this to work, your audio must be in an .ogg file encoded with OPUS + (other formats may be sent as Audio or Document). + + Source: https://core.telegram.org/bots/api#sendvoice + + :param voice: Audio file to send. + :type voice: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Voice message caption, 0-200 characters + :type caption: :obj:`typing.Union[base.String, None]` + :param duration: Duration of the voice message in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :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_voice(self.chat.id, + voice=voice, + caption=caption, + duration=duration, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_video_note(self, video_note: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + length: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + Use this method to send video messages. + + Source: https://core.telegram.org/bots/api#sendvideonote + + :param video_note: Video note to send. + :type video_note: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param length: Video width and height + :type length: :obj:`typing.Union[base.Integer, None]` + :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_video_note(self.chat.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def reply_media_group(self, media: typing.Union['MediaGroup', typing.List], + disable_notification: typing.Union[base.Boolean, None] = None, + reply=True) -> typing.List['Message']: + """ + Use this method to send a group of photos or videos as an album. + + Source: https://core.telegram.org/bots/api#sendmediagroup + + :param media: A JSON-serialized array describing photos and videos to be sent + :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, an array of the sent Messages is returned. + :rtype: typing.List[types.Message] + """ + return await self.bot.send_media_group(self.chat.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None) + + async def reply_location(self, latitude: base.Float, + longitude: base.Float, live_period: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send point on the map. + + Source: https://core.telegram.org/bots/api#sendlocation + + :param latitude: Latitude of the location + :type latitude: :obj:`base.Float` + :param longitude: Longitude of the location + :type longitude: :obj:`base.Float` + :param live_period: Period in seconds for which the location will be updated + :type live_period: :obj:`typing.Union[base.Integer, None]` + :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_location(self.chat.id, + latitude=latitude, + longitude=longitude, + live_period=live_period, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send information about a venue. + + Source: https://core.telegram.org/bots/api#sendvenue + + :param latitude: Latitude of the venue + :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue + :type longitude: :obj:`base.Float` + :param title: Name of the venue + :type title: :obj:`base.String` + :param address: Address of the venue + :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue + :type foursquare_id: :obj:`typing.Union[base.String, None]` + :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_venue(self.chat.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def send_contact(self, phone_number: base.String, + first_name: base.String, last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> 'Message': + """ + Use this method to send phone contacts. + + Source: https://core.telegram.org/bots/api#sendcontact + + :param phone_number: Contact's phone number + :type phone_number: :obj:`base.String` + :param first_name: Contact's first name + :type first_name: :obj:`base.String` + :param last_name: Contact's last name + :type last_name: :obj:`typing.Union[base.String, None]` + :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_contact(self.chat.id, + phone_number=phone_number, + first_name=first_name, last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) async def forward(self, chat_id, disable_notification=None) -> 'Message': """ @@ -204,12 +553,30 @@ class Message(base.TelegramObject): async def edit_text(self, text: base.String, parse_mode: typing.Union[base.String, None] = None, disable_web_page_preview: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union['types.InlineKeyboardMarkup', - None] = None): - payload = generate_payload(**locals()) - payload['message_id'] = self.message_id - payload['chat_id'] = self.chat.id - return await self.bot.edit_message_text(**payload) + reply_markup=None): + """ + Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). + + Source: https://core.telegram.org/bots/api#editmessagetext + + :param text: New text of the message + :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, + fixed-width text or inline URLs in your bot's message. + :type parse_mode: :obj:`typing.Union[base.String, None]` + :param disable_web_page_preview: Disables link previews for links in this message + :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: A JSON-serialized object for an inline keyboard. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :return: On success, if edited message is sent by the bot, + the edited Message is returned, otherwise True is returned. + :rtype: :obj:`typing.Union[types.Message, base.Boolean]` + """ + return await self.bot.edit_message_text(text=text, + chat_id=self.chat.id, message_id=self.message_id, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup) async def delete(self): """ @@ -220,16 +587,14 @@ class Message(base.TelegramObject): return await self.bot.delete_message(self.chat.id, self.message_id) async def pin(self, disable_notification: bool = False): + """ + Pin message + + :param disable_notification: + :return: + """ return await self.chat.pin_message(self.message_id, disable_notification) - def __hash__(self): - return self.message_id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.message_id == self.message_id - return self.message_id == other - def __int__(self): return self.message_id diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py new file mode 100644 index 00000000..0d0a42f9 --- /dev/null +++ b/aiogram/types/mixins.py @@ -0,0 +1,46 @@ +import os +import pathlib + + +class Downloadable: + """ + Mixin for files + """ + + async def download(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True): + """ + Download file + + :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` + :param timeout: Integer + :param chunk_size: Integer + :param seek: Boolean - go to start of file when downloading is finished. + :param make_dirs: Make dirs if not exist + :return: destination + """ + file = await self.get_file() + + is_path = True + if destination is None: + destination = file.file_path + elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): + os.path.join(destination, file.file_path) + else: + is_path = False + + if is_path and make_dirs: + os.makedirs(os.path.dirname(destination), exist_ok=True) + + return await self.bot.download_file(file_path=file.file_path, destination=destination, timeout=timeout, + chunk_size=chunk_size, seek=seek) + + async def get_file(self): + """ + Get file information + + :return: :obj:`aiogram.types.File` + """ + if hasattr(self, 'file_path'): + return self + else: + return await self.bot.get_file(self.file_id) diff --git a/aiogram/types/photo_size.py b/aiogram/types/photo_size.py index 8b6bfcdf..c7ba59b6 100644 --- a/aiogram/types/photo_size.py +++ b/aiogram/types/photo_size.py @@ -1,8 +1,9 @@ from . import base from . import fields +from . import mixins -class PhotoSize(base.TelegramObject): +class PhotoSize(base.TelegramObject, mixins.Downloadable): """ This object represents one size of a photo or a file / sticker thumbnail. diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 27a595c2..c9f89d71 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -33,32 +33,45 @@ class ReplyKeyboardMarkup(base.TelegramObject): self.conf['row_width'] = value def add(self, *args): - i = 1 + """ + Add buttons + + :param args: + :return: + """ row = [] - for button in args: - if isinstance(button, str): - row.append({'text': button}) - elif isinstance(button, bytes): - row.append({'text': button.decode('utf-8')}) - else: - row.append(button.to_python()) - if i % self.row_width == 0: + for index, button in enumerate(args): + row.append(button) + if index % self.row_width == 0: self.keyboard.append(row) row = [] - i += 1 if len(row) > 0: self.keyboard.append(row) def row(self, *args): + """ + Add row + + :param args: + :return: + """ btn_array = [] for button in args: - if isinstance(button, str): - btn_array.append({'text': button}) - else: - btn_array.append(button.to_python()) + btn_array.append(button) self.keyboard.append(btn_array) return self + def insert(self, button): + """ + Insert button to last row + + :param button: + """ + if self.keyboard and len(self.keyboard[-1] < self.row_width): + self.keyboard[-1].append(button) + else: + self.add(button) + class KeyboardButton(base.TelegramObject): """ diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 210a3343..7f9cf11a 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -30,11 +30,6 @@ class Update(base.TelegramObject): def __hash__(self): return self.update_id - def __eq__(self, other): - if isinstance(other, type(self)): - return other.update_id == self.update_id - return self.update_id == other - def __int__(self): return self.update_id diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 537c091e..aa37a6af 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -64,7 +64,10 @@ class User(base.TelegramObject): def url(self): return f"tg://user?id={self.id}" - def get_mention(self, name=None, as_html=False): + def get_mention(self, name=None, as_html=None): + if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html': + as_html = True + if name is None: name = self.mention if as_html: @@ -75,12 +78,10 @@ class User(base.TelegramObject): return await self.bot.get_user_profile_photos(self.id, offset, limit) def __hash__(self): - return self.id - - def __eq__(self, other): - if isinstance(other, type(self)): - return other.id == self.id - return self.id == other + return self.id + \ + hash(self.is_bot) + \ + hash(self.full_name) + \ + (hash(self.username) if self.username else 0) def __int__(self): return self.id From 221ce362fc02d8580aa40743cab6bab32fbfc10b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Jan 2018 02:30:07 +0200 Subject: [PATCH 21/44] Update .gitignore --- .gitignore | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.gitignore b/.gitignore index 1300ec27..925f8dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -39,10 +39,6 @@ htmlcov/ nosetests.xml coverage.xml *,cover -.hypothesis/ - -# Scrapy stuff: -.scrapy # Sphinx documentation docs/_build/ @@ -50,22 +46,11 @@ docs/_build/ # pyenv .python-version -# SageMath parsed files -*.sage.py - -# dotenv -.env - # virtualenv .venv venv/ ENV/ -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 From a18ea8297423c6a11d7115b2d3cda4927499f944 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Jan 2018 02:33:42 +0200 Subject: [PATCH 22/44] Fixed overlapping of names in context module. --- aiogram/utils/context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/context.py b/aiogram/utils/context.py index b31c71c0..ea922b48 100644 --- a/aiogram/utils/context.py +++ b/aiogram/utils/context.py @@ -46,10 +46,10 @@ def get_current_state() -> typing.Dict: :rtype: :obj:`dict` """ task = asyncio.Task.current_task() - context = getattr(task, 'context', None) - if context is None: - context = task.context = {} - return context + context_ = getattr(task, 'context', None) + if context_ is None: + context_ = task.context = {} + return context_ def get_value(key, default=None): From ee1f445845b2c25fdf59d2b49c3bd5fd0a2a3b33 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Jan 2018 02:35:35 +0200 Subject: [PATCH 23/44] #7 Change Optional[str] to Optional[Dict] --- 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 622e4182..b1e6377f 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -76,7 +76,7 @@ class BaseStorage: 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: + default: typing.Optional[typing.Dict] = None) -> typing.Dict: """ Get state-data for user in chat. Return `default` if data is not presented in storage. From 07b9f3ee3eadabb0304598b5e422be685c3607fb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 24 Jan 2018 04:00:54 +0200 Subject: [PATCH 24/44] Fix payload generator. --- aiogram/utils/payload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py index 8ab7467d..277573c9 100644 --- a/aiogram/utils/payload.py +++ b/aiogram/utils/payload.py @@ -19,7 +19,7 @@ def generate_payload(exclude=None, **kwargs): exclude = [] return {key: value for key, value in kwargs.items() if key not in exclude + DEFAULT_FILTER - and value + and value is not None and not key.startswith('_')} From 9571608f7e042a49c5ad1a2e0f38fbb447ba7bf5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 03:34:53 +0200 Subject: [PATCH 25/44] Small changes in annotations. --- aiogram/bot/base.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index baa74b84..7bcdd0c9 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -4,9 +4,8 @@ from typing import Dict, List, Optional, Union import aiohttp -from aiogram.types import ParseMode from . import api -from ..types import base +from ..types import ParseMode, base from ..utils import json @@ -18,8 +17,8 @@ class BaseBot: def __init__(self, token: base.String, loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None, connections_limit: Optional[base.Integer] = 10, - proxy: str = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, - validate_token: Optional[bool] = True, + proxy: Optional[base.String] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, + validate_token: Optional[base.Boolean] = True, parse_mode=None): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot @@ -80,7 +79,7 @@ class BaseBot: if self.session and not self.session.closed: self.session.close() - def create_temp_session(self, limit: int = 1, force_close: bool = False) -> aiohttp.ClientSession: + def create_temp_session(self, limit: base.Integer = 1, force_close: base.Boolean = False) -> aiohttp.ClientSession: """ Create temporary session From 77f5b4f03afedeb1b53a85551f56ab5a39bed71a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 03:39:01 +0200 Subject: [PATCH 26/44] Edit/stop live location from message object. --- aiogram/types/message.py | 59 +++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 453ee3eb..23933e5a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -188,7 +188,7 @@ class Message(base.TelegramObject): :param reply: fill 'reply_to_message_id' :return: :class:`aiogram.types.Message` """ - return await self.bot.send_message(self.chat.id, text, + return await self.bot.send_message(chat_id=self.chat.id, text=text, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, @@ -217,7 +217,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_photo(self.chat.id, photo=photo, caption=caption, + return await self.bot.send_photo(chat_id=self.chat.id, photo=photo, caption=caption, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) @@ -257,7 +257,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_audio(self.chat.id, + return await self.bot.send_audio(chat_id=self.chat.id, audio=audio, caption=caption, duration=duration, @@ -292,7 +292,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_document(self.chat.id, + return await self.bot.send_document(chat_id=self.chat.id, document=document, caption=caption, disable_notification=disable_notification, @@ -332,7 +332,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video(self.chat.id, + return await self.bot.send_video(chat_id=self.chat.id, video=video, duration=duration, width=width, @@ -372,7 +372,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_voice(self.chat.id, + return await self.bot.send_voice(chat_id=self.chat.id, voice=voice, caption=caption, duration=duration, @@ -407,7 +407,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video_note(self.chat.id, + return await self.bot.send_video_note(chat_id=self.chat.id, video_note=video_note, duration=duration, length=length, @@ -461,7 +461,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_location(self.chat.id, + return await self.bot.send_location(chat_id=self.chat.id, latitude=latitude, longitude=longitude, live_period=live_period, @@ -469,6 +469,45 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def edit_live_location(self, latitude: base.Float, longitude: base.Float, + reply_markup=None) -> 'Message' or base.Boolean: + """ + Use this method to edit live location messages sent by the bot or via the bot (for inline bots). + A location can be edited until its live_period expires or editing is explicitly disabled by a call + to stopMessageLiveLocation. + + Source: https://core.telegram.org/bots/api#editmessagelivelocation + + :param latitude: Latitude of new location + :type latitude: :obj:`base.Float` + :param longitude: Longitude of new location + :type longitude: :obj:`base.Float` + :param reply_markup: A JSON-serialized object for a new inline keyboard. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :return: On success, if the edited message was sent by the bot, the edited Message is returned, + otherwise True is returned. + :rtype: :obj:`typing.Union[types.Message, base.Boolean]` + """ + return await self.bot.edit_message_live_location(latitude=latitude, longitude=longitude, + chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + + async def stop_live_location(self, reply_markup=None) -> 'Message' or base.Boolean: + """ + Use this method to stop updating a live location message sent by the bot or via the bot + (for inline bots) before live_period expires. + + Source: https://core.telegram.org/bots/api#stopmessagelivelocation + + :param reply_markup: A JSON-serialized object for a new inline keyboard. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :return: On success, if the message was sent by the bot, the sent Message is returned, + otherwise True is returned. + :rtype: :obj:`typing.Union[types.Message, base.Boolean]` + """ + return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, @@ -498,7 +537,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_venue(self.chat.id, + return await self.bot.send_venue(chat_id=self.chat.id, latitude=latitude, longitude=longitude, title=title, @@ -533,7 +572,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_contact(self.chat.id, + return await self.bot.send_contact(chat_id=self.chat.id, phone_number=phone_number, first_name=first_name, last_name=last_name, disable_notification=disable_notification, From 7c40b974bb7ce933674ba8dfa2162ed827205423 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 05:26:51 +0200 Subject: [PATCH 27/44] Parse text mention from entities. --- aiogram/types/message_entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 76f51327..24e6da5f 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -40,6 +40,8 @@ class MessageEntity(base.TelegramObject): return self._apply(text, lambda url: markdown.link(url, url)) elif self.type == MessageEntityType.TEXT_LINK: return self._apply(text, lambda url: markdown.link(url, self.url)) + if self.type == MessageEntityType.TEXT_MENTION and self.user: + return self._apply(text, lambda name: self.user.get_mention(name, as_html=False)) return text def apply_html(self, text): @@ -61,6 +63,8 @@ class MessageEntity(base.TelegramObject): return self._apply(text, lambda url: markdown.hlink(url, url)) elif self.type == MessageEntityType.TEXT_LINK: return self._apply(text, lambda url: markdown.hlink(url, self.url)) + if self.type == MessageEntityType.TEXT_MENTION and self.user: + return self._apply(text, lambda name: self.user.get_mention(name, as_html=True)) return text From f874310965f89c913d1c16a7a029b0d5f2b8a2c6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 07:20:52 +0200 Subject: [PATCH 28/44] Refactor webhook responses. --- aiogram/dispatcher/webhook.py | 177 +++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 22 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index e98635f9..1711bffe 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -10,8 +10,10 @@ from aiohttp import web from .. import types from ..bot import api +from ..types import ParseMode from ..types.base import Boolean, Float, Integer, String from ..utils import context +from ..utils import helper, markdown from ..utils import json from ..utils.deprecated import warn_deprecated as warn from ..utils.exceptions import TimeoutWarning @@ -319,8 +321,24 @@ class BaseResponse: :param bot: Bot instance. :return: """ + method_name = helper.HelperMode.apply(self.method, helper.HelperMode.snake_case) + method = getattr(bot, method_name, None) + if method: + return await method(**self.cleanup()) return await bot.request(self.method, self.cleanup()) + async def __call__(self, bot=None): + if bot is None: + from aiogram.dispatcher import ctx + bot = ctx.get_bot() + return await self.execute_response(bot) + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + return await self() + class ReplyToMixin: """ @@ -337,8 +355,80 @@ class ReplyToMixin: setattr(self, 'reply_to_message_id', message.message_id if isinstance(message, types.Message) else message) return self + def to(self, target: typing.Union[types.Message, types.Chat, types.base.Integer, types.base.String]): + """ + Send to chat -class SendMessage(BaseResponse, ReplyToMixin): + :param target: message or chat or id + :return: + """ + if isinstance(target, types.Message): + chat_id = target.chat.id + elif isinstance(target, types.Chat): + chat_id = target.id + elif isinstance(target, (int, str)): + chat_id = target + else: + raise TypeError(f"Bad type of target. ({type(target)})") + + setattr(self, 'chat_id', chat_id) + return self + + +class DisableNotificationMixin: + def without_notification(self): + """ + Disable notification + + :return: + """ + setattr(self, 'disable_notification', True) + return self + + +class DisableWebPagePreviewMixin: + def no_web_page_preview(self): + """ + Disable web page preview + + :return: + """ + setattr(self, 'disable_web_page_preview', True) + return self + + +class ParseModeMixin: + def as_html(self): + """ + Set parse_mode to HTML + + :return: + """ + setattr(self, 'parse_mode', ParseMode.HTML) + return self + + def as_markdown(self): + """ + Set parse_mode to Markdown + + :return: + """ + setattr(self, 'parse_mode', ParseMode.MARKDOWN) + return self + + @staticmethod + def _global_parse_mode(): + """ + Detect global parse mode + + :return: + """ + bot = context.get_value('bot', None) + if bot is not None: + return bot.parse_mode + + +class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificationMixin, DisableWebPagePreviewMixin): """ You can send message with webhook by using this instance of this object. All arguments is equal with Bot.send_message method. @@ -350,8 +440,8 @@ class SendMessage(BaseResponse, ReplyToMixin): method = api.Methods.SEND_MESSAGE - def __init__(self, chat_id: Union[Integer, String], - text: String, + def __init__(self, chat_id: Union[Integer, String] = None, + text: String = None, parse_mode: Optional[String] = None, disable_web_page_preview: Optional[Boolean] = None, disable_notification: Optional[Boolean] = None, @@ -372,6 +462,11 @@ class SendMessage(BaseResponse, ReplyToMixin): - 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. """ + if text is None: + text = '' + if parse_mode is None: + parse_mode = self._global_parse_mode() + self.chat_id = chat_id self.text = text self.parse_mode = parse_mode @@ -391,8 +486,32 @@ class SendMessage(BaseResponse, ReplyToMixin): 'reply_markup': prepare_arg(self.reply_markup) } + def write(self, *text, sep=' '): + """ + Write text to response -class ForwardMessage(BaseResponse): + :param text: + :param sep: + :return: + """ + self.text += markdown.text(*text, sep) + return self + + def write_ln(self, *text, sep=' '): + """ + Write line + + :param text: + :param sep: + :return: + """ + if self.text and self.text[-1] != '\n': + self.text += '\n' + self.text += markdown.text(*text, sep) + '\n' + return self + + +class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for forward messages of any kind on to webhook. """ @@ -400,9 +519,9 @@ class ForwardMessage(BaseResponse): method = api.Methods.FORWARD_MESSAGE - def __init__(self, chat_id: Union[Integer, String], - from_chat_id: Union[Integer, String], - message_id: Integer, + def __init__(self, chat_id: Union[Integer, String] = None, + from_chat_id: Union[Integer, String] = None, + message_id: Integer = None, disable_notification: Optional[Boolean] = None): """ :param chat_id: Union[Integer, String] - Unique identifier for the target chat or username of the @@ -418,6 +537,17 @@ class ForwardMessage(BaseResponse): self.message_id = message_id self.disable_notification = disable_notification + def message(self, message: types.Message): + """ + Select target message + + :param message: + :return: + """ + setattr(self, 'from_chat_id', message.chat.id) + setattr(self, 'message_id', message.message_id) + return self + def prepare(self) -> dict: return { 'chat_id': self.chat_id, @@ -427,7 +557,7 @@ class ForwardMessage(BaseResponse): } -class SendPhoto(BaseResponse, ReplyToMixin): +class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send photo on to webhook. """ @@ -476,7 +606,7 @@ class SendPhoto(BaseResponse, ReplyToMixin): } -class SendAudio(BaseResponse, ReplyToMixin): +class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send audio on to webhook. """ @@ -538,7 +668,7 @@ class SendAudio(BaseResponse, ReplyToMixin): } -class SendDocument(BaseResponse, ReplyToMixin): +class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send document on to webhook. """ @@ -588,7 +718,7 @@ class SendDocument(BaseResponse, ReplyToMixin): } -class SendVideo(BaseResponse, ReplyToMixin): +class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send video on to webhook. """ @@ -651,7 +781,7 @@ class SendVideo(BaseResponse, ReplyToMixin): } -class SendVoice(BaseResponse, ReplyToMixin): +class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send voice on to webhook. """ @@ -705,7 +835,7 @@ class SendVoice(BaseResponse, ReplyToMixin): } -class SendVideoNote(BaseResponse, ReplyToMixin): +class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send video note on to webhook. """ @@ -758,7 +888,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin): } -class SendMediaGroup(BaseResponse): +class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use this method to send a group of photos or videos as an album. """ @@ -839,7 +969,7 @@ class SendMediaGroup(BaseResponse): return self -class SendLocation(BaseResponse, ReplyToMixin): +class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send location on to webhook. """ @@ -884,7 +1014,7 @@ class SendLocation(BaseResponse, ReplyToMixin): } -class SendVenue(BaseResponse, ReplyToMixin): +class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send venue on to webhook. """ @@ -943,7 +1073,7 @@ class SendVenue(BaseResponse, ReplyToMixin): } -class SendContact(BaseResponse, ReplyToMixin): +class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send contact on to webhook. """ @@ -1268,7 +1398,7 @@ class SetChatDescription(BaseResponse): } -class PinChatMessage(BaseResponse): +class PinChatMessage(BaseResponse, DisableNotificationMixin): """ Use that response type for pin chat message on to webhook. """ @@ -1387,7 +1517,7 @@ class AnswerCallbackQuery(BaseResponse): } -class EditMessageText(BaseResponse): +class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin): """ Use that response type for edit message text on to webhook. """ @@ -1419,6 +1549,9 @@ class EditMessageText(BaseResponse): :param reply_markup: types.InlineKeyboardMarkup (Optional) - A JSON-serialized object for an inline keyboard. """ + if parse_mode is None: + parse_mode = self._global_parse_mode() + self.chat_id = chat_id self.message_id = message_id self.inline_message_id = inline_message_id @@ -1541,7 +1674,7 @@ class DeleteMessage(BaseResponse): } -class SendSticker(BaseResponse, ReplyToMixin): +class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send sticker on to webhook. """ @@ -1787,7 +1920,7 @@ class AnswerInlineQuery(BaseResponse): } -class SendInvoice(BaseResponse, ReplyToMixin): +class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send invoice on to webhook. """ @@ -1968,7 +2101,7 @@ class AnswerPreCheckoutQuery(BaseResponse): } -class SendGame(BaseResponse, ReplyToMixin): +class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): """ Use that response type for send game on to webhook. """ From ba44ca67fabca9b9c2ac00ba6046b67fd2208748 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 07:45:46 +0200 Subject: [PATCH 29/44] Small changes. --- aiogram/bot/__init__.py | 4 +++- aiogram/contrib/fsm_storage/rethinkdb.py | 25 ++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/aiogram/bot/__init__.py b/aiogram/bot/__init__.py index 3d871235..252c465b 100644 --- a/aiogram/bot/__init__.py +++ b/aiogram/bot/__init__.py @@ -1,7 +1,9 @@ +from . import api from .base import BaseBot from .bot import Bot __all__ = [ 'BaseBot', - 'Bot' + 'Bot', + 'api' ] diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index ed8733b1..cb84a59f 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -1,6 +1,3 @@ -# -*- coding:utf-8; -*- -__all__ = ['RethinkDBStorage'] - import asyncio import typing @@ -8,6 +5,7 @@ import rethinkdb as r from ...dispatcher import BaseStorage +__all__ = ['RethinkDBStorage', 'ConnectionNotClosed'] r.set_loop_type('asyncio') @@ -36,6 +34,7 @@ class RethinkDBStorage(BaseStorage): await storage.close() """ + def __init__(self, host='localhost', port=28015, db='aiogram', table='aiogram', auth_key=None, user=None, password=None, timeout=20, ssl=None, loop=None): self._host = host @@ -51,15 +50,17 @@ class RethinkDBStorage(BaseStorage): self._connection: r.Connection = None self._loop = loop or asyncio.get_event_loop() self._lock = asyncio.Lock(loop=self._loop) - + async def 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) + 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 async def close(self): @@ -99,7 +100,8 @@ class RethinkDBStorage(BaseStorage): else: await r.table(self._table).insert({'id': chat, user: {'state': state}}).run(conn) - async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): + 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() if await r.table(self._table).get(chat).run(conn): @@ -107,7 +109,8 @@ class RethinkDBStorage(BaseStorage): else: await r.table(self._table).insert({'id': chat, user: {'data': data}}).run(conn) - async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None, + 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() @@ -125,7 +128,8 @@ class RethinkDBStorage(BaseStorage): conn = await self.connection() return await r.table(self._table).get(chat)[user]['bucket'].default(default or {}).run(conn) - async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, bucket: typing.Dict = None): + 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() if await r.table(self._table).get(chat).run(conn): @@ -133,7 +137,8 @@ class RethinkDBStorage(BaseStorage): else: await r.table(self._table).insert({'id': chat, user: {'bucket': bucket}}).run(conn) - async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, bucket: typing.Dict = None, + 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() From 47a865aa5f4864888e698ec43a9a6c7695a6e317 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 08:15:34 +0200 Subject: [PATCH 30/44] More usable ChatType checker. Allow to use message or chat instances. --- aiogram/types/chat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index e86936d4..dcb38876 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -384,9 +384,11 @@ class ChatType(helper.Helper): @staticmethod def _check(obj, chat_types) -> bool: - if not hasattr(obj, 'chat'): + if hasattr(obj, 'chat'): + obj = obj.chat + if not hasattr(obj, 'type'): return False - return obj.chat.type in chat_types + return obj.type in chat_types @classmethod def is_private(cls, obj) -> bool: From e0e058e943f0495a96a10c099f36cf5a3d1457e9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 09:54:19 +0200 Subject: [PATCH 31/44] Update setup script and requirements list. --- dev_requirements.txt | 16 +++++++++------- requirements.txt | 13 ++----------- setup.py | 28 +++++++++++++++------------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 92eaaa4b..1d976556 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,8 +1,10 @@ -r requirements.txt -ujson -emoji -pytest -pytest-asyncio -uvloop -aioredis -rethinkdb + +ujson>=1.35 +emoji>=0.4.5 +pytest>=3.3.0 +pytest-asyncio>=0.8.0 +uvloop>=0.9.1 +aioredis>=1.0.0 +wheel>=0.30.0 +rethinkdb>=2.3.0 diff --git a/requirements.txt b/requirements.txt index 66620d60..8a63284c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,2 @@ -aiohttp>=2.1.0 -appdirs>=1.4.3 -async-timeout>=1.2.1 -Babel>=2.4.0 -chardet>=3.0.3 -multidict>=2.1.6 -packaging>=16.8 -pyparsing>=2.2.0 -pytz>=2017.2 -six>=1.10.0 -yarl>=0.10.2 +aiohttp>=2.3.5 +Babel>=2.5.1 diff --git a/setup.py b/setup.py index 04da67d2..95377b79 100755 --- a/setup.py +++ b/setup.py @@ -2,9 +2,10 @@ from distutils.core import setup +from pip.req import parse_requirements from setuptools import PackageFinder -from aiogram import VERSION +from aiogram import Stage, VERSION def get_description(): @@ -14,7 +15,7 @@ def get_description(): :return: description :rtype: str """ - with open('README.rst', encoding='utf-8') as f: + with open('README.rst', 'r', encoding='utf-8') as f: return f.read() @@ -25,17 +26,16 @@ def get_requirements(): :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) + filename = 'requirements.txt' + if VERSION.stage == Stage.DEV: + filename = 'dev_' + filename - return requirements + install_reqs = parse_requirements(filename, session='hack') + return [str(ir.req) for ir in install_reqs] +install_requires = get_requirements() + setup( name='aiogram', version=VERSION.version, @@ -48,11 +48,13 @@ setup( long_description=get_description(), classifiers=[ VERSION.pypi_development_status, # Automated change classifier by build stage - 'Programming Language :: Python :: 3.6', 'Environment :: Console', 'Framework :: AsyncIO', - 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.6', + 'Topic :: Software Development :: Libraries :: Application Frameworks', ], - install_requires=get_requirements() + install_requires=install_requires ) From 05399ecaa119baa2631d496f34e5093fa6f4b2cb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 11:13:12 +0200 Subject: [PATCH 32/44] Add recommendations to installations instruction. --- dev_requirements.txt | 2 ++ docs/source/install.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1d976556..93a20597 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,5 @@ uvloop>=0.9.1 aioredis>=1.0.0 wheel>=0.30.0 rethinkdb>=2.3.0 +sphinx>=1.6.6 +sphinx-rtd-theme>=0.2.4 diff --git a/docs/source/install.rst b/docs/source/install.rst index 016cba94..3191305f 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -14,3 +14,31 @@ From sources $ git clone https://github.com/aiogram/aiogram.git $ cd aiogram $ python setup.py install + + +Recommendations +--------------- +You can speedup your bots by following next instructions: + +- Use `uvloop `_ instead of default asyncio loop. + + *uvloop* is a fast, drop-in replacement of the built-in asyncio event loop. uvloop is implemented in Cython and uses libuv under the hood. + + **Installation:** + + .. code-block:: bash + + $ pip install uvloop + + +- Use `ujson `_ instead of default json module. + + *UltraJSON* is an ultra fast JSON encoder and decoder written in pure C with bindings for Python 2.5+ and 3. + + **Installation:** + + .. code-block:: bash + + $ pip install ujson + +In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment. From b0e62b0522245a5824e24b8bdbcd0eca62e58330 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 11:46:08 +0200 Subject: [PATCH 33/44] Update types doc. --- docs/source/types.rst | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/source/types.rst b/docs/source/types.rst index 8eae8ad3..1afe64b1 100644 --- a/docs/source/types.rst +++ b/docs/source/types.rst @@ -3,9 +3,6 @@ Data types Bases ----- -:class:`aiogram.types.base.Serializable` - -:class:`aiogram.types.base.Deserializable` .. automodule:: aiogram.types.base :members: @@ -210,6 +207,16 @@ ResponseParameters :members: :show-inheritance: +InputMedia +---------- +:class:`aiogram.types.InputMediaPhoto` +:class:`aiogram.types.InputMediaVideo` +:class:`aiogram.types.MediaGroup` + +.. automodule:: aiogram.types.input_media + :members: + :show-inheritance: + Sticker ------- :class:`aiogram.types.Sticker` @@ -299,3 +306,13 @@ Games .. automodule:: aiogram.types.game_high_score :members: :show-inheritance: + + +InputFile interface +------------------- + +:class:`aiogram.types.InputFile` + +.. automodule:: aiogram.types.input_file + :members: + :show-inheritance: From 67bcfb4772da82679e4b1d1fc4ada34812f60604 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 12:12:54 +0200 Subject: [PATCH 34/44] Use User.full_name instead User.mention in User.get_mention() method. --- aiogram/types/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index aa37a6af..c8864f8d 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -69,7 +69,7 @@ class User(base.TelegramObject): as_html = True if name is None: - name = self.mention + name = self.full_name if as_html: return markdown.hlink(name, self.url) return markdown.link(name, self.url) From c598a7d82a8f1aa1a91f5ca54ebbcc4d6503466f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 22:38:22 +0200 Subject: [PATCH 35/44] Update tests (small fixes) --- tests/types/test_chat.py | 2 +- tests/types/test_game.py | 1 + tests/types/test_message.py | 2 +- tests/types/test_update.py | 2 +- tests/types/test_user.py | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/types/test_chat.py b/tests/types/test_chat.py index c2b6de4a..1caa228d 100644 --- a/tests/types/test_chat.py +++ b/tests/types/test_chat.py @@ -13,7 +13,7 @@ def test_export(): def test_id(): assert isinstance(chat.id, int) assert chat.id == CHAT['id'] - assert hash(chat) == CHAT['id'] + # assert hash(chat) == CHAT['id'] def test_name(): diff --git a/tests/types/test_game.py b/tests/types/test_game.py index c81809f3..9a051160 100644 --- a/tests/types/test_game.py +++ b/tests/types/test_game.py @@ -3,6 +3,7 @@ from .dataset import GAME game = types.Game(**GAME) + def test_export(): exported = game.to_python() assert isinstance(exported, dict) diff --git a/tests/types/test_message.py b/tests/types/test_message.py index 8071207e..8751d064 100644 --- a/tests/types/test_message.py +++ b/tests/types/test_message.py @@ -13,7 +13,7 @@ def test_export(): def test_message_id(): - assert hash(message) == MESSAGE['message_id'] + # assert hash(message) == MESSAGE['message_id'] assert message.message_id == MESSAGE['message_id'] assert message['message_id'] == MESSAGE['message_id'] diff --git a/tests/types/test_update.py b/tests/types/test_update.py index 72b97571..6b724a23 100644 --- a/tests/types/test_update.py +++ b/tests/types/test_update.py @@ -12,7 +12,7 @@ def test_export(): def test_update_id(): assert isinstance(update.update_id, int) - assert hash(update) == UPDATE['update_id'] + # assert hash(update) == UPDATE['update_id'] assert update.update_id == UPDATE['update_id'] diff --git a/tests/types/test_user.py b/tests/types/test_user.py index ae8413aa..585e6fc0 100644 --- a/tests/types/test_user.py +++ b/tests/types/test_user.py @@ -15,7 +15,7 @@ def test_export(): def test_id(): assert isinstance(user.id, int) assert user.id == USER['id'] - assert hash(user) == USER['id'] + # assert hash(user) == USER['id'] def test_bot(): @@ -40,7 +40,7 @@ def test_full_name(): def test_mention(): assert user.mention == f"@{USER['username']}" - assert user.get_mention('foo') == f"[foo](tg://user?id={USER['id']})" + assert user.get_mention('foo', as_html=False) == f"[foo](tg://user?id={USER['id']})" assert user.get_mention('foo', as_html=True) == f"foo" From 26d5ff446398b1abe5690027c9b3b23f3ec0042a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 22:44:46 +0200 Subject: [PATCH 36/44] Fix packages list. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 95377b79..1d999d2e 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ install_requires = get_requirements() setup( name='aiogram', version=VERSION.version, - packages=PackageFinder.find(exclude=('tests', 'examples', 'docs',)), + packages=PackageFinder.find(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), url='https://github.com/aiogram/aiogram', license='MIT', author='Alex Root Junior', From 112e38f5b1a7702e96e4ff9aed5459b4ba094d83 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 27 Jan 2018 23:35:08 +0200 Subject: [PATCH 37/44] Change version number. Oops. I forgot commit that. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8ea10ddb..ddd63c0f 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 0, 5, stage=Stage.DEV, build=0) +VERSION = Version(1, 1, 0, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 5) __version__ = VERSION.version From 104ffd32d0b9b1ab16d379926b49b46325b433a1 Mon Sep 17 00:00:00 2001 From: Andrey Pikelner Date: Sat, 3 Feb 2018 01:14:50 +0100 Subject: [PATCH 38/44] Added left_chat_member state to the Message class --- aiogram/types/message.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 23933e5a..058a508b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -100,6 +100,8 @@ class Message(base.TelegramObject): return ContentType.VENUE[0] if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS[0] + if self.left_chat_member: + return ContentType.LEFT_CHAT_MEMBER if self.invoice: return ContentType.INVOICE[0] if self.successful_payment: @@ -655,6 +657,7 @@ class ContentType(helper.Helper): :key: LOCATION :key: VENUE :key: NEW_CHAT_MEMBERS + :key: LEFT_CHAT_MEMBER :key: INVOICE :key: SUCCESSFUL_PAYMENT """ @@ -673,6 +676,7 @@ class ContentType(helper.Helper): LOCATION = helper.ListItem() # location VENUE = helper.ListItem() # venue NEW_CHAT_MEMBERS = helper.ListItem() # new_chat_member + LEFT_CHAT_MEMBER = helper.Item() # left_chat_member INVOICE = helper.ListItem() # invoice SUCCESSFUL_PAYMENT = helper.ListItem() # successful_payment From 01519e35e2281504b0673ede6ecf7978f640b261 Mon Sep 17 00:00:00 2001 From: Andrey Pikelner Date: Sat, 3 Feb 2018 10:01:17 +0100 Subject: [PATCH 39/44] LEFT_CHAT_MEMBER switched to list --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 058a508b..9ff020f4 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -101,7 +101,7 @@ class Message(base.TelegramObject): if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS[0] if self.left_chat_member: - return ContentType.LEFT_CHAT_MEMBER + return ContentType.LEFT_CHAT_MEMBER[0] if self.invoice: return ContentType.INVOICE[0] if self.successful_payment: @@ -676,7 +676,7 @@ class ContentType(helper.Helper): LOCATION = helper.ListItem() # location VENUE = helper.ListItem() # venue NEW_CHAT_MEMBERS = helper.ListItem() # new_chat_member - LEFT_CHAT_MEMBER = helper.Item() # left_chat_member + LEFT_CHAT_MEMBER = helper.ListItem() # left_chat_member INVOICE = helper.ListItem() # invoice SUCCESSFUL_PAYMENT = helper.ListItem() # successful_payment From 7c9290784016c087b0bc36f6617843b9a5995155 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 8 Feb 2018 21:19:06 +0200 Subject: [PATCH 40/44] Upd description of `exportChatInviteLink` --- aiogram/bot/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 392ef179..86c3d350 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -907,8 +907,8 @@ class Bot(BaseBot): async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: """ - Use this method to export an invite link to a supergroup or a channel. - The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Use this method to generate a new invite link for a chat; any previously generated link is revoked. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Source: https://core.telegram.org/bots/api#exportchatinvitelink From e3b79ea81e012cef427468dcfd295a99af87a2b6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 8 Feb 2018 21:34:36 +0200 Subject: [PATCH 41/44] Don't set default commands. --- aiogram/dispatcher/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index dc378439..99b26e99 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -444,8 +444,6 @@ class Dispatcher: :param kwargs: :return: decorated function """ - if commands is None: - commands = [] if content_types is None: content_types = ContentType.TEXT if custom_filters is None: @@ -509,8 +507,6 @@ class Dispatcher: :param kwargs: :return: decorated function """ - if commands is None: - commands = [] if content_types is None: content_types = ContentType.TEXT if custom_filters is None: @@ -566,8 +562,6 @@ class Dispatcher: :param kwargs: :return: decorated function """ - if commands is None: - commands = [] if content_types is None: content_types = ContentType.TEXT if custom_filters is None: From 3c99b171f642a8ba4d983cc8d649ae808915e731 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Feb 2018 12:50:28 +0300 Subject: [PATCH 42/44] Minor typos fixes --- aiogram/bot/api.py | 5 ++--- aiogram/bot/bot.py | 8 ++++---- aiogram/types/audio.py | 10 +++++----- aiogram/types/chat.py | 4 ++-- aiogram/types/fields.py | 6 +++--- aiogram/types/message_entity.py | 4 ++-- examples/media_group.py | 4 ++-- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1b4728c1..6b952f39 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -1,13 +1,12 @@ -import asyncio -import logging import os +import logging from http import HTTPStatus import aiohttp from .. import types -from ..utils import exceptions from ..utils import json +from ..utils import exceptions from ..utils.helper import Helper, HelperMode, Item # Main aiogram logger diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 392ef179..61c215d9 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -60,7 +60,7 @@ class Bot(BaseBot): typing.Union[typing.List[base.String], None] = None) -> typing.List[types.Update]: """ Use this method to receive incoming updates using long polling (wiki). - + Notes 1. This method will not work if an outgoing webhook is set up. 2. In order to avoid getting duplicate updates, recalculate offset after each server response. @@ -132,7 +132,7 @@ class Bot(BaseBot): async def get_webhook_info(self) -> types.WebhookInfo: """ Use this method to get current webhook status. Requires no parameters. - + If the bot is using getUpdates, will return an object with the url field empty. Source: https://core.telegram.org/bots/api#getwebhookinfo @@ -180,7 +180,7 @@ class Bot(BaseBot): :type chat_id: :obj:`typing.Union[base.Integer, base.String]` :param text: Text of the message to be sent :type text: :obj:`base.String` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. :type parse_mode: :obj:`typing.Union[base.String, None]` :param disable_web_page_preview: Disables link previews for links in this message @@ -190,7 +190,7 @@ class Bot(BaseBot): :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Union[base.Integer, None]` :param reply_markup: Additional interface options. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 8615cfdd..7946c28a 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -18,8 +18,8 @@ class Audio(base.TelegramObject, mixins.Downloadable): def __hash__(self): return hash(self.file_id) + \ - self.duration + \ - hash(self.performer) + \ - hash(self.title) + \ - hash(self.mime_type) + \ - self.file_size + self.duration + \ + hash(self.performer) + \ + hash(self.title) + \ + hash(self.mime_type) + \ + self.file_size diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index dcb38876..a696e90e 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -40,7 +40,7 @@ class Chat(base.TelegramObject): @property def mention(self): """ - Get mention if dialog have username or full name if this is Private dialog otherwise None + Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned """ if self.username: return '@' + self.username @@ -51,7 +51,7 @@ class Chat(base.TelegramObject): @property def user_url(self): if self.type != ChatType.PRIVATE: - raise TypeError('This property available only in private chats.') + raise TypeError('`user_url` property is only available in private chats!') return f"tg://user?id={self.id}" diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index 401d562c..fc12dd2e 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -15,8 +15,8 @@ class BaseField(metaclass=abc.ABCMeta): :param base: class for child element :param default: default value - :param alias: alias name (for e.g. field named 'from' must be has name 'from_user' - ('from' is builtin Python keyword) + :param alias: alias name (for e.g. field 'from' has to be named 'from_user' + as 'from' is a builtin Python keyword """ self.base_object = base self.default = default @@ -34,7 +34,7 @@ class BaseField(metaclass=abc.ABCMeta): def get_value(self, instance): """ - Get value for current object instance + Get value for the current object instance :param instance: :return: diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 24e6da5f..46733f3a 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/examples/media_group.py b/examples/media_group.py index 0f4efc1b..0545cec9 100644 --- a/examples/media_group.py +++ b/examples/media_group.py @@ -14,13 +14,13 @@ dp = Dispatcher(bot) @dp.message_handler(commands=['start']) async def send_welcome(message: types.Message): - # So... By first i want to send something like that: + # So... At first I want to send something like this: await message.reply("Do you want to see many pussies? Are you ready?") # And wait few seconds... await asyncio.sleep(1) - # Good bots always must be send chat actions. Or not. + # Good bots should send chat actions. Or not. await ChatActions.upload_photo() # Create media group From 5a905abe87db3c0b375f7df8c372d510c4ebbb6e Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Feb 2018 12:57:26 +0300 Subject: [PATCH 43/44] Fixed ReplyKeyboardMarkup.insert method --- aiogram/types/reply_keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index c9f89d71..197202c2 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -67,7 +67,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): :param button: """ - if self.keyboard and len(self.keyboard[-1] < self.row_width): + if self.keyboard and len(self.keyboard[-1]) < self.row_width: self.keyboard[-1].append(button) else: self.add(button) From 819a212b559d63a2b2fefa52a9f71e82e28db181 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 14 Feb 2018 14:48:13 +0200 Subject: [PATCH 44/44] Implemented features from the latest Bot API 3.6. --- aiogram/__init__.py | 4 ++-- aiogram/bot/bot.py | 9 ++++++--- aiogram/types/input_media.py | 23 +++++++++++++++++++---- aiogram/types/message.py | 1 + 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ddd63c0f..d9d32776 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,8 +20,8 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 1, 0, stage=Stage.FINAL, build=0) -API_VERSION = Version(3, 5) +VERSION = Version(1, 1, 1, stage=Stage.DEV, build=0) +API_VERSION = Version(3, 6) __version__ = VERSION.version __api_version__ = API_VERSION.version diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 86c3d350..8aa3b0fe 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -356,6 +356,7 @@ class Bot(BaseBot): width: typing.Union[base.Integer, None] = None, height: typing.Union[base.Integer, None] = None, caption: typing.Union[base.String, None] = None, + supports_streaming: typing.Union[base.Boolean, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, reply_to_message_id: typing.Union[base.Integer, None] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, @@ -363,8 +364,8 @@ class Bot(BaseBot): types.ReplyKeyboardRemove, types.ForceReply, None] = None) -> types.Message: """ - Use this method to send video files, Telegram clients support mp4 videos - (other formats may be sent as Document). + Use this method to send video files, Telegram clients support mp4 videos + (other formats may be sent as Document). Source: https://core.telegram.org/bots/api#sendvideo @@ -380,12 +381,14 @@ class Bot(BaseBot): :type height: :obj:`typing.Union[base.Integer, None]` :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters :type caption: :obj:`typing.Union[base.String, None]` + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming + :type supports_streaming: :obj:`typing.Union[base.Boolean, None]` :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_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Union[base.Integer, None]` :param reply_markup: Additional interface options. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index e7981462..2ae4da4f 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -22,6 +22,15 @@ class InputMedia(base.TelegramObject): type: base.String = fields.Field(default='photo') media: base.String = fields.Field() caption: base.String = fields.Field() + parse_mode: base.Boolean = fields.Field() + + def __init__(self, *args, **kwargs): + super(InputMedia, self).__init__(*args, **kwargs) + try: + if self.parse_mode is None and self.bot.parse_mode: + self.parse_mode = self.bot.parse_mode + except RuntimeError: + pass @property def file(self): @@ -49,8 +58,9 @@ class InputMediaPhoto(InputMedia): https://core.telegram.org/bots/api#inputmediaphoto """ - def __init__(self, media: base.InputFile, caption: base.String = None, **kwargs): - super(InputMediaPhoto, self).__init__(type='photo', media=media, caption=caption, conf=kwargs) + def __init__(self, media: base.InputFile, caption: base.String = None, parse_mode: base.Boolean = None, **kwargs): + super(InputMediaPhoto, self).__init__(type='photo', media=media, caption=caption, parse_mode=parse_mode, + conf=kwargs) if isinstance(media, (io.IOBase, InputFile)): self.file = media @@ -65,11 +75,16 @@ class InputMediaVideo(InputMedia): width: base.Integer = fields.Field() height: base.Integer = fields.Field() duration: base.Integer = fields.Field() + supports_streaming: base.Boolean = fields.Field() def __init__(self, media: base.InputFile, caption: base.String = None, - width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, **kwargs): + width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, + parse_mode: base.Boolean = None, + supports_streaming: base.Boolean = None, **kwargs): super(InputMediaVideo, self).__init__(type='video', media=media, caption=caption, - width=width, height=height, duration=duration, conf=kwargs) + width=width, height=height, duration=duration, + parse_mode=parse_mode, + supports_streaming=supports_streaming, conf=kwargs) if isinstance(media, (io.IOBase, InputFile)): self.file = media diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9ff020f4..1e78598c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -70,6 +70,7 @@ class Message(base.TelegramObject): pinned_message: 'Message' = fields.Field(base='Message') invoice: Invoice = fields.Field(base=Invoice) successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment) + connected_website: base.String = fields.Field() @property @functools.lru_cache()