Merge branch 'dev-1.x' of https://github.com/aiogram/aiogram into dev-1.x

This commit is contained in:
Arslan 'Ars2014' Sakhapov 2018-02-14 18:43:57 +05:00
commit 4a9533ded5
60 changed files with 1433 additions and 387 deletions

15
.gitignore vendored
View file

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

View file

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

View file

@ -30,7 +30,7 @@ AIOGramBot
:alt: MIT License
**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/>`_.

View file

@ -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
@ -19,8 +20,8 @@ else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
VERSION = Version(1, 0, 5, stage=Stage.DEV, 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

View file

@ -1,7 +1,9 @@
from . import api
from .base import BaseBot
from .bot import Bot
__all__ = [
'BaseBot',
'Bot'
'Bot',
'api'
]

View file

@ -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
@ -101,7 +100,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():
@ -115,7 +114,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

View file

@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union
import aiohttp
from . import api
from ..types import base
from ..types import ParseMode, base
from ..utils import json
@ -17,8 +17,9 @@ 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
@ -34,6 +35,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 +64,8 @@ class BaseBot:
# Data stored in bot instance
self._data = {}
self.parse_mode = parse_mode
def __del__(self):
self.close()
@ -74,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
@ -223,3 +228,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

View file

@ -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,13 +190,16 @@ 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`
"""
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)
@ -353,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,
@ -360,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
@ -377,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`
@ -904,8 +910,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
@ -1245,6 +1251,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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,13 @@ from typing import TypeVar
from .fields import BaseField
from ..utils import json
from ..utils.context import get_value
__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)
@ -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

View file

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

View file

@ -1,4 +1,5 @@
import asyncio
import typing
from . import base
from . import fields
@ -39,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
@ -50,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}"
@ -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
@ -125,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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,10 +54,21 @@ class InlineKeyboardMarkup(base.TelegramObject):
"""
btn_array = []
for button in args:
btn_array.append(button.to_json())
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):
"""

View file

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

View file

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

View file

@ -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):
@ -30,13 +39,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):
@ -46,8 +58,9 @@ 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, 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
@ -62,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):
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)
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
@ -82,7 +100,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]):
"""

View file

@ -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):
@ -71,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()
@ -101,6 +101,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[0]
if self.invoice:
return ContentType.INVOICE[0]
if self.successful_payment:
@ -177,7 +179,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 +188,399 @@ 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(chat_id=self.chat.id, text=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(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)
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(chat_id=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(chat_id=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(chat_id=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(chat_id=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(chat_id=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(chat_id=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 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,
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(chat_id=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(chat_id=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 +595,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 +629,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
@ -251,6 +658,7 @@ class ContentType(helper.Helper):
:key: LOCATION
:key: VENUE
:key: NEW_CHAT_MEMBERS
:key: LEFT_CHAT_MEMBER
:key: INVOICE
:key: SUCCESSFUL_PAYMENT
"""
@ -269,6 +677,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.ListItem() # left_chat_member
INVOICE = helper.ListItem() # invoice
SUCCESSFUL_PAYMENT = helper.ListItem() # successful_payment

View file

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

46
aiogram/types/mixins.py Normal file
View file

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

View file

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

View file

@ -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_json())
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_json())
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):
"""

View file

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

View file

@ -64,9 +64,12 @@ 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
name = self.full_name
if as_html:
return markdown.hlink(name, self.url)
return markdown.link(name, self.url)
@ -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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,8 @@ HTML_QUOTES_MAP = {
'"': '&quot;'
}
_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 &lt;, > with &gt; and & with &amp;).
All <, >, & and " symbols that are not a part of a tag or
an HTML entity must be replaced with the corresponding HTML entities
(< with &lt; > with &gt; & with &amp and " with &quot).
: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 "<a href=\"{0}\">{1}</a>".format(url, quote_html(title))
return '<a href="{0}">{1}</a>'.format(url, quote_html(title))
def escape_md(*content, sep=' '):

View file

@ -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('_')}

View file

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

View file

@ -1,8 +1,12 @@
-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
sphinx>=1.6.6
sphinx-rtd-theme>=0.2.4

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <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.
Official aiogram resources
@ -45,17 +45,17 @@ Features
--------
- Asynchronous
- Be awesome
- Make things faster
- Have `FSM <https://en.wikipedia.org/wiki/Finite-state_machine>`_
- Can reply into webhook
- Awesome
- Makes things faster
- Has `FSM <https://en.wikipedia.org/wiki/Finite-state_machine>`_
- Can reply into webhook. (In other words `make requests in response to updates <https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates>`_)
Contribute
----------
- `Issue Tracker <https://bitbucket.org/illemius/aiogram/issues>`_
- `Source Code <https://bitbucket.org/illemius/aiogram.git>`_
- `Issue Tracker <https://github.com/aiogram/aiogram/issues>`_
- `Source Code <https://github.com/aiogram/aiogram.git>`_
Contents

View file

@ -1,8 +1,8 @@
Installation Guide
==================
From PIP
--------
Using PIP
---------
.. code-block:: bash
$ pip install -U aiogram
@ -11,5 +11,34 @@ 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
Recommendations
---------------
You can speedup your bots by following next instructions:
- Use `uvloop <https://github.com/MagicStack/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 <https://github.com/esnme/ultrajson>`_ 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.

View file

@ -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 <https://t.me/BotFather>`_
@ -21,7 +21,7 @@ Bot token you can get from `@BotFather <https://t.me/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,12 +41,16 @@ Summary
.. code-block:: python3
from aiogram import Bot
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
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)

View file

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

View file

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

View file

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

View file

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

View file

@ -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,34 +26,35 @@ 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,
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',
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
'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
)

View file

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

View file

@ -3,6 +3,7 @@ from .dataset import GAME
game = types.Game(**GAME)
def test_export():
exported = game.to_python()
assert isinstance(exported, dict)

View file

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

View file

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

View file

@ -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"<a href=\"tg://user?id={USER['id']}\">foo</a>"