Clean project

This commit is contained in:
jrootjunior 2019-11-15 12:17:57 +02:00
parent a83dd3ca63
commit bdae5fb026
259 changed files with 1303 additions and 21135 deletions

View file

@ -1,5 +1,5 @@
[flake8]
max-line-length = 80
max-line-length = 99
select = C,E,F,W,B,B950
ignore = E501,W503,E203
exclude =

69
.gitignore vendored
View file

@ -1,65 +1,12 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.pytest_cache/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Sphinx documentation
docs/_build/
# virtualenv
.venv
venv/
ENV/
# JetBrains
.idea/
# Current project
experiment.py
__pycache__/
*.py[cod]
# Doc's
docs/html
env/
build/
dist/
*.egg-info/
*.egg
# i18n/l10n
*.mo
.mypy_cache
.mypy_cache

View file

@ -1,19 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: dev_requirements.txt

View file

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jroot.junior@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View file

@ -1,49 +1,42 @@
VENV_NAME := venv
PYTHON := $(VENV_NAME)/bin/python
AIOGRAM_VERSION := $(shell $(PYTHON) -c "import aiogram;print(aiogram.__version__)")
.DEFAULT_GOAL := help
RM := rm -rf
python := python3.7
mkvenv:
virtualenv $(VENV_NAME)
$(PYTHON) -m pip install -r requirements.txt
clean:
find . -name '*.pyc' -exec $(RM) {} +
find . -name '*.pyo' -exec $(RM) {} +
find . -name '*~' -exec $(RM) {} +
find . -name '__pycache__' -exec $(RM) {} +
$(RM) build/ dist/ docs/build/ .tox/ .cache/ .pytest_cache/ *.egg-info
tag:
@echo "Add tag: '$(AIOGRAM_VERSION)'"
git tag v$(AIOGRAM_VERSION)
build:
$(PYTHON) setup.py sdist bdist_wheel
upload:
twine upload dist/*
release:
make clean
make test
make build
make tag
@echo "Released aiogram $(AIOGRAM_VERSION)"
full-release:
make release
make upload
.PHONY: help
help:
@echo "======================================================================================="
@echo " aiogram build tools "
@echo "======================================================================================="
@echo "Commands list:"
@echo " install: Install development dependencies"
@echo " isort: Run isort tool"
@echo " black: Run black tool"
@echo " flake8: Run flake8 tool"
@echo " mypy: Run mypy tool"
@echo " lint: Run isort, black, flake8 and mypy tools"
@echo ""
@echo ""
.PHONY: install
install:
$(PYTHON) setup.py install
$(python) -m pip install --user -U poetry
poetry install
test:
tox
.PHONY: isort
isort:
poetry run isort -rc aiogram tests
summary:
cloc aiogram/ tests/ examples/ setup.py
.PHONY: black
black:
poetry run black aiogram tests
docs: docs/source/*
cd docs && $(MAKE) html
.PHONY: flake8
flake8:
poetry run flake8 aiogram tests
.PHONY: mypy
mypy:
poetry run mypy aiogram tests
.PHONY: lint
lint: isort black flake8 mypy

View file

@ -1,57 +0,0 @@
# AIOGram
[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?style=flat-square)](https://opencollective.com/aiogram)
[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live)
[![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest)
[![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 a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 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/).
## Official aiogram resources:
- News: [@aiogram_live](https://t.me/aiogram_live)
- Community: [@aiogram](https://t.me/aiogram)
- Russian community: [@aiogram_ru](https://t.me/aiogram_ru)
- Pip: [aiogram](https://pypi.python.org/pypi/aiogram)
- Docs: [ReadTheDocs](http://aiogram.readthedocs.io)
- Source: [Github repo](https://github.com/aiogram/aiogram)
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)].
<a href="https://github.com/aiogram/aiogram/graphs/contributors"><img src="https://opencollective.com/aiogram/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)]
#### Individuals
<a href="https://opencollective.com/aiogram"><img src="https://opencollective.com/aiogram/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)]
<a href="https://opencollective.com/aiogram/organization/0/website"><img src="https://opencollective.com/aiogram/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/1/website"><img src="https://opencollective.com/aiogram/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/2/website"><img src="https://opencollective.com/aiogram/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/3/website"><img src="https://opencollective.com/aiogram/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/4/website"><img src="https://opencollective.com/aiogram/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/5/website"><img src="https://opencollective.com/aiogram/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/6/website"><img src="https://opencollective.com/aiogram/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/7/website"><img src="https://opencollective.com/aiogram/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/8/website"><img src="https://opencollective.com/aiogram/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/aiogram/organization/9/website"><img src="https://opencollective.com/aiogram/organization/9/avatar.svg"></a>

View file

@ -1,42 +1,14 @@
import asyncio
import os
from . import bot
from . import contrib
from . import dispatcher
from . import types
from . import utils
from .bot import Bot
from .dispatcher import Dispatcher
from .dispatcher import filters
from .dispatcher import middlewares
from .utils import exceptions, executor, helper, markdown as md
from .api import methods, session, types
from .api.client.bot import Bot
try:
import uvloop
uvloop.install()
except ImportError:
uvloop = None
else:
if 'DISABLE_UVLOOP' not in os.environ:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
__all__ = [
'Bot',
'Dispatcher',
'__api_version__',
'__version__',
'bot',
'contrib',
'dispatcher',
'exceptions',
'executor',
'filters',
'helper',
'md',
'middlewares',
'types',
'utils'
]
__all__ = ["__api_version__", "__version__", "types", "methods", "Bot", "session"]
__version__ = '2.4'
__api_version__ = '4.4'
__version__ = "3.0dev.1"
__api_version__ = "4.4"

View file

@ -1,83 +0,0 @@
import platform
import sys
import aiohttp
import aiogram
from aiogram.utils import json
class SysInfo:
@property
def os(self):
return platform.platform()
@property
def python_implementation(self):
return platform.python_implementation()
@property
def python(self):
return sys.version.replace("\n", "")
@property
def aiogram(self):
return aiogram.__version__
@property
def api(self):
return aiogram.__api_version__
@property
def uvloop(self):
try:
import uvloop
except ImportError:
return
return uvloop.__version__
@property
def ujson(self):
try:
import ujson
except ImportError:
return
return ujson.__version__
@property
def rapidjson(self):
try:
import rapidjson
except ImportError:
return
return rapidjson.__version__
@property
def aiohttp(self):
return aiohttp.__version__
def collect(self):
yield f"{self.python_implementation}: {self.python}"
yield f"OS: {self.os}"
yield f"aiogram: {self.aiogram}"
yield f"aiohttp: {self.aiohttp}"
uvloop = self.uvloop
if uvloop:
yield f"uvloop: {uvloop}"
yield f"JSON mode: {json.mode}"
rapidjson = self.rapidjson
if rapidjson:
yield f"rapidjson: {rapidjson}"
ujson = self.ujson
if ujson:
yield f"ujson: {ujson}"
def __str__(self):
return "\n".join(self.collect())
if __name__ == "__main__":
print(SysInfo())

View file

@ -146,7 +146,7 @@ class Bot(BaseBot):
:return: An Array of Update objects is returned.
"""
call = GetUpdates(
offset=offset, limit=limit, timeout=timeout, allowed_updates=allowed_updates,
offset=offset, limit=limit, timeout=timeout, allowed_updates=allowed_updates
)
return await self.emit(call)
@ -1007,7 +1007,7 @@ class Bot(BaseBot):
)
return await self.emit(call)
async def send_chat_action(self, chat_id: Union[int, str], action: str,) -> bool:
async def send_chat_action(self, chat_id: Union[int, str], action: str) -> bool:
"""
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,
@ -1030,11 +1030,11 @@ class Bot(BaseBot):
data, record_video_note or upload_video_note for video notes.
:return: Returns True on success.
"""
call = SendChatAction(chat_id=chat_id, action=action,)
call = SendChatAction(chat_id=chat_id, action=action)
return await self.emit(call)
async def get_user_profile_photos(
self, user_id: int, offset: Optional[int] = None, limit: Optional[int] = None,
self, user_id: int, offset: Optional[int] = None, limit: Optional[int] = None
) -> UserProfilePhotos:
"""
Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos
@ -1049,10 +1049,10 @@ class Bot(BaseBot):
accepted. Defaults to 100.
:return: Returns a UserProfilePhotos object.
"""
call = GetUserProfilePhotos(user_id=user_id, offset=offset, limit=limit,)
call = GetUserProfilePhotos(user_id=user_id, offset=offset, limit=limit)
return await self.emit(call)
async def get_file(self, file_id: str,) -> File:
async def get_file(self, file_id: str) -> File:
"""
Use this method to get basic info about a file and prepare it for downloading. For the
moment, bots can download files of up to 20MB in size. On success, a File object is
@ -1068,7 +1068,7 @@ class Bot(BaseBot):
:param file_id: File identifier to get info about
:return: On success, a File object is returned.
"""
call = GetFile(file_id=file_id,)
call = GetFile(file_id=file_id)
return await self.emit(call)
async def kick_chat_member(
@ -1094,10 +1094,10 @@ class Bot(BaseBot):
:return: 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. Returns True on success.
"""
call = KickChatMember(chat_id=chat_id, user_id=user_id, until_date=until_date,)
call = KickChatMember(chat_id=chat_id, user_id=user_id, until_date=until_date)
return await self.emit(call)
async def unban_chat_member(self, chat_id: Union[int, str], user_id: int,) -> bool:
async def unban_chat_member(self, chat_id: Union[int, str], user_id: int) -> bool:
"""
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,
@ -1111,7 +1111,7 @@ class Bot(BaseBot):
:return: The user will not return to the group or channel automatically, but will be able
to join via link, etc. Returns True on success.
"""
call = UnbanChatMember(chat_id=chat_id, user_id=user_id,)
call = UnbanChatMember(chat_id=chat_id, user_id=user_id)
return await self.emit(call)
async def restrict_chat_member(
@ -1138,7 +1138,7 @@ class Bot(BaseBot):
:return: Returns True on success.
"""
call = RestrictChatMember(
chat_id=chat_id, user_id=user_id, permissions=permissions, until_date=until_date,
chat_id=chat_id, user_id=user_id, permissions=permissions, until_date=until_date
)
return await self.emit(call)
@ -1199,7 +1199,7 @@ class Bot(BaseBot):
return await self.emit(call)
async def set_chat_permissions(
self, chat_id: Union[int, str], permissions: ChatPermissions,
self, chat_id: Union[int, str], permissions: ChatPermissions
) -> bool:
"""
Use this method to set default chat permissions for all members. The bot must be an
@ -1213,10 +1213,10 @@ class Bot(BaseBot):
:param permissions: New default chat permissions
:return: Returns True on success.
"""
call = SetChatPermissions(chat_id=chat_id, permissions=permissions,)
call = SetChatPermissions(chat_id=chat_id, permissions=permissions)
return await self.emit(call)
async def export_chat_invite_link(self, chat_id: Union[int, str],) -> str:
async def export_chat_invite_link(self, chat_id: Union[int, str]) -> str:
"""
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
@ -1233,10 +1233,10 @@ class Bot(BaseBot):
(in the format @channelusername)
:return: Returns the new invite link as String on success.
"""
call = ExportChatInviteLink(chat_id=chat_id,)
call = ExportChatInviteLink(chat_id=chat_id)
return await self.emit(call)
async def set_chat_photo(self, chat_id: Union[int, str], photo: InputFile,) -> bool:
async def set_chat_photo(self, chat_id: Union[int, str], photo: InputFile) -> bool:
"""
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
@ -1249,10 +1249,10 @@ class Bot(BaseBot):
:param photo: New chat photo, uploaded using multipart/form-data
:return: Returns True on success.
"""
call = SetChatPhoto(chat_id=chat_id, photo=photo,)
call = SetChatPhoto(chat_id=chat_id, photo=photo)
return await self.emit(call)
async def delete_chat_photo(self, chat_id: Union[int, str],) -> bool:
async def delete_chat_photo(self, chat_id: Union[int, str]) -> bool:
"""
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
@ -1264,10 +1264,10 @@ class Bot(BaseBot):
(in the format @channelusername)
:return: Returns True on success.
"""
call = DeleteChatPhoto(chat_id=chat_id,)
call = DeleteChatPhoto(chat_id=chat_id)
return await self.emit(call)
async def set_chat_title(self, chat_id: Union[int, str], title: str,) -> bool:
async def set_chat_title(self, chat_id: Union[int, str], title: str) -> bool:
"""
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
@ -1280,11 +1280,11 @@ class Bot(BaseBot):
:param title: New chat title, 1-255 characters
:return: Returns True on success.
"""
call = SetChatTitle(chat_id=chat_id, title=title,)
call = SetChatTitle(chat_id=chat_id, title=title)
return await self.emit(call)
async def set_chat_description(
self, chat_id: Union[int, str], description: Optional[str] = None,
self, chat_id: Union[int, str], description: Optional[str] = None
) -> bool:
"""
Use this method to change the description of a group, a supergroup or a channel. The bot
@ -1298,7 +1298,7 @@ class Bot(BaseBot):
:param description: New chat description, 0-255 characters
:return: Returns True on success.
"""
call = SetChatDescription(chat_id=chat_id, description=description,)
call = SetChatDescription(chat_id=chat_id, description=description)
return await self.emit(call)
async def pin_chat_message(
@ -1324,11 +1324,11 @@ class Bot(BaseBot):
:return: Returns True on success.
"""
call = PinChatMessage(
chat_id=chat_id, message_id=message_id, disable_notification=disable_notification,
chat_id=chat_id, message_id=message_id, disable_notification=disable_notification
)
return await self.emit(call)
async def unpin_chat_message(self, chat_id: Union[int, str],) -> bool:
async def unpin_chat_message(self, chat_id: Union[int, str]) -> bool:
"""
Use this method to unpin a message in a group, a supergroup, or a channel. The bot must be
an administrator in the chat for this to work and must have the can_pin_messages admin
@ -1341,10 +1341,10 @@ class Bot(BaseBot):
(in the format @channelusername)
:return: Returns True on success.
"""
call = UnpinChatMessage(chat_id=chat_id,)
call = UnpinChatMessage(chat_id=chat_id)
return await self.emit(call)
async def leave_chat(self, chat_id: Union[int, str],) -> bool:
async def leave_chat(self, chat_id: Union[int, str]) -> bool:
"""
Use this method for your bot to leave a group, supergroup or channel. Returns True on
success.
@ -1355,10 +1355,10 @@ class Bot(BaseBot):
or channel (in the format @channelusername)
:return: Returns True on success.
"""
call = LeaveChat(chat_id=chat_id,)
call = LeaveChat(chat_id=chat_id)
return await self.emit(call)
async def get_chat(self, chat_id: Union[int, str],) -> Chat:
async def get_chat(self, chat_id: Union[int, str]) -> Chat:
"""
Use this method to get up to date information about the chat (current name of the user for
one-on-one conversations, current username of a user, group or channel, etc.). Returns a
@ -1370,10 +1370,10 @@ class Bot(BaseBot):
or channel (in the format @channelusername)
:return: Returns a Chat object on success.
"""
call = GetChat(chat_id=chat_id,)
call = GetChat(chat_id=chat_id)
return await self.emit(call)
async def get_chat_administrators(self, chat_id: Union[int, str],) -> List[ChatMember]:
async def get_chat_administrators(self, chat_id: Union[int, str]) -> List[ChatMember]:
"""
Use this method to get a list of administrators in a chat. On success, returns an Array of
ChatMember objects that contains information about all chat administrators except other
@ -1389,10 +1389,10 @@ class Bot(BaseBot):
supergroup and no administrators were appointed, only the creator will be
returned.
"""
call = GetChatAdministrators(chat_id=chat_id,)
call = GetChatAdministrators(chat_id=chat_id)
return await self.emit(call)
async def get_chat_members_count(self, chat_id: Union[int, str],) -> int:
async def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
"""
Use this method to get the number of members in a chat. Returns Int on success.
@ -1402,10 +1402,10 @@ class Bot(BaseBot):
or channel (in the format @channelusername)
:return: Returns Int on success.
"""
call = GetChatMembersCount(chat_id=chat_id,)
call = GetChatMembersCount(chat_id=chat_id)
return await self.emit(call)
async def get_chat_member(self, chat_id: Union[int, str], user_id: int,) -> ChatMember:
async def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> ChatMember:
"""
Use this method to get information about a member of a chat. Returns a ChatMember object
on success.
@ -1417,10 +1417,10 @@ class Bot(BaseBot):
:param user_id: Unique identifier of the target user
:return: Returns a ChatMember object on success.
"""
call = GetChatMember(chat_id=chat_id, user_id=user_id,)
call = GetChatMember(chat_id=chat_id, user_id=user_id)
return await self.emit(call)
async def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str,) -> bool:
async def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str) -> bool:
"""
Use this method to set a new group sticker set for a supergroup. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights. Use
@ -1435,10 +1435,10 @@ class Bot(BaseBot):
:return: Use the field can_set_sticker_set optionally returned in getChat requests to
check if the bot can use this method. Returns True on success.
"""
call = SetChatStickerSet(chat_id=chat_id, sticker_set_name=sticker_set_name,)
call = SetChatStickerSet(chat_id=chat_id, sticker_set_name=sticker_set_name)
return await self.emit(call)
async def delete_chat_sticker_set(self, chat_id: Union[int, str],) -> bool:
async def delete_chat_sticker_set(self, chat_id: Union[int, str]) -> bool:
"""
Use this method to delete a group sticker set from a supergroup. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights. Use
@ -1452,7 +1452,7 @@ class Bot(BaseBot):
:return: Use the field can_set_sticker_set optionally returned in getChat requests to
check if the bot can use this method. Returns True on success.
"""
call = DeleteChatStickerSet(chat_id=chat_id,)
call = DeleteChatStickerSet(chat_id=chat_id)
return await self.emit(call)
async def answer_callback_query(
@ -1671,10 +1671,10 @@ class Bot(BaseBot):
:param reply_markup: A JSON-serialized object for a new message inline keyboard.
:return: On success, the stopped Poll with the final results is returned.
"""
call = StopPoll(chat_id=chat_id, message_id=message_id, reply_markup=reply_markup,)
call = StopPoll(chat_id=chat_id, message_id=message_id, reply_markup=reply_markup)
return await self.emit(call)
async def delete_message(self, chat_id: Union[int, str], message_id: int,) -> bool:
async def delete_message(self, chat_id: Union[int, str], message_id: int) -> bool:
"""
Use this method to delete a message, including service messages, with the following
limitations:
@ -1694,7 +1694,7 @@ class Bot(BaseBot):
:param message_id: Identifier of the message to delete
:return: Returns True on success.
"""
call = DeleteMessage(chat_id=chat_id, message_id=message_id,)
call = DeleteMessage(chat_id=chat_id, message_id=message_id)
return await self.emit(call)
# =============================================================================================
@ -1741,7 +1741,7 @@ class Bot(BaseBot):
)
return await self.emit(call)
async def get_sticker_set(self, name: str,) -> StickerSet:
async def get_sticker_set(self, name: str) -> StickerSet:
"""
Use this method to get a sticker set. On success, a StickerSet object is returned.
@ -1750,10 +1750,10 @@ class Bot(BaseBot):
:param name: Name of the sticker set
:return: On success, a StickerSet object is returned.
"""
call = GetStickerSet(name=name,)
call = GetStickerSet(name=name)
return await self.emit(call)
async def upload_sticker_file(self, user_id: int, png_sticker: InputFile,) -> File:
async def upload_sticker_file(self, user_id: int, png_sticker: InputFile) -> File:
"""
Use this method to upload a .png file with a sticker for later use in createNewStickerSet
and addStickerToSet methods (can be used multiple times). Returns the uploaded File on
@ -1767,7 +1767,7 @@ class Bot(BaseBot):
exactly 512px.
:return: Returns the uploaded File on success.
"""
call = UploadStickerFile(user_id=user_id, png_sticker=png_sticker,)
call = UploadStickerFile(user_id=user_id, png_sticker=png_sticker)
return await self.emit(call)
async def create_new_sticker_set(
@ -1850,7 +1850,7 @@ class Bot(BaseBot):
)
return await self.emit(call)
async def set_sticker_position_in_set(self, sticker: str, position: int,) -> bool:
async def set_sticker_position_in_set(self, sticker: str, position: int) -> bool:
"""
Use this method to move a sticker in a set created by the bot to a specific position .
Returns True on success.
@ -1861,10 +1861,10 @@ class Bot(BaseBot):
:param position: New sticker position in the set, zero-based
:return: Returns True on success.
"""
call = SetStickerPositionInSet(sticker=sticker, position=position,)
call = SetStickerPositionInSet(sticker=sticker, position=position)
return await self.emit(call)
async def delete_sticker_from_set(self, sticker: str,) -> bool:
async def delete_sticker_from_set(self, sticker: str) -> bool:
"""
Use this method to delete a sticker from a set created by the bot. Returns True on
success.
@ -1874,7 +1874,7 @@ class Bot(BaseBot):
:param sticker: File identifier of the sticker
:return: Returns True on success.
"""
call = DeleteStickerFromSet(sticker=sticker,)
call = DeleteStickerFromSet(sticker=sticker)
return await self.emit(call)
# =============================================================================================
@ -2066,7 +2066,7 @@ class Bot(BaseBot):
return await self.emit(call)
async def answer_pre_checkout_query(
self, pre_checkout_query_id: str, ok: bool, error_message: Optional[str] = None,
self, pre_checkout_query_id: str, ok: bool, error_message: Optional[str] = None
) -> bool:
"""
Once the user has confirmed their payment and shipping details, the Bot API sends the
@ -2088,7 +2088,7 @@ class Bot(BaseBot):
:return: On success, True is returned.
"""
call = AnswerPreCheckoutQuery(
pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message,
pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message
)
return await self.emit(call)
@ -2098,7 +2098,7 @@ class Bot(BaseBot):
# =============================================================================================
async def set_passport_data_errors(
self, user_id: int, errors: List[PassportElementError],
self, user_id: int, errors: List[PassportElementError]
) -> bool:
"""
Informs a user that some of the Telegram Passport elements they provided contains errors.
@ -2118,7 +2118,7 @@ class Bot(BaseBot):
fixed (the contents of the field for which you returned the error must change).
Returns True on success.
"""
call = SetPassportDataErrors(user_id=user_id, errors=errors,)
call = SetPassportDataErrors(user_id=user_id, errors=errors)
return await self.emit(call)
# =============================================================================================

View file

@ -28,9 +28,7 @@ class AddStickerToSet(TelegramMethod[bool]):
"""A JSON-serialized object for position where the mask should be placed on faces"""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"png_sticker",}
)
data: Dict[str, Any] = self.dict(exclude={"png_sticker"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker)

View file

@ -36,9 +36,7 @@ class CreateNewStickerSet(TelegramMethod[bool]):
"""A JSON-serialized object for position where the mask should be placed on faces"""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"png_sticker",}
)
data: Dict[str, Any] = self.dict(exclude={"png_sticker"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker)

View file

@ -58,9 +58,7 @@ class SendAnimation(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"animation", "thumb",}
)
data: Dict[str, Any] = self.dict(exclude={"animation", "thumb"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="animation", value=self.animation)

View file

@ -60,9 +60,7 @@ class SendAudio(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"audio", "thumb",}
)
data: Dict[str, Any] = self.dict(exclude={"audio", "thumb"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="audio", value=self.audio)

View file

@ -52,9 +52,7 @@ class SendDocument(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"document", "thumb",}
)
data: Dict[str, Any] = self.dict(exclude={"document", "thumb"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="document", value=self.document)

View file

@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional, Union
from ..types import InputMediaPhoto, InputMediaVideo, Message, InputFile
from ..types import InputFile, InputMediaPhoto, InputMediaVideo, Message
from .base import Request, TelegramMethod

View file

@ -43,9 +43,7 @@ class SendPhoto(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"photo",}
)
data: Dict[str, Any] = self.dict(exclude={"photo"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="photo", value=self.photo)

View file

@ -39,9 +39,7 @@ class SendSticker(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"sticker",}
)
data: Dict[str, Any] = self.dict(exclude={"sticker"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="sticker", value=self.sticker)

View file

@ -60,9 +60,7 @@ class SendVideo(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"video", "thumb",}
)
data: Dict[str, Any] = self.dict(exclude={"video", "thumb"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="video", value=self.video)

View file

@ -50,9 +50,7 @@ class SendVideoNote(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"video_note", "thumb",}
)
data: Dict[str, Any] = self.dict(exclude={"video_note", "thumb"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="video_note", value=self.video_note)

View file

@ -49,9 +49,7 @@ class SendVoice(TelegramMethod[Message]):
keyboard, instructions to remove reply keyboard or to force a reply from the user."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"voice",}
)
data: Dict[str, Any] = self.dict(exclude={"voice"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="voice", value=self.voice)

View file

@ -22,9 +22,7 @@ class SetChatPhoto(TelegramMethod[bool]):
"""New chat photo, uploaded using multipart/form-data"""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"photo",}
)
data: Dict[str, Any] = self.dict(exclude={"photo"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="photo", value=self.photo)

View file

@ -43,9 +43,7 @@ class SetWebhook(TelegramMethod[bool]):
regardless of type (default). If not specified, the previous setting will be used."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"certificate",}
)
data: Dict[str, Any] = self.dict(exclude={"certificate"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="certificate", value=self.certificate)

View file

@ -21,9 +21,7 @@ class UploadStickerFile(TelegramMethod[File]):
512px, and either width or height must be exactly 512px."""
def build_request(self) -> Request:
data: Dict[str, Any] = self.dict(
exclude={"png_sticker",}
)
data: Dict[str, Any] = self.dict(exclude={"png_sticker"})
files: Dict[str, InputFile] = {}
self.prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker)

View file

@ -1,9 +1,9 @@
from typing import Optional, TypeVar, Callable, cast
from typing import Callable, Optional, TypeVar, cast
from aiohttp import ClientSession, FormData
from .base import PRODUCTION, BaseSession, TelegramAPIServer
from ..methods import Request, TelegramMethod
from .base import PRODUCTION, BaseSession, TelegramAPIServer
T = TypeVar("T")

View file

@ -2,7 +2,7 @@ import abc
import asyncio
import datetime
import json
from typing import TypeVar, Union, Any, List, Dict, Optional, Callable
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
from pydantic.dataclasses import dataclass

View file

@ -1,5 +0,0 @@
from . import api
from .base import BaseBot
from .bot import Bot
__all__ = ["BaseBot", "Bot", "api"]

View file

@ -1,264 +0,0 @@
import logging
import os
from http import HTTPStatus
import aiohttp
from .. import types
from ..utils import exceptions
from ..utils import json
from ..utils.helper import Helper, HelperMode, Item
# Main aiogram logger
log = logging.getLogger("aiogram")
# API Url's
API_URL = "https://api.telegram.org/bot{token}/{method}"
FILE_URL = "https://api.telegram.org/file/bot{token}/{path}"
def check_token(token: str) -> bool:
"""
Validate BOT token
:param token:
:return:
"""
if any(x.isspace() for x in token):
raise exceptions.ValidationError("Token is invalid!")
left, sep, right = token.partition(":")
if (not sep) or (not left.isdigit()) or (len(left) < 3):
raise exceptions.ValidationError("Token is invalid!")
return True
def check_result(method_name: str, content_type: str, status_code: int, body: str):
"""
Checks whether `result` is a valid API response.
A result is considered invalid if:
- The server returned an HTTP response code other than 200
- The content of the result is invalid JSON.
- The method call was unsuccessful (The JSON 'ok' field equals False)
:param method_name: The name of the method called
:param status_code: status code
:param content_type: content type of result
:param body: result body
:return: The result parsed to a JSON dictionary
:raises ApiException: if one of the above listed cases is applicable
"""
log.debug('Response for %s: [%d] "%r"', method_name, status_code, body)
if content_type != "application/json":
raise exceptions.NetworkError(
f'Invalid response with content type {content_type}: "{body}"'
)
try:
result_json = json.loads(body)
except ValueError:
result_json = {}
description = result_json.get("description") or body
parameters = types.ResponseParameters(**result_json.get("parameters", {}) or {})
if HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED:
return result_json.get("result")
elif parameters.retry_after:
raise exceptions.RetryAfter(parameters.retry_after)
elif parameters.migrate_to_chat_id:
raise exceptions.MigrateToChat(parameters.migrate_to_chat_id)
elif status_code == HTTPStatus.BAD_REQUEST:
exceptions.BadRequest.detect(description)
elif status_code == HTTPStatus.NOT_FOUND:
exceptions.NotFound.detect(description)
elif status_code == HTTPStatus.CONFLICT:
exceptions.ConflictError.detect(description)
elif status_code in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]:
exceptions.Unauthorized.detect(description)
elif status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
raise exceptions.NetworkError(
"File too large for uploading. "
"Check telegram api limits https://core.telegram.org/bots/api#senddocument"
)
elif status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
if "restart" in description:
raise exceptions.RestartingTelegram()
raise exceptions.TelegramAPIError(description)
raise exceptions.TelegramAPIError(f"{description} [{status_code}]")
async def make_request(session, token, method, data=None, files=None, **kwargs):
# log.debug(f"Make request: '{method}' with data: {data} and files {files}")
log.debug('Make request: "%s" with data: "%r" and files "%r"', method, data, files)
url = Methods.api_url(token=token, method=method)
req = compose_data(data, files)
try:
async with session.post(url, data=req, **kwargs) as response:
return check_result(
method, response.content_type, response.status, await response.text()
)
except aiohttp.ClientError as e:
raise exceptions.NetworkError(
f"aiohttp client throws an error: {e.__class__.__name__}: {e}"
)
def guess_filename(obj):
"""
Get file name from object
:param obj:
:return:
"""
name = getattr(obj, "name", None)
if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">":
return os.path.basename(name)
def compose_data(params=None, files=None):
"""
Prepare request data
:param params:
:param files:
:return:
"""
data = aiohttp.formdata.FormData(quote_fields=False)
if params:
for key, value in params.items():
data.add_field(key, str(value))
if files:
for key, f in files.items():
if isinstance(f, tuple):
if len(f) == 2:
filename, fileobj = f
else:
raise ValueError("Tuple must have exactly 2 elements: filename, fileobj")
elif isinstance(f, types.InputFile):
filename, fileobj = f.filename, f.file
else:
filename, fileobj = guess_filename(f) or key, f
data.add_field(key, fileobj, filename=filename)
return data
class Methods(Helper):
"""
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
List is updated to Bot API 4.4
"""
mode = HelperMode.lowerCamelCase
# Getting Updates
GET_UPDATES = Item() # getUpdates
SET_WEBHOOK = Item() # setWebhook
DELETE_WEBHOOK = Item() # deleteWebhook
GET_WEBHOOK_INFO = Item() # getWebhookInfo
# Available methods
GET_ME = Item() # getMe
SEND_MESSAGE = Item() # sendMessage
FORWARD_MESSAGE = Item() # forwardMessage
SEND_PHOTO = Item() # sendPhoto
SEND_AUDIO = Item() # sendAudio
SEND_DOCUMENT = Item() # sendDocument
SEND_VIDEO = Item() # sendVideo
SEND_ANIMATION = Item() # sendAnimation
SEND_VOICE = Item() # sendVoice
SEND_VIDEO_NOTE = Item() # sendVideoNote
SEND_MEDIA_GROUP = Item() # sendMediaGroup
SEND_LOCATION = Item() # sendLocation
EDIT_MESSAGE_LIVE_LOCATION = Item() # editMessageLiveLocation
STOP_MESSAGE_LIVE_LOCATION = Item() # stopMessageLiveLocation
SEND_VENUE = Item() # sendVenue
SEND_CONTACT = Item() # sendContact
SEND_POLL = Item() # sendPoll
SEND_CHAT_ACTION = Item() # sendChatAction
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
GET_FILE = Item() # getFile
KICK_CHAT_MEMBER = Item() # kickChatMember
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
SET_CHAT_PHOTO = Item() # setChatPhoto
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
SET_CHAT_TITLE = Item() # setChatTitle
SET_CHAT_DESCRIPTION = Item() # setChatDescription
PIN_CHAT_MESSAGE = Item() # pinChatMessage
UNPIN_CHAT_MESSAGE = Item() # unpinChatMessage
LEAVE_CHAT = Item() # leaveChat
GET_CHAT = Item() # getChat
GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators
GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount
GET_CHAT_MEMBER = Item() # getChatMember
SET_CHAT_STICKER_SET = Item() # setChatStickerSet
DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
# Updating messages
EDIT_MESSAGE_TEXT = Item() # editMessageText
EDIT_MESSAGE_CAPTION = Item() # editMessageCaption
EDIT_MESSAGE_MEDIA = Item() # editMessageMedia
EDIT_MESSAGE_REPLY_MARKUP = Item() # editMessageReplyMarkup
STOP_POLL = Item() # stopPoll
DELETE_MESSAGE = Item() # deleteMessage
# Stickers
SEND_STICKER = Item() # sendSticker
GET_STICKER_SET = Item() # getStickerSet
UPLOAD_STICKER_FILE = Item() # uploadStickerFile
CREATE_NEW_STICKER_SET = Item() # createNewStickerSet
ADD_STICKER_TO_SET = Item() # addStickerToSet
SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet
DELETE_STICKER_FROM_SET = Item() # deleteStickerFromSet
# Inline mode
ANSWER_INLINE_QUERY = Item() # answerInlineQuery
# Payments
SEND_INVOICE = Item() # sendInvoice
ANSWER_SHIPPING_QUERY = Item() # answerShippingQuery
ANSWER_PRE_CHECKOUT_QUERY = Item() # answerPreCheckoutQuery
# Telegram Passport
SET_PASSPORT_DATA_ERRORS = Item() # setPassportDataErrors
# Games
SEND_GAME = Item() # sendGame
SET_GAME_SCORE = Item() # setGameScore
GET_GAME_HIGH_SCORES = Item() # getGameHighScores
@staticmethod
def api_url(token, method):
"""
Generate API URL with included token and method name
:param token:
:param method:
:return:
"""
return API_URL.format(token=token, method=method)
@staticmethod
def file_url(token, path):
"""
Generate File URL with included token and file path
:param token:
:param path:
:return:
"""
return FILE_URL.format(token=token, path=path)

View file

@ -1,314 +0,0 @@
import asyncio
import contextlib
import io
import ssl
import typing
from contextvars import ContextVar
from typing import Dict, List, Optional, Union
import aiohttp
import certifi
from aiohttp.helpers import sentinel
from . import api
from ..types import ParseMode, base
from ..utils import json
from ..utils.auth_widget import check_integrity
class BaseBot:
"""
Base class for bot. It's raw bot.
"""
_ctx_timeout = ContextVar("TelegramRequestTimeout")
_ctx_token = ContextVar("BotDifferentToken")
def __init__(
self,
token: base.String,
loop: Optional[asyncio.AbstractEventLoop] = None,
connections_limit: Optional[base.Integer] = None,
proxy: Optional[base.String] = None,
proxy_auth: Optional[aiohttp.BasicAuth] = None,
validate_token: Optional[base.Boolean] = True,
parse_mode: typing.Optional[base.String] = None,
timeout: typing.Optional[
typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]
] = None,
):
"""
Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot
:param token: token from @BotFather
:type token: :obj:`str`
:param loop: event loop
:type loop: Optional Union :obj:`asyncio.BaseEventLoop`, :obj:`asyncio.AbstractEventLoop`
:param connections_limit: connections limit for aiohttp.ClientSession
:type connections_limit: :obj:`int`
:param proxy: HTTP proxy URL
:type proxy: :obj:`str`
:param proxy_auth: Authentication information
: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`
:param timeout: Request timeout
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
:raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError`
"""
# Authentication
if validate_token:
api.check_token(token)
self._token = None
self.__token = token
self.proxy = proxy
self.proxy_auth = proxy_auth
# Asyncio loop instance
if loop is None:
loop = asyncio.get_event_loop()
self.loop = loop
# aiohttp main session
ssl_context = ssl.create_default_context(cafile=certifi.where())
if isinstance(proxy, str) and (
proxy.startswith("socks5://") or proxy.startswith("socks4://")
):
from aiohttp_socks import SocksConnector
from aiohttp_socks.helpers import parse_socks_url
socks_ver, host, port, username, password = parse_socks_url(proxy)
if proxy_auth:
if not username:
username = proxy_auth.login
if not password:
password = proxy_auth.password
connector = SocksConnector(
socks_ver=socks_ver,
host=host,
port=port,
username=username,
password=password,
limit=connections_limit,
ssl_context=ssl_context,
rdns=True,
loop=self.loop,
)
self.proxy = None
self.proxy_auth = None
else:
connector = aiohttp.TCPConnector(
limit=connections_limit, ssl=ssl_context, loop=self.loop
)
self._timeout = None
self.timeout = timeout
self.session = aiohttp.ClientSession(
connector=connector, loop=self.loop, json_serialize=json.dumps
)
self.parse_mode = parse_mode
def __del__(self):
if not hasattr(self, 'loop'):
return
if self.loop.is_running():
self.loop.create_task(self.close())
return
loop = asyncio.new_event_loop()
loop.run_until_complete(self.close())
@staticmethod
def _prepare_timeout(
value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]
) -> typing.Optional[aiohttp.ClientTimeout]:
if value is None or isinstance(value, aiohttp.ClientTimeout):
return value
return aiohttp.ClientTimeout(total=value)
@property
def timeout(self):
timeout = self._ctx_timeout.get(self._timeout)
if timeout is None:
return sentinel
return timeout
@timeout.setter
def timeout(self, value):
self._timeout = self._prepare_timeout(value)
@timeout.deleter
def timeout(self):
self.timeout = None
@contextlib.contextmanager
def request_timeout(
self, timeout: typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]
):
"""
Context manager implements opportunity to change request timeout in current context
:param timeout: Request timeout
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
:return:
"""
timeout = self._prepare_timeout(timeout)
token = self._ctx_timeout.set(timeout)
try:
yield
finally:
self._ctx_timeout.reset(token)
@property
def __token(self):
return self._ctx_token.get(self._token)
@__token.setter
def __token(self, value):
self._token = value
@contextlib.contextmanager
def with_token(self, bot_token: base.String, validate_token: Optional[base.Boolean] = True):
if validate_token:
api.check_token(bot_token)
token = self._ctx_token.set(bot_token)
try:
yield
finally:
self._ctx_token.reset(token)
async def close(self):
"""
Close all client sessions
"""
await self.session.close()
async def request(
self,
method: base.String,
data: Optional[Dict] = None,
files: Optional[Dict] = None,
**kwargs,
) -> Union[List, Dict, base.Boolean]:
"""
Make an request to Telegram Bot API
https://core.telegram.org/bots/api#making-requests
:param method: API method
:type method: :obj:`str`
:param data: request parameters
:type data: :obj:`dict`
:param files: files
:type files: :obj:`dict`
:return: result
:rtype: Union[List, Dict]
:raise: :obj:`aiogram.exceptions.TelegramApiError`
"""
return await api.make_request(
self.session,
self.__token,
method,
data,
files,
proxy=self.proxy,
proxy_auth=self.proxy_auth,
timeout=self.timeout,
**kwargs,
)
async def download_file(
self,
file_path: base.String,
destination: Optional[base.InputFile] = None,
timeout: Optional[base.Integer] = sentinel,
chunk_size: Optional[base.Integer] = 65536,
seek: Optional[base.Boolean] = True,
) -> Union[io.BytesIO, io.FileIO]:
"""
Download file by file_path to destination
if You want to automatically create destination (:class:`io.BytesIO`) use default
value of destination and handle result of this method.
:param file_path: file path on telegram server (You can get it from :obj:`aiogram.types.File`)
:type file_path: :obj:`str`
:param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO`
:param timeout: Integer
:param chunk_size: Integer
:param seek: Boolean - go to start of file when downloading is finished.
:return: destination
"""
if destination is None:
destination = io.BytesIO()
url = self.get_file_url(file_path)
dest = destination if isinstance(destination, io.IOBase) else open(destination, "wb")
async with self.session.get(
url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth
) as response:
while True:
chunk = await response.content.read(chunk_size)
if not chunk:
break
dest.write(chunk)
dest.flush()
if seek:
dest.seek(0)
return dest
def get_file_url(self, file_path):
return api.Methods.file_url(token=self.__token, path=file_path)
async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]:
"""
Send file
https://core.telegram.org/bots/api#inputfile
:param file_type: field name
:param method: API method
:param file: String or io.IOBase
:param payload: request payload
:return: response
"""
if file is None:
files = {}
elif isinstance(file, str):
# You can use file ID or URL in the most of requests
payload[file_type] = file
files = None
else:
files = {file_type: file}
return await self.request(method, payload, files)
@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 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
def check_auth_widget(self, data):
return check_integrity(self.__token, data)

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
import json
import pathlib
import pickle
import typing
from .memory import MemoryStorage
class _FileStorage(MemoryStorage):
def __init__(self, path: typing.Union[pathlib.Path, str]):
"""
:param path: file path
"""
super(_FileStorage, self).__init__()
path = self.path = pathlib.Path(path)
try:
self.data = self.read(path)
except FileNotFoundError:
pass
async def close(self):
if self.data:
self.write(self.path)
await super(_FileStorage, self).close()
def read(self, path: pathlib.Path):
raise NotImplementedError
def write(self, path: pathlib.Path):
raise NotImplementedError
class JSONStorage(_FileStorage):
"""
JSON File storage based on MemoryStorage
"""
def read(self, path: pathlib.Path):
with path.open("r") as f:
return json.load(f)
def write(self, path: pathlib.Path):
with path.open("w") as f:
return json.dump(self.data, f, indent=4)
class PickleStorage(_FileStorage):
"""
Pickle File storage based on MemoryStorage
"""
def read(self, path: pathlib.Path):
with path.open("rb") as f:
return pickle.load(f)
def write(self, path: pathlib.Path):
with path.open("wb") as f:
return pickle.dump(self.data, f, protocol=pickle.HIGHEST_PROTOCOL)

View file

@ -1,131 +0,0 @@
import copy
import typing
from ...dispatcher.storage import BaseStorage
class MemoryStorage(BaseStorage):
"""
In-memory based states storage.
This type of storage is not recommended for usage in bots, because you will lost all states after restarting.
"""
async def wait_closed(self):
pass
async def close(self):
self.data.clear()
def __init__(self):
self.data = {}
def resolve_address(self, chat, user):
chat_id, user_id = map(str, self.check_address(chat=chat, user=user))
if chat_id not in self.data:
self.data[chat_id] = {}
if user_id not in self.data[chat_id]:
self.data[chat_id][user_id] = {"state": None, "data": {}, "bucket": {}}
return chat_id, user_id
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
chat, user = self.resolve_address(chat=chat, user=user)
return self.data[chat][user]["state"]
async def get_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Dict:
chat, user = self.resolve_address(chat=chat, user=user)
return copy.deepcopy(self.data[chat][user]["data"])
async def update_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
**kwargs,
):
if data is None:
data = {}
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]["data"].update(data, **kwargs)
async def set_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
state: typing.AnyStr = None,
):
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]["state"] = state
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 = self.resolve_address(chat=chat, user=user)
self.data[chat][user]["data"] = copy.deepcopy(data)
async def reset_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
with_data: typing.Optional[bool] = True,
):
await self.set_state(chat=chat, user=user, state=None)
if with_data:
await self.set_data(chat=chat, user=user, data={})
def has_bucket(self):
return True
async def get_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None,
) -> typing.Dict:
chat, user = self.resolve_address(chat=chat, user=user)
return copy.deepcopy(self.data[chat][user]["bucket"])
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 = self.resolve_address(chat=chat, user=user)
self.data[chat][user]["bucket"] = copy.deepcopy(bucket)
async def update_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
**kwargs,
):
if bucket is None:
bucket = {}
chat, user = self.resolve_address(chat=chat, user=user)
self.data[chat][user]["bucket"].update(bucket, **kwargs)

View file

@ -1,200 +0,0 @@
"""
This module has mongo storage for finite-state machine
based on `aiomongo <https://github.com/ZeoAlliance/aiomongo`_ driver
"""
from typing import Union, Dict, Optional, List, Tuple, AnyStr
import aiomongo
from aiomongo import AioMongoClient, Database
from ...dispatcher.storage import BaseStorage
STATE = 'aiogram_state'
DATA = 'aiogram_data'
BUCKET = 'aiogram_bucket'
COLLECTIONS = (STATE, DATA, BUCKET)
class MongoStorage(BaseStorage):
"""
Mongo-based storage for FSM.
Usage:
.. code-block:: python3
storage = MongoStorage(host='localhost', port=27017, db_name='aiogram_fsm')
dp = Dispatcher(bot, storage=storage)
And need to close Mongo client connections when shutdown
.. code-block:: python3
await dp.storage.close()
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm',
username=None, password=None, index=True, **kwargs):
self._host = host
self._port = port
self._db_name: str = db_name
self._username = username
self._password = password
self._kwargs = kwargs
self._mongo: Union[AioMongoClient, None] = None
self._db: Union[Database, None] = None
self._index = index
async def get_client(self) -> AioMongoClient:
if isinstance(self._mongo, AioMongoClient):
return self._mongo
uri = 'mongodb://'
# set username + password
if self._username and self._password:
uri += f'{self._username}:{self._password}@'
# set host and port (optional)
uri += f'{self._host}:{self._port}' if self._host else f'localhost:{self._port}'
# define and return client
self._mongo = await aiomongo.create_client(uri)
return self._mongo
async def get_db(self) -> Database:
"""
Get Mongo db
This property is awaitable.
"""
if isinstance(self._db, Database):
return self._db
mongo = await self.get_client()
self._db = mongo.get_database(self._db_name)
if self._index:
await self.apply_index(self._db)
return self._db
@staticmethod
async def apply_index(db):
for collection in COLLECTIONS:
await db[collection].create_index(keys=[('chat', 1), ('user', 1)],
name="chat_user_idx", unique=True, background=True)
async def close(self):
if self._mongo:
self._mongo.close()
async def wait_closed(self):
if self._mongo:
return await self._mongo.wait_closed()
return True
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
state: Optional[AnyStr] = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
if state is None:
await db[STATE].delete_one(filter={'chat': chat, 'user': user})
else:
await db[STATE].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'state': state}}, upsert=True)
async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[str] = None) -> Optional[str]:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
return result.get('state') if result else default
async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
data: Dict = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
await db[DATA].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'data': data}}, upsert=True)
async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[dict] = None) -> Dict:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[DATA].find_one(filter={'chat': chat, 'user': user})
return result.get('data') if result else default or {}
async def update_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
data: Dict = None, **kwargs):
if data is None:
data = {}
temp_data = await self.get_data(chat=chat, user=user, default={})
temp_data.update(data, **kwargs)
await self.set_data(chat=chat, user=user, data=temp_data)
def has_bucket(self):
return True
async def get_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
default: Optional[dict] = None) -> Dict:
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
result = await db[BUCKET].find_one(filter={'chat': chat, 'user': user})
return result.get('bucket') if result else default or {}
async def set_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
bucket: Dict = None):
chat, user = self.check_address(chat=chat, user=user)
db = await self.get_db()
await db[BUCKET].update_one(filter={'chat': chat, 'user': user},
update={'$set': {'bucket': bucket}}, upsert=True)
async def update_bucket(self, *, chat: Union[str, int, None] = None,
user: Union[str, int, None] = None,
bucket: Dict = None, **kwargs):
if bucket is None:
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
Reset states in DB
:param full: clean DB or clean only states
:return:
"""
db = await self.get_db()
await db[STATE].drop()
if full:
await db[DATA].drop()
await db[BUCKET].drop()
async def get_states_list(self) -> List[Tuple[int, int]]:
"""
Get list of all stored chat's and user's
:return: list of tuples where first element is chat id and second is user id
"""
db = await self.get_db()
result = []
items = await db[STATE].find().to_list()
for item in items:
result.append(
(int(item['chat']), int(item['user']))
)
return result

View file

@ -1,405 +0,0 @@
"""
This module has redis storage for finite-state machine based on `aioredis <https://github.com/aio-libs/aioredis>`_ driver
"""
import asyncio
import logging
import typing
import aioredis
from ...dispatcher.storage import BaseStorage
from ...utils import json
STATE_KEY = 'state'
STATE_DATA_KEY = 'data'
STATE_BUCKET_KEY = 'bucket'
class RedisStorage(BaseStorage):
"""
Simple Redis-base storage for FSM.
Usage:
.. code-block:: python3
storage = RedisStorage('localhost', 6379, db=5)
dp = Dispatcher(bot, storage=storage)
And need to close Redis connection when shutdown
.. code-block:: python3
await dp.storage.close()
await dp.storage.wait_closed()
"""
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
self._host = host
self._port = port
self._db = db
self._password = password
self._ssl = ssl
self._loop = loop or asyncio.get_event_loop()
self._kwargs = kwargs
self._redis: aioredis.RedisConnection = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def close(self):
if self._redis and not self._redis.closed:
self._redis.close()
del self._redis
self._redis = None
async def wait_closed(self):
if self._redis:
return await self._redis.wait_closed()
return True
async def redis(self) -> aioredis.RedisConnection:
"""
Get Redis connection
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
if self._redis is None:
self._redis = await aioredis.create_connection((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
loop=self._loop,
**self._kwargs)
return self._redis
async def get_record(self, *,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None) -> typing.Dict:
"""
Get record from storage
:param chat:
:param user:
:return:
"""
chat, user = self.check_address(chat=chat, user=user)
addr = f"fsm:{chat}:{user}"
conn = await self.redis()
data = await conn.execute('GET', addr)
if data is None:
return {'state': None, 'data': {}}
return json.loads(data)
async def set_record(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
state=None, data=None, bucket=None):
"""
Write record to storage
:param bucket:
:param chat:
:param user:
:param state:
:param data:
:return:
"""
if data is None:
data = {}
if bucket is None:
bucket = {}
chat, user = self.check_address(chat=chat, user=user)
addr = f"fsm:{chat}:{user}"
record = {'state': state, 'data': data, 'bucket': bucket}
conn = await self.redis()
await conn.execute('SET', addr, json.dumps(record))
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Optional[str]:
record = await self.get_record(chat=chat, user=user)
return record['state']
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:
record = await self.get_record(chat=chat, user=user)
return record['data']
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None):
record = await self.get_record(chat=chat, user=user)
await self.set_record(chat=chat, user=user, state=state, data=record['data'])
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None):
record = await self.get_record(chat=chat, user=user)
await self.set_record(chat=chat, user=user, state=record['state'], data=data)
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
if data is None:
data = {}
record = await self.get_record(chat=chat, user=user)
record_data = record.get('data', {})
record_data.update(data, **kwargs)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_data)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
"""
Get list of all stored chat's and user's
:return: list of tuples where first element is chat id and second is user id
"""
conn = await self.redis()
result = []
keys = await conn.execute('KEYS', 'fsm:*')
for item in keys:
*_, chat, user = item.decode('utf-8').split(':')
result.append((chat, user))
return result
async def reset_all(self, full=True):
"""
Reset states in DB
:param full: clean DB or clean only states
:return:
"""
conn = await self.redis()
if full:
await conn.execute('FLUSHDB')
else:
keys = await conn.execute('KEYS', 'fsm:*')
await conn.execute('DEL', *keys)
def has_bucket(self):
return True
async def get_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Dict:
record = await self.get_record(chat=chat, user=user)
return record.get('bucket', {})
async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None):
record = await self.get_record(chat=chat, user=user)
await self.set_record(chat=chat, user=user, state=record['state'], data=record['data'], bucket=bucket)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None, **kwargs):
record = await self.get_record(chat=chat, user=user)
record_bucket = record.get('bucket', {})
if bucket is None:
bucket = {}
record_bucket.update(bucket, **kwargs)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_bucket, bucket=bucket)
class RedisStorage2(BaseStorage):
"""
Busted Redis-base storage for FSM.
Works with Redis connection pool and customizable keys prefix.
Usage:
.. code-block:: python3
storage = RedisStorage('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key')
dp = Dispatcher(bot, storage=storage)
And need to close Redis connection when shutdown
.. code-block:: python3
await dp.storage.close()
await dp.storage.wait_closed()
"""
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
ssl=None, pool_size=10, loop=None, prefix='fsm',
state_ttl: int = 0,
data_ttl: int = 0,
bucket_ttl: int = 0,
**kwargs):
self._host = host
self._port = port
self._db = db
self._password = password
self._ssl = ssl
self._pool_size = pool_size
self._loop = loop or asyncio.get_event_loop()
self._kwargs = kwargs
self._prefix = (prefix,)
self._state_ttl = state_ttl
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
self._redis: aioredis.RedisConnection = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def redis(self) -> aioredis.Redis:
"""
Get Redis connection
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
if self._redis is None:
self._redis = await aioredis.create_redis_pool((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
minsize=1, maxsize=self._pool_size,
loop=self._loop, **self._kwargs)
return self._redis
def generate_key(self, *parts):
return ':'.join(self._prefix + tuple(map(str, parts)))
async def close(self):
async with self._connection_lock:
if self._redis and not self._redis.closed:
self._redis.close()
del self._redis
self._redis = None
async def wait_closed(self):
async with self._connection_lock:
if self._redis:
return await self._redis.wait_closed()
return True
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Optional[str]:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_KEY)
redis = await self.redis()
return await redis.get(key, encoding='utf8') or None
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None) -> typing.Dict:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
raw_result = await redis.get(key, encoding='utf8')
if raw_result:
return json.loads(raw_result)
return default or {}
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None):
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_KEY)
redis = await self.redis()
if state is None:
await redis.delete(key)
else:
await redis.set(key, state, expire=self._state_ttl)
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 = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_DATA_KEY)
redis = await self.redis()
await redis.set(key, json.dumps(data), expire=self._data_ttl)
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
if data is None:
data = {}
temp_data = await self.get_data(chat=chat, user=user, default={})
temp_data.update(data, **kwargs)
await self.set_data(chat=chat, user=user, data=temp_data)
def has_bucket(self):
return True
async def get_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None) -> typing.Dict:
chat, user = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
redis = await self.redis()
raw_result = await redis.get(key, encoding='utf8')
if raw_result:
return json.loads(raw_result)
return default or {}
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 = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
redis = await self.redis()
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None, **kwargs):
if bucket is None:
bucket = {}
temp_bucket = await self.get_bucket(chat=chat, user=user)
temp_bucket.update(bucket, **kwargs)
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
async def reset_all(self, full=True):
"""
Reset states in DB
:param full: clean DB or clean only states
:return:
"""
conn = await self.redis()
if full:
await conn.flushdb()
else:
keys = await conn.keys(self.generate_key('*'))
await conn.delete(*keys)
async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
"""
Get list of all stored chat's and user's
:return: list of tuples where first element is chat id and second is user id
"""
conn = await self.redis()
result = []
keys = await conn.keys(self.generate_key('*', '*', STATE_KEY), encoding='utf8')
for item in keys:
*_, chat, user, _ = item.split(':')
result.append((chat, user))
return result
async def import_redis1(self, redis1):
await migrate_redis1_to_redis2(redis1, self)
async def migrate_redis1_to_redis2(storage1: RedisStorage, storage2: RedisStorage2):
"""
Helper for migrating from RedisStorage to RedisStorage2
:param storage1: instance of RedisStorage
:param storage2: instance of RedisStorage2
:return:
"""
if not isinstance(storage1, RedisStorage): # better than assertion
raise TypeError(f"{type(storage1)} is not RedisStorage instance.")
if not isinstance(storage2, RedisStorage):
raise TypeError(f"{type(storage2)} is not RedisStorage instance.")
log = logging.getLogger('aiogram.RedisStorage')
for chat, user in await storage1.get_states_list():
state = await storage1.get_state(chat=chat, user=user)
await storage2.set_state(chat=chat, user=user, state=state)
data = await storage1.get_data(chat=chat, user=user)
await storage2.set_data(chat=chat, user=user, data=data)
bucket = await storage1.get_bucket(chat=chat, user=user)
await storage2.set_bucket(chat=chat, user=user, bucket=bucket)
log.info(f"Migrated user {user} in chat {chat}")

View file

@ -1,242 +0,0 @@
import asyncio
import contextlib
import typing
import rethinkdb
from rethinkdb.asyncio_net.net_asyncio import Connection
from ...dispatcher.storage import BaseStorage
__all__ = ["RethinkDBStorage"]
r = rethinkdb.RethinkDB()
r.set_loop_type("asyncio")
class RethinkDBStorage(BaseStorage):
"""
RethinkDB-based storage for FSM.
Usage:
..code-block:: python3
storage = RethinkDBStorage(db='aiogram', table='aiogram', user='aiogram', password='aiogram_secret')
dispatcher = Dispatcher(bot, storage=storage)
And need to close connection when shutdown
..code-clock:: python3
await storage.close()
"""
def __init__(
self,
host: str = "localhost",
port: int = 28015,
db: str = "aiogram",
table: str = "aiogram",
auth_key: typing.Optional[str] = None,
user: typing.Optional[str] = None,
password: typing.Optional[str] = None,
timeout: int = 20,
ssl: typing.Optional[dict] = None,
loop: typing.Optional[asyncio.AbstractEventLoop] = None,
):
self._host = host
self._port = port
self._db = db
self._table = table
self._auth_key = auth_key
self._user = user
self._password = password
self._timeout = timeout
self._ssl = ssl or {}
self._loop = loop
self._conn: typing.Union[Connection, None] = None
async def connect(self) -> Connection:
"""
Get or create a connection.
"""
if self._conn is None:
self._conn = await r.connect(
host=self._host,
port=self._port,
db=self._db,
auth_key=self._auth_key,
user=self._user,
password=self._password,
timeout=self._timeout,
ssl=self._ssl,
io_loop=self._loop,
)
return self._conn
@contextlib.asynccontextmanager
async def connection(self):
conn = await self.connect()
yield conn
async def close(self):
"""
Close a connection.
"""
self._conn.close()
self._conn = None
async def wait_closed(self):
"""
Does nothing
"""
pass
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
return (
await r.table(self._table)
.get(chat)[user]["state"]
.default(default or None)
.run(conn)
)
async def get_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Dict:
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
return (
await r.table(self._table).get(chat)[user]["data"].default(default or {}).run(conn)
)
async def set_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None,
):
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
await r.table(self._table).insert(
{"id": chat, user: {"state": state}}, conflict="update"
).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,
):
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
if await r.table(self._table).get(chat).run(conn):
await r.table(self._table).get(chat).update({user: {"data": r.literal(data)}}).run(
conn
)
else:
await r.table(self._table).insert({"id": chat, user: {"data": data}}).run(conn)
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))
async with self.connection() as conn:
await r.table(self._table).insert(
{"id": chat, user: {"data": data}}, conflict="update"
).run(conn)
def has_bucket(self):
return True
async def get_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None,
) -> typing.Dict:
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
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,
):
chat, user = map(str, self.check_address(chat=chat, user=user))
async with self.connection() as conn:
if await r.table(self._table).get(chat).run(conn):
await r.table(self._table).get(chat).update(
{user: {"bucket": r.literal(bucket)}}
).run(conn)
else:
await r.table(self._table).insert({"id": chat, user: {"bucket": bucket}}).run(conn)
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))
async with self.connection() as conn:
await r.table(self._table).insert(
{"id": chat, user: {"bucket": bucket}}, conflict="update"
).run(conn)
async def get_states_list(self) -> typing.List[typing.Tuple[int, int]]:
"""
Get list of all stored chat's and user's
:return: list of tuples where first element is chat id and second is user id
"""
async with self.connection() as conn:
result = []
items = (await r.table(self._table).run(conn)).items
for item in items:
chat = int(item.pop("id"))
for key in item.keys():
user = int(key)
result.append((chat, user))
return result
async def reset_all(self):
"""
Reset states in DB
"""
async with self.connection() as conn:
await r.table(self._table).delete().run(conn)

View file

@ -1,21 +0,0 @@
from aiogram.dispatcher.middlewares import BaseMiddleware
class EnvironmentMiddleware(BaseMiddleware):
def __init__(self, context=None):
super(EnvironmentMiddleware, self).__init__()
if context is None:
context = {}
self.context = context
def update_data(self, data):
dp = self.manager.dispatcher
data.update(bot=dp.bot, dispatcher=dp, loop=dp.loop)
if self.context:
data.update(self.context)
async def trigger(self, action, args):
if "error" not in action and action.startswith("pre_process_"):
self.update_data(args[-1])
return True

View file

@ -1,80 +0,0 @@
import copy
import weakref
from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware
from aiogram.dispatcher.storage import FSMContext
class FSMMiddleware(LifetimeControllerMiddleware):
skip_patterns = ["error", "update"]
def __init__(self):
super(FSMMiddleware, self).__init__()
self._proxies = weakref.WeakKeyDictionary()
async def pre_process(self, obj, data, *args):
proxy = await FSMSStorageProxy.create(self.manager.dispatcher.current_state())
data["state_data"] = proxy
async def post_process(self, obj, data, *args):
proxy = data.get("state_data", None)
if isinstance(proxy, FSMSStorageProxy):
await proxy.save()
class FSMSStorageProxy(dict):
def __init__(self, fsm_context: FSMContext):
super(FSMSStorageProxy, self).__init__()
self.fsm_context = fsm_context
self._copy = {}
self._data = {}
self._state = None
self._is_dirty = False
@classmethod
async def create(cls, fsm_context: FSMContext):
"""
:param fsm_context:
:return:
"""
proxy = cls(fsm_context)
await proxy.load()
return proxy
async def load(self):
self.clear()
self._state = await self.fsm_context.get_state()
self.update(await self.fsm_context.get_data())
self._copy = copy.deepcopy(self)
self._is_dirty = False
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self._is_dirty = True
@state.deleter
def state(self):
self._state = None
self._is_dirty = True
async def save(self, force=False):
if self._copy != self or force:
await self.fsm_context.set_data(data=self)
if self._is_dirty or force:
await self.fsm_context.set_state(self.state)
self._is_dirty = False
self._copy = copy.deepcopy(self)
def __str__(self):
s = super(FSMSStorageProxy, self).__str__()
readable_state = f"'{self.state}'" if self.state else "''"
return f"<{self.__class__.__name__}(state={readable_state}, data={s})>"
def clear(self):
del self.state
return super(FSMSStorageProxy, self).clear()

View file

@ -1,150 +0,0 @@
import gettext
import os
from contextvars import ContextVar
from typing import Any, Dict, Tuple
from babel import Locale
from babel.support import LazyProxy
from ... import types
from ...dispatcher.middlewares import BaseMiddleware
class I18nMiddleware(BaseMiddleware):
"""
I18n middleware based on gettext util
>>> dp = Dispatcher(bot)
>>> i18n = I18nMiddleware(DOMAIN, LOCALES_DIR)
>>> dp.middleware.setup(i18n)
and then
>>> _ = i18n.gettext
or
>>> _ = i18n = I18nMiddleware(DOMAIN_NAME, LOCALES_DIR)
"""
ctx_locale = ContextVar("ctx_user_locale", default=None)
def __init__(self, domain, path=None, default="en"):
"""
:param domain: domain
:param path: path where located all *.mo files
:param default: default locale name
"""
super(I18nMiddleware, self).__init__()
if path is None:
path = os.path.join(os.getcwd(), "locales")
self.domain = domain
self.path = path
self.default = default
self.locales = self.find_locales()
def find_locales(self) -> Dict[str, gettext.GNUTranslations]:
"""
Load all compiled locales from path
:return: dict with locales
"""
translations = {}
for name in os.listdir(self.path):
if not os.path.isdir(os.path.join(self.path, name)):
continue
mo_path = os.path.join(self.path, name, "LC_MESSAGES", self.domain + ".mo")
if os.path.exists(mo_path):
with open(mo_path, "rb") as fp:
translations[name] = gettext.GNUTranslations(fp)
elif os.path.exists(mo_path[:-2] + "po"):
raise RuntimeError(f"Found locale '{name} but this language is not compiled!")
return translations
def reload(self):
"""
Hot reload locles
"""
self.locales = self.find_locales()
@property
def available_locales(self) -> Tuple[str]:
"""
list of loaded locales
:return:
"""
return tuple(self.locales.keys())
def __call__(self, singular, plural=None, n=1, locale=None) -> str:
return self.gettext(singular, plural, n, locale)
def gettext(self, singular, plural=None, n=1, locale=None) -> str:
"""
Get text
:param singular:
:param plural:
:param n:
:param locale:
:return:
"""
if locale is None:
locale = self.ctx_locale.get()
if locale not in self.locales:
if n is 1:
return singular
return plural
translator = self.locales[locale]
if plural is None:
return translator.gettext(singular)
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=False) -> LazyProxy:
"""
Lazy get text
:param singular:
:param plural:
:param n:
:param locale:
:param enable_cache:
:return:
"""
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
"""
User locale getter
You can override the method if you want to use different way of getting user language.
:param action: event name
:param args: event arguments
:return: locale name
"""
user: types.User = types.User.get_current()
locale: Locale = user.locale
if locale:
*_, data = args
language = data["locale"] = locale.language
return language
async def trigger(self, action, args):
"""
Event trigger
:param action: event name
:param args: event arguments
:return:
"""
if "update" not in action and "error" not in action and action.startswith("pre_process"):
locale = await self.get_user_locale(action, args)
self.ctx_locale.set(locale)
return True

View file

@ -1,427 +0,0 @@
import time
import logging
from aiogram import types
from aiogram.dispatcher.middlewares import BaseMiddleware
HANDLED_STR = ['Unhandled', 'Handled']
class LoggingMiddleware(BaseMiddleware):
def __init__(self, logger=__name__):
if not isinstance(logger, logging.Logger):
logger = logging.getLogger(logger)
self.logger = logger
super(LoggingMiddleware, self).__init__()
def check_timeout(self, obj):
start = obj.conf.get('_start', None)
if start:
del obj.conf['_start']
return round((time.time() - start) * 1000)
return -1
async def on_pre_process_update(self, update: types.Update, data: dict):
update.conf['_start'] = time.time()
self.logger.debug(f"Received update [ID:{update.update_id}]")
async def on_post_process_update(self, update: types.Update, result, data: dict):
timeout = self.check_timeout(update)
if timeout > 0:
self.logger.info(f"Process update [ID:{update.update_id}]: [success] (in {timeout} ms)")
async def on_pre_process_message(self, message: types.Message, data: dict):
self.logger.info(f"Received message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]")
async def on_post_process_message(self, message: types.Message, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"message [ID:{message.message_id}] in chat [{message.chat.type}:{message.chat.id}]")
async def on_pre_process_edited_message(self, edited_message, data: dict):
self.logger.info(f"Received edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]")
async def on_post_process_edited_message(self, edited_message, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"edited message [ID:{edited_message.message_id}] "
f"in chat [{edited_message.chat.type}:{edited_message.chat.id}]")
async def on_pre_process_channel_post(self, channel_post: types.Message, data: dict):
self.logger.info(f"Received channel post [ID:{channel_post.message_id}] "
f"in channel [ID:{channel_post.chat.id}]")
async def on_post_process_channel_post(self, channel_post: types.Message, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"channel post [ID:{channel_post.message_id}] "
f"in chat [{channel_post.chat.type}:{channel_post.chat.id}]")
async def on_pre_process_edited_channel_post(self, edited_channel_post: types.Message, data: dict):
self.logger.info(f"Received edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]")
async def on_post_process_edited_channel_post(self, edited_channel_post: types.Message, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"edited channel post [ID:{edited_channel_post.message_id}] "
f"in channel [ID:{edited_channel_post.chat.id}]")
async def on_pre_process_inline_query(self, inline_query: types.InlineQuery, data: dict):
self.logger.info(f"Received inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]")
async def on_post_process_inline_query(self, inline_query: types.InlineQuery, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"inline query [ID:{inline_query.id}] "
f"from user [ID:{inline_query.from_user.id}]")
async def on_pre_process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult, data: dict):
self.logger.info(f"Received chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]")
async def on_post_process_chosen_inline_result(self, chosen_inline_result, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"chosen inline result [Inline msg ID:{chosen_inline_result.inline_message_id}] "
f"from user [ID:{chosen_inline_result.from_user.id}] "
f"result [ID:{chosen_inline_result.result_id}]")
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
if callback_query.message:
text = (f"Received callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for inline message [ID:{callback_query.inline_message_id}] ")
async def on_post_process_callback_query(self, callback_query, results, data: dict):
if callback_query.message:
text = (f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}] "
f"for message [ID:{callback_query.message.message_id}] "
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
if callback_query.message.from_user:
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
self.logger.info(text)
else:
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"callback query [ID:{callback_query.id}] "
f"from user [ID:{callback_query.from_user.id}]"
f"from inline message [ID:{callback_query.inline_message_id}]")
async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict):
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
f"from user [ID:{shipping_query.from_user.id}]")
async def on_post_process_shipping_query(self, shipping_query, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"shipping query [ID:{shipping_query.id}] "
f"from user [ID:{shipping_query.from_user.id}]")
async def on_pre_process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery, data: dict):
self.logger.info(f"Received pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]")
async def on_post_process_pre_checkout_query(self, pre_checkout_query, results, data: dict):
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
f"pre-checkout query [ID:{pre_checkout_query.id}] "
f"from user [ID:{pre_checkout_query.from_user.id}]")
async def on_pre_process_error(self, update, error, data: dict):
timeout = self.check_timeout(update)
if timeout > 0:
self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)")
class LoggingFilter(logging.Filter):
"""
Extend LogRecord by data from Telegram Update object.
Can be used in logging config:
.. code-block: python3
'filters': {
'telegram': {
'()': LoggingFilter,
'include_content': True,
}
},
...
'handlers': {
'graypy': {
'()': GELFRabbitHandler,
'url': 'amqp://localhost:5672/',
'routing_key': '#',
'localname': 'testapp',
'filters': ['telegram']
},
},
"""
def __init__(self, name='', prefix='tg', include_content=False):
"""
:param name:
:param prefix: prefix for all records
:param include_content: pass into record all data from Update object
"""
super(LoggingFilter, self).__init__(name=name)
self.prefix = prefix
self.include_content = include_content
def filter(self, record: logging.LogRecord):
"""
Extend LogRecord by data from Telegram Update object.
:param record:
:return:
"""
update = types.Update.get_current(True)
if update:
for key, value in self.make_prefix(self.prefix, self.process_update(update)):
setattr(record, key, value)
return True
def process_update(self, update: types.Update):
"""
Parse Update object
:param update:
:return:
"""
yield 'update_id', update.update_id
if update.message:
yield 'update_type', 'message'
yield from self.process_message(update.message)
if update.edited_message:
yield 'update_type', 'edited_message'
yield from self.process_message(update.edited_message)
if update.channel_post:
yield 'update_type', 'channel_post'
yield from self.process_message(update.channel_post)
if update.edited_channel_post:
yield 'update_type', 'edited_channel_post'
yield from self.process_message(update.edited_channel_post)
if update.inline_query:
yield 'update_type', 'inline_query'
yield from self.process_inline_query(update.inline_query)
if update.chosen_inline_result:
yield 'update_type', 'chosen_inline_result'
yield from self.process_chosen_inline_result(update.chosen_inline_result)
if update.callback_query:
yield 'update_type', 'callback_query'
yield from self.process_callback_query(update.callback_query)
if update.shipping_query:
yield 'update_type', 'shipping_query'
yield from self.process_shipping_query(update.shipping_query)
if update.pre_checkout_query:
yield 'update_type', 'pre_checkout_query'
yield from self.process_pre_checkout_query(update.pre_checkout_query)
def make_prefix(self, prefix, iterable):
"""
Add prefix to the label
:param prefix:
:param iterable:
:return:
"""
if not prefix:
yield from iterable
for key, value in iterable:
yield f"{prefix}_{key}", value
def process_user(self, user: types.User):
"""
Generate user data
:param user:
:return:
"""
if not user:
return
yield 'user_id', user.id
if self.include_content:
yield 'user_full_name', user.full_name
if user.username:
yield 'user_name', f"@{user.username}"
def process_chat(self, chat: types.Chat):
"""
Generate chat data
:param chat:
:return:
"""
if not chat:
return
yield 'chat_id', chat.id
yield 'chat_type', chat.type
if self.include_content:
yield 'chat_title', chat.full_name
if chat.username:
yield 'chat_name', f"@{chat.username}"
def process_message(self, message: types.Message):
yield 'message_content_type', message.content_type
yield from self.process_user(message.from_user)
yield from self.process_chat(message.chat)
if not self.include_content:
return
if message.reply_to_message:
yield from self.make_prefix('reply_to', self.process_message(message.reply_to_message))
if message.forward_from:
yield from self.make_prefix('forward_from', self.process_user(message.forward_from))
if message.forward_from_chat:
yield from self.make_prefix('forward_from_chat', self.process_chat(message.forward_from_chat))
if message.forward_from_message_id:
yield 'message_forward_from_message_id', message.forward_from_message_id
if message.forward_date:
yield 'message_forward_date', message.forward_date
if message.edit_date:
yield 'message_edit_date', message.edit_date
if message.media_group_id:
yield 'message_media_group_id', message.media_group_id
if message.author_signature:
yield 'message_author_signature', message.author_signature
if message.text:
yield 'text', message.text or message.caption
yield 'html_text', message.html_text
elif message.audio:
yield 'audio', message.audio.file_id
elif message.animation:
yield 'animation', message.animation.file_id
elif message.document:
yield 'document', message.document.file_id
elif message.game:
yield 'game', message.game.title
elif message.photo:
yield 'photo', message.photo[-1].file_id
elif message.sticker:
yield 'sticker', message.sticker.file_id
elif message.video:
yield 'video', message.video.file_id
elif message.video_note:
yield 'video_note', message.video_note.file_id
elif message.voice:
yield 'voice', message.voice.file_id
elif message.contact:
yield 'contact_full_name', message.contact.full_name
yield 'contact_phone_number', message.contact.phone_number
elif message.venue:
yield 'venue_address', message.venue.address
yield 'location_latitude', message.venue.location.latitude
yield 'location_longitude', message.venue.location.longitude
elif message.location:
yield 'location_latitude', message.location.latitude
yield 'location_longitude', message.location.longitude
elif message.new_chat_members:
yield 'new_chat_members', [user.id for user in message.new_chat_members]
elif message.left_chat_member:
yield 'left_chat_member', [user.id for user in message.new_chat_members]
elif message.invoice:
yield 'invoice_title', message.invoice.title
yield 'invoice_description', message.invoice.description
yield 'invoice_start_parameter', message.invoice.start_parameter
yield 'invoice_currency', message.invoice.currency
yield 'invoice_total_amount', message.invoice.total_amount
elif message.successful_payment:
yield 'successful_payment_currency', message.successful_payment.currency
yield 'successful_payment_total_amount', message.successful_payment.total_amount
yield 'successful_payment_invoice_payload', message.successful_payment.invoice_payload
yield 'successful_payment_shipping_option_id', message.successful_payment.shipping_option_id
yield 'successful_payment_telegram_payment_charge_id', message.successful_payment.telegram_payment_charge_id
yield 'successful_payment_provider_payment_charge_id', message.successful_payment.provider_payment_charge_id
elif message.connected_website:
yield 'connected_website', message.connected_website
elif message.migrate_from_chat_id:
yield 'migrate_from_chat_id', message.migrate_from_chat_id
elif message.migrate_to_chat_id:
yield 'migrate_to_chat_id', message.migrate_to_chat_id
elif message.pinned_message:
yield from self.make_prefix('pinned_message', message.pinned_message)
elif message.new_chat_title:
yield 'new_chat_title', message.new_chat_title
elif message.new_chat_photo:
yield 'new_chat_photo', message.new_chat_photo[-1].file_id
# elif message.delete_chat_photo:
# yield 'delete_chat_photo', message.delete_chat_photo
# elif message.group_chat_created:
# yield 'group_chat_created', message.group_chat_created
# elif message.passport_data:
# yield 'passport_data', message.passport_data
def process_inline_query(self, inline_query: types.InlineQuery):
yield 'inline_query_id', inline_query.id
yield from self.process_user(inline_query.from_user)
if self.include_content:
yield 'inline_query_text', inline_query.query
if inline_query.location:
yield 'location_latitude', inline_query.location.latitude
yield 'location_longitude', inline_query.location.longitude
if inline_query.offset:
yield 'inline_query_offset', inline_query.offset
def process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult):
yield 'chosen_inline_result_id', chosen_inline_result.result_id
yield from self.process_user(chosen_inline_result.from_user)
if self.include_content:
yield 'inline_query_text', chosen_inline_result.query
if chosen_inline_result.location:
yield 'location_latitude', chosen_inline_result.location.latitude
yield 'location_longitude', chosen_inline_result.location.longitude
def process_callback_query(self, callback_query: types.CallbackQuery):
yield from self.process_user(callback_query.from_user)
yield 'callback_query_data', callback_query.data
if callback_query.message:
yield from self.make_prefix('callback_query_message', self.process_message(callback_query.message))
if callback_query.inline_message_id:
yield 'callback_query_inline_message_id', callback_query.inline_message_id
if callback_query.chat_instance:
yield 'callback_query_chat_instance', callback_query.chat_instance
if callback_query.game_short_name:
yield 'callback_query_game_short_name', callback_query.game_short_name
def process_shipping_query(self, shipping_query: types.ShippingQuery):
yield 'shipping_query_id', shipping_query.id
yield from self.process_user(shipping_query.from_user)
if self.include_content:
yield 'shipping_query_invoice_payload', shipping_query.invoice_payload
def process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery):
yield 'pre_checkout_query_id', pre_checkout_query.id
yield from self.process_user(pre_checkout_query.from_user)
if self.include_content:
yield 'pre_checkout_query_currency', pre_checkout_query.currency
yield 'pre_checkout_query_total_amount', pre_checkout_query.total_amount
yield 'pre_checkout_query_invoice_payload', pre_checkout_query.invoice_payload
yield 'pre_checkout_query_shipping_option_id', pre_checkout_query.shipping_option_id

View file

@ -1,17 +0,0 @@
from . import filters
from . import handler
from . import middlewares
from . import storage
from . import webhook
from .dispatcher import Dispatcher, FSMContext, DEFAULT_RATE_LIMIT
__all__ = [
"DEFAULT_RATE_LIMIT",
"Dispatcher",
"FSMContext",
"filters",
"handler",
"middlewares",
"storage",
"webhook",
]

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
Text, IDFilter, AdminFilter, IsReplyFilter
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
__all__ = [
'AbstractFilter',
'BoundFilter',
'Command',
'CommandStart',
'CommandHelp',
'CommandPrivacy',
'CommandSettings',
'ContentTypeFilter',
'ExceptionsFilter',
'HashTag',
'Filter',
'FilterNotPassed',
'FilterRecord',
'FiltersFactory',
'RegexpCommandsFilter',
'Regexp',
'StateFilter',
'Text',
'IDFilter',
'IsReplyFilter',
'AdminFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
'check_filters',
]

View file

@ -1,646 +0,0 @@
import inspect
import re
import typing
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, Optional, Union
from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
class Command(Filter):
"""
You can handle commands by using this filter.
If filter is successful processed the :obj:`Command.CommandObj` will be passed to the handler arguments.
By default this filter is registered for messages and edited messages handlers.
"""
def __init__(self, commands: Union[Iterable, str],
prefixes: Union[Iterable, str] = '/',
ignore_case: bool = True,
ignore_mention: bool = False):
"""
Filter can be initialized from filters factory or by simply creating instance of this class.
Examples:
.. code-block:: python
@dp.message_handler(commands=['myCommand'])
@dp.message_handler(Command(['myCommand']))
@dp.message_handler(commands=['myCommand'], commands_prefix='!/')
:param commands: Command or list of commands always without leading slashes (prefix)
:param prefixes: Allowed commands prefix. By default is slash.
If you change the default behavior pass the list of prefixes to this argument.
:param ignore_case: Ignore case of the command
:param ignore_mention: Ignore mention in command
(By default this filter pass only the commands addressed to current bot)
"""
if isinstance(commands, str):
commands = (commands,)
self.commands = list(map(str.lower, commands)) if ignore_case else commands
self.prefixes = prefixes
self.ignore_case = ignore_case
self.ignore_mention = ignore_mention
@classmethod
def validate(cls, full_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Validator for filters factory
From filters factory this filter can be registered with arguments:
- ``command``
- ``commands_prefix`` (will be passed as ``prefixes``)
- ``commands_ignore_mention`` (will be passed as ``ignore_mention``
:param full_config:
:return: config or empty dict
"""
config = {}
if 'commands' in full_config:
config['commands'] = full_config.pop('commands')
if config and 'commands_prefix' in full_config:
config['prefixes'] = full_config.pop('commands_prefix')
if config and 'commands_ignore_mention' in full_config:
config['ignore_mention'] = full_config.pop('commands_ignore_mention')
return config
async def check(self, message: types.Message):
return await self.check_command(message, self.commands, self.prefixes, self.ignore_case, self.ignore_mention)
@staticmethod
async def check_command(message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False):
if not message.text: # Prevent to use with non-text content types
return False
full_command = message.text.split()[0]
prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@')
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
return False
if prefix not in prefixes:
return False
if (command.lower() if ignore_case else command) not in commands:
return False
return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)}
@dataclass
class CommandObj:
"""
Instance of this object is always has command and it prefix.
Can be passed as keyword argument ``command`` to the handler
"""
"""Command prefix"""
prefix: str = '/'
"""Command without prefix and mention"""
command: str = ''
"""Mention (if available)"""
mention: str = None
"""Command argument"""
args: str = field(repr=False, default=None)
@property
def mentioned(self) -> bool:
"""
This command has mention?
:return:
"""
return bool(self.mention)
@property
def text(self) -> str:
"""
Generate original text from object
:return:
"""
line = self.prefix + self.command
if self.mentioned:
line += '@' + self.mention
if self.args:
line += ' ' + self.args
return line
class CommandStart(Command):
"""
This filter based on :obj:`Command` filter but can handle only ``/start`` command.
"""
def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None):
"""
Also this filter can handle `deep-linking <https://core.telegram.org/bots#deep-linking>`_ arguments.
Example:
.. code-block:: python
@dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)')))
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
"""
super().__init__(['start'])
self.deep_link = deep_link
async def check(self, message: types.Message):
"""
If deep-linking is passed to the filter result of the matching will be passed as ``deep_link`` to the handler
:param message:
:return:
"""
check = await super().check(message)
if check and self.deep_link is not None:
if not isinstance(self.deep_link, re.Pattern):
return message.get_args() == self.deep_link
match = self.deep_link.match(message.get_args())
if match:
return {'deep_link': match}
return False
return check
class CommandHelp(Command):
"""
This filter based on :obj:`Command` filter but can handle only ``/help`` command.
"""
def __init__(self):
super().__init__(['help'])
class CommandSettings(Command):
"""
This filter based on :obj:`Command` filter but can handle only ``/settings`` command.
"""
def __init__(self):
super().__init__(['settings'])
class CommandPrivacy(Command):
"""
This filter based on :obj:`Command` filter but can handle only ``/privacy`` command.
"""
def __init__(self):
super().__init__(['privacy'])
class Text(Filter):
"""
Simple text filter
"""
_default_params = (
('text', 'equals'),
('text_contains', 'contains'),
('text_startswith', 'startswith'),
('text_endswith', 'endswith'),
)
def __init__(self,
equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None,
ignore_case=False):
"""
Check text for one of pattern. Only one mode can be used in one filter.
In every pattern, a single string is treated as a list with 1 element.
:param equals: True if object's text in the list
:param contains: True if object's text contains all strings from the list
:param startswith: True if object's text starts with any of strings from the list
:param endswith: True if object's text ends with any of strings from the list
:param ignore_case: case insensitive
"""
# Only one mode can be used. check it.
check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
if check > 1:
args = "' and '".join([arg[0] for arg in [('equals', equals),
('contains', contains),
('startswith', startswith),
('endswith', endswith)
] if arg[1] is not None])
raise ValueError(f"Arguments '{args}' cannot be used together.")
elif check == 0:
raise ValueError(f"No one mode is specified!")
equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
else e,
(equals, contains, endswith, startswith))
self.equals = equals
self.contains = contains
self.endswith = endswith
self.startswith = startswith
self.ignore_case = ignore_case
@classmethod
def validate(cls, full_config: Dict[str, Any]):
for param, key in cls._default_params:
if param in full_config:
return {key: full_config.pop(param)}
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
text = obj.text or obj.caption or ''
if not text and obj.poll:
text = obj.poll.question
elif isinstance(obj, CallbackQuery):
text = obj.data
elif isinstance(obj, InlineQuery):
text = obj.query
elif isinstance(obj, Poll):
text = obj.question
else:
return False
if self.ignore_case:
text = text.lower()
_pre_process_func = lambda s: str(s).lower()
else:
_pre_process_func = str
# now check
if self.equals is not None:
equals = list(map(_pre_process_func, self.equals))
return text in equals
if self.contains is not None:
contains = list(map(_pre_process_func, self.contains))
return all(map(text.__contains__, contains))
if self.startswith is not None:
startswith = list(map(_pre_process_func, self.startswith))
return any(map(text.startswith, startswith))
if self.endswith is not None:
endswith = list(map(_pre_process_func, self.endswith))
return any(map(text.endswith, endswith))
return False
class HashTag(Filter):
"""
Filter for hashtag's and cashtag's
"""
# TODO: allow to use regexp
def __init__(self, hashtags=None, cashtags=None):
if not hashtags and not cashtags:
raise ValueError('No one hashtag or cashtag is specified!')
if hashtags is None:
hashtags = []
elif isinstance(hashtags, str):
hashtags = [hashtags]
if cashtags is None:
cashtags = []
elif isinstance(cashtags, str):
cashtags = [cashtags.upper()]
else:
cashtags = list(map(str.upper, cashtags))
self.hashtags = hashtags
self.cashtags = cashtags
@classmethod
def validate(cls, full_config: Dict[str, Any]):
config = {}
if 'hashtags' in full_config:
config['hashtags'] = full_config.pop('hashtags')
if 'cashtags' in full_config:
config['cashtags'] = full_config.pop('cashtags')
return config
async def check(self, message: types.Message):
if message.caption:
text = message.caption
entities = message.caption_entities
elif message.text:
text = message.text
entities = message.entities
else:
return False
hashtags, cashtags = self._get_tags(text, entities)
if self.hashtags and set(hashtags) & set(self.hashtags) \
or self.cashtags and set(cashtags) & set(self.cashtags):
return {'hashtags': hashtags, 'cashtags': cashtags}
def _get_tags(self, text, entities):
hashtags = []
cashtags = []
for entity in entities:
if entity.type == types.MessageEntityType.HASHTAG:
value = entity.get_text(text).lstrip('#')
hashtags.append(value)
elif entity.type == types.MessageEntityType.CASHTAG:
value = entity.get_text(text).lstrip('$')
cashtags.append(value)
return hashtags, cashtags
class Regexp(Filter):
"""
Regexp filter for messages and callback query
"""
def __init__(self, regexp):
if not isinstance(regexp, re.Pattern):
regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE)
self.regexp = regexp
@classmethod
def validate(cls, full_config: Dict[str, Any]):
if 'regexp' in full_config:
return {'regexp': full_config.pop('regexp')}
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
if isinstance(obj, Message):
content = obj.text or obj.caption or ''
if not content and obj.poll:
content = obj.poll.question
elif isinstance(obj, CallbackQuery) and obj.data:
content = obj.data
elif isinstance(obj, InlineQuery):
content = obj.query
elif isinstance(obj, Poll):
content = obj.question
else:
return False
match = self.regexp.search(content)
if match:
return {'regexp': match}
return False
class RegexpCommandsFilter(BoundFilter):
"""
Check commands by regexp in message
"""
key = 'regexp_commands'
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:
return {'regexp_command': search}
return False
class ContentTypeFilter(BoundFilter):
"""
Check message content type
"""
key = 'content_types'
required = True
default = types.ContentTypes.TEXT
def __init__(self, content_types):
self.content_types = content_types
async def check(self, message):
return types.ContentType.ANY in self.content_types or \
message.content_type in self.content_types
class StateFilter(BoundFilter):
"""
Check user state
"""
key = 'state'
required = True
ctx_state = ContextVar('user_state')
def __init__(self, dispatcher, state):
from aiogram.dispatcher.filters.state import State, StatesGroup
self.dispatcher = dispatcher
states = []
if not isinstance(state, (list, set, tuple, frozenset)) or state is None:
state = [state, ]
for item in state:
if isinstance(item, State):
states.append(item.state)
elif inspect.isclass(item) and issubclass(item, StatesGroup):
states.extend(item.all_states_names)
else:
states.append(item)
self.states = states
def get_target(self, obj):
return getattr(getattr(obj, 'chat', None), 'id', None), getattr(getattr(obj, 'from_user', None), 'id', None)
async def check(self, obj):
if '*' in self.states:
return {'state': self.dispatcher.current_state()}
try:
state = self.ctx_state.get()
except LookupError:
chat, user = self.get_target(obj)
if chat or user:
state = await self.dispatcher.storage.get_state(chat=chat, user=user)
self.ctx_state.set(state)
if state in self.states:
return {'state': self.dispatcher.current_state(), 'raw_state': state}
else:
if state in self.states:
return {'state': self.dispatcher.current_state(), 'raw_state': state}
return False
class ExceptionsFilter(BoundFilter):
"""
Filter for exceptions
"""
key = 'exception'
def __init__(self, exception):
self.exception = exception
async def check(self, update, exception):
try:
raise exception
except self.exception:
return True
except:
return False
class IDFilter(Filter):
def __init__(self,
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
):
"""
:param user_id:
:param chat_id:
"""
if user_id is None and chat_id is None:
raise ValueError("Both user_id and chat_id can't be None")
self.user_id = None
self.chat_id = None
if user_id:
if isinstance(user_id, Iterable):
self.user_id = list(map(int, user_id))
else:
self.user_id = [int(user_id), ]
if chat_id:
if isinstance(chat_id, Iterable):
self.chat_id = list(map(int, chat_id))
else:
self.chat_id = [int(chat_id), ]
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if 'user_id' in full_config:
result['user_id'] = full_config.pop('user_id')
if 'chat_id' in full_config:
result['chat_id'] = full_config.pop('chat_id')
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
if isinstance(obj, Message):
user_id = obj.from_user.id
chat_id = obj.chat.id
elif isinstance(obj, CallbackQuery):
user_id = obj.from_user.id
chat_id = None
if obj.message is not None:
# if the button was sent with message
chat_id = obj.message.chat.id
elif isinstance(obj, InlineQuery):
user_id = obj.from_user.id
chat_id = None
else:
return False
if self.user_id and self.chat_id:
return user_id in self.user_id and chat_id in self.chat_id
if self.user_id:
return user_id in self.user_id
if self.chat_id:
return chat_id in self.chat_id
return False
class AdminFilter(Filter):
"""
Checks if user is admin in a chat.
If is_chat_admin is not set, the filter will check in the current chat (correct only for messages).
is_chat_admin is required for InlineQuery.
"""
def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
if is_chat_admin:
if isinstance(is_chat_admin, bool):
self._check_current = is_chat_admin
if isinstance(is_chat_admin, Iterable):
self._chat_ids = list(is_chat_admin)
else:
self._chat_ids = [is_chat_admin]
else:
self._check_current = True
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
result = {}
if "is_chat_admin" in full_config:
result["is_chat_admin"] = full_config.pop("is_chat_admin")
return result
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool:
user_id = obj.from_user.id
if self._check_current:
if isinstance(obj, Message):
message = obj
elif isinstance(obj, CallbackQuery) and obj.message:
message = obj.message
else:
return False
if ChatType.is_private(message): # there is no admin in private chats
return False
chat_ids = [message.chat.id]
else:
chat_ids = self._chat_ids
admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)]
return user_id in admins
class IsReplyFilter(BoundFilter):
"""
Check if message is replied and send reply message to handler
"""
key = 'is_reply'
def __init__(self, is_reply):
self.is_reply = is_reply
async def check(self, msg: Message):
if msg.reply_to_message and self.is_reply:
return {'reply': msg.reply_to_message}
elif not msg.reply_to_message and not self.is_reply:
return True

View file

@ -1,80 +0,0 @@
import typing
from .filters import AbstractFilter, FilterRecord
from ..handler import Handler
class FiltersFactory:
"""
Filters factory
"""
def __init__(self, dispatcher):
self._dispatcher = dispatcher
self._registered: typing.List[FilterRecord] = []
def bind(
self,
callback: typing.Union[typing.Callable, AbstractFilter],
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.List[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
):
"""
Register filter
:param callback: callable or subclass of :obj:`AbstractFilter`
:param validator: custom validator.
:param event_handlers: list of instances of :obj:`Handler`
:param exclude_event_handlers: list of excluded event handlers (:obj:`Handler`)
"""
record = FilterRecord(callback, validator, event_handlers, exclude_event_handlers)
self._registered.append(record)
def unbind(self, callback: typing.Union[typing.Callable, AbstractFilter]):
"""
Unregister callback
:param callback: callable of subclass of :obj:`AbstractFilter`
"""
for record in self._registered:
if record.callback == callback:
self._registered.remove(record)
def resolve(
self, event_handler, *custom_filters, **full_config
) -> typing.List[typing.Union[typing.Callable, AbstractFilter]]:
"""
Resolve filters to filters-set
:param event_handler:
:param custom_filters:
:param full_config:
:return:
"""
filters_set = []
filters_set.extend(
self._resolve_registered(
event_handler, {k: v for k, v in full_config.items() if v is not None}
)
)
if custom_filters:
filters_set.extend(custom_filters)
return filters_set
def _resolve_registered(self, event_handler, full_config) -> typing.Generator:
"""
Resolve registered filters
:param event_handler:
:param full_config:
:return:
"""
for record in self._registered:
filter_ = record.resolve(self._dispatcher, event_handler, full_config)
if filter_:
yield filter_
if full_config:
raise NameError("Invalid filter name(s): '" + "', ".join(full_config.keys()) + "'")

View file

@ -1,289 +0,0 @@
import abc
import inspect
import typing
from ..handler import Handler, FilterObj
class FilterNotPassed(Exception):
pass
def wrap_async(func):
async def async_wrapper(*args, **kwargs):
return func(*args, **kwargs)
if inspect.isawaitable(func) \
or inspect.iscoroutinefunction(func) \
or isinstance(func, AbstractFilter):
return func
return async_wrapper
def get_filter_spec(dispatcher, filter_: callable):
kwargs = {}
if not callable(filter_):
raise TypeError('Filter must be callable and/or awaitable!')
spec = inspect.getfullargspec(filter_)
if 'dispatcher' in spec:
kwargs['dispatcher'] = dispatcher
if inspect.isawaitable(filter_) \
or inspect.iscoroutinefunction(filter_) \
or isinstance(filter_, AbstractFilter):
return FilterObj(filter=filter_, kwargs=kwargs, is_async=True)
else:
return FilterObj(filter=filter_, kwargs=kwargs, is_async=False)
def get_filters_spec(dispatcher, filters: typing.Iterable[callable]):
data = []
if filters is not None:
for i in filters:
data.append(get_filter_spec(dispatcher, i))
return data
async def execute_filter(filter_: FilterObj, args):
"""
Helper for executing filter
:param filter_:
:param args:
:return:
"""
if filter_.is_async:
return await filter_.filter(*args, **filter_.kwargs)
else:
return filter_.filter(*args, **filter_.kwargs)
async def check_filters(filters: typing.Iterable[FilterObj], args):
"""
Check list of filters
:param filters:
:param args:
:return:
"""
data = {}
if filters is not None:
for filter_ in filters:
f = await execute_filter(filter_, args)
if not f:
raise FilterNotPassed()
elif isinstance(f, dict):
data.update(f)
return data
class FilterRecord:
"""
Filters record for factory
"""
def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'],
validator: typing.Optional[typing.Callable] = None,
event_handlers: typing.Optional[typing.Iterable[Handler]] = None,
exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None):
if event_handlers and exclude_event_handlers:
raise ValueError("'event_handlers' and 'exclude_event_handlers' arguments cannot be used together.")
self.callback = callback
self.event_handlers = event_handlers
self.exclude_event_handlers = exclude_event_handlers
if validator is not None:
if not callable(validator):
raise TypeError(f"validator must be callable, not {type(validator)}")
self.resolver = validator
elif issubclass(callback, AbstractFilter):
self.resolver = callback.validate
else:
raise RuntimeError('validator is required!')
def resolve(self, dispatcher, event_handler, full_config):
if not self._check_event_handler(event_handler):
return
config = self.resolver(full_config)
if config:
if 'dispatcher' not in config:
spec = inspect.getfullargspec(self.callback)
if 'dispatcher' in spec.args:
config['dispatcher'] = dispatcher
for key in config:
if key in full_config:
full_config.pop(key)
return self.callback(**config)
def _check_event_handler(self, event_handler) -> bool:
if self.event_handlers:
return event_handler in self.event_handlers
elif self.exclude_event_handlers:
return event_handler not in self.exclude_event_handlers
return True
class AbstractFilter(abc.ABC):
"""
Abstract class for custom filters.
"""
@classmethod
@abc.abstractmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
"""
Validate and parse config.
This method will be called by the filters factory when you bind this filter.
Must be overridden.
:param full_config: dict with arguments passed to handler registrar
:return: Current filter config
"""
pass
@abc.abstractmethod
async def check(self, *args) -> bool:
"""
Will be called when filters checks.
This method must be overridden.
:param args:
:return:
"""
pass
async def __call__(self, *args) -> bool:
return await self.check(*args)
def __invert__(self):
return NotFilter(self)
def __and__(self, other):
if isinstance(self, AndFilter):
self.append(other)
return self
return AndFilter(self, other)
def __or__(self, other):
if isinstance(self, OrFilter):
self.append(other)
return self
return OrFilter(self, other)
class Filter(AbstractFilter):
"""
You can make subclasses of that class for custom filters.
Method ``check`` must be overridden
"""
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
"""
Here method ``validate`` is optional.
If you need to use filter from filters factory you need to override this method.
:param full_config: dict with arguments passed to handler registrar
:return: Current filter config
"""
pass
class BoundFilter(Filter):
"""
To easily create your own filters with one parameter, you can inherit from this filter.
You need to implement ``__init__`` method with single argument related with key attribute
and ``check`` method where you need to implement filter logic.
"""
key = None
"""Unique name of the filter argument. You need to override this attribute."""
required = False
"""If :obj:`True` this filter will be added to the all of the registered handlers"""
default = None
"""Default value for configure required filters"""
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
"""
If ``cls.key`` is not :obj:`None` and that is in config returns config with that argument.
:param full_config:
:return:
"""
if cls.key is not None:
if cls.key in full_config:
return {cls.key: full_config[cls.key]}
elif cls.required:
return {cls.key: cls.default}
class _LogicFilter(Filter):
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]):
raise ValueError('That filter can\'t be used in filters factory!')
class NotFilter(_LogicFilter):
def __init__(self, target):
self.target = wrap_async(target)
async def check(self, *args):
return not bool(await self.target(*args))
class AndFilter(_LogicFilter):
def __init__(self, *targets):
self.targets = list(wrap_async(target) for target in targets)
async def check(self, *args):
"""
All filters must return a positive result
:param args:
:return:
"""
data = {}
for target in self.targets:
result = await target(*args)
if not result:
return False
if isinstance(result, dict):
data.update(result)
if not data:
return True
return data
def append(self, target):
self.targets.append(wrap_async(target))
class OrFilter(_LogicFilter):
def __init__(self, *targets):
self.targets = list(wrap_async(target) for target in targets)
async def check(self, *args):
"""
One of filters must return a positive result
:param args:
:return:
"""
for target in self.targets:
result = await target(*args)
if result:
if isinstance(result, dict):
return result
return True
return False
def append(self, target):
self.targets.append(wrap_async(target))

View file

@ -1,197 +0,0 @@
import inspect
from typing import Optional
from ..dispatcher import Dispatcher
class State:
"""
State object
"""
def __init__(self, state: Optional[str] = None, group_name: Optional[str] = None):
self._state = state
self._group_name = group_name
self._group = None
@property
def group(self):
if not self._group:
raise RuntimeError('This state is not in any group.')
return self._group
def get_root(self):
return self.group.get_root()
@property
def state(self):
if self._state is None or self._state == '*':
return self._state
if self._group_name is None and self._group:
group = self._group.__full_group_name__
elif self._group_name:
group = self._group_name
else:
group = '@'
return f'{group}:{self._state}'
def set_parent(self, group):
if not issubclass(group, StatesGroup):
raise ValueError('Group must be subclass of StatesGroup')
self._group = group
def __set_name__(self, owner, name):
if self._state is None:
self._state = name
self.set_parent(owner)
def __str__(self):
return f"<State '{self.state or ''}'>"
__repr__ = __str__
async def set(self):
state = Dispatcher.get_current().current_state()
await state.set_state(self.state)
class StatesGroupMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super(StatesGroupMeta, mcs).__new__(mcs, name, bases, namespace)
states = []
childs = []
cls._group_name = name
for name, prop in namespace.items():
if isinstance(prop, State):
states.append(prop)
elif inspect.isclass(prop) and issubclass(prop, StatesGroup):
childs.append(prop)
prop._parent = cls
cls._parent = None
cls._childs = tuple(childs)
cls._states = tuple(states)
cls._state_names = tuple(state.state for state in states)
return cls
@property
def __group_name__(cls) -> str:
return cls._group_name
@property
def __full_group_name__(cls) -> str:
if cls._parent:
return '.'.join((cls._parent.__full_group_name__, cls._group_name))
return cls._group_name
@property
def states(cls) -> tuple:
return cls._states
@property
def childs(cls) -> tuple:
return cls._childs
@property
def all_childs(cls):
result = cls.childs
for child in cls.childs:
result += child.childs
return result
@property
def all_states(cls):
result = cls.states
for group in cls.childs:
result += group.all_states
return result
@property
def all_states_names(cls):
return tuple(state.state for state in cls.all_states)
@property
def states_names(cls) -> tuple:
return tuple(state.state for state in cls.states)
def get_root(cls):
if cls._parent is None:
return cls
return cls._parent.get_root()
def __contains__(cls, item):
if isinstance(item, str):
return item in cls.all_states_names
if isinstance(item, State):
return item in cls.all_states
if isinstance(item, StatesGroup):
return item in cls.all_childs
return False
def __str__(self):
return f"<StatesGroup '{self.__full_group_name__}'>"
class StatesGroup(metaclass=StatesGroupMeta):
@classmethod
async def next(cls) -> str:
state = Dispatcher.get_current().current_state()
state_name = await state.get_state()
try:
next_step = cls.states_names.index(state_name) + 1
except ValueError:
next_step = 0
try:
next_state_name = cls.states[next_step].state
except IndexError:
next_state_name = None
await state.set_state(next_state_name)
return next_state_name
@classmethod
async def previous(cls) -> str:
state = Dispatcher.get_current().current_state()
state_name = await state.get_state()
try:
previous_step = cls.states_names.index(state_name) - 1
except ValueError:
previous_step = 0
if previous_step < 0:
previous_state_name = None
else:
previous_state_name = cls.states[previous_step].state
await state.set_state(previous_state_name)
return previous_state_name
@classmethod
async def first(cls) -> str:
state = Dispatcher.get_current().current_state()
first_step_name = cls.states_names[0]
await state.set_state(first_step_name)
return first_step_name
@classmethod
async def last(cls) -> str:
state = Dispatcher.get_current().current_state()
last_step_name = cls.states_names[-1]
await state.set_state(last_step_name)
return last_step_name
default_state = State()
any_state = State(state='*')

View file

@ -1,139 +0,0 @@
import inspect
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Optional, Iterable, List
ctx_data = ContextVar('ctx_handler_data')
current_handler = ContextVar('current_handler')
@dataclass
class FilterObj:
filter: callable
kwargs: dict
is_async: bool
class SkipHandler(Exception):
pass
class CancelHandler(Exception):
pass
def _get_spec(func: callable):
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
func = func.__wrapped__
spec = inspect.getfullargspec(func)
return spec
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
if spec.varkw:
return kwargs
return {k: v for k, v in kwargs.items() if k in spec.args}
class Handler:
def __init__(self, dispatcher, once=True, middleware_key=None):
self.dispatcher = dispatcher
self.once = once
self.handlers: List[Handler.HandlerObj] = []
self.middleware_key = middleware_key
def register(self, handler, filters=None, index=None):
"""
Register callback
Filters can be awaitable or not.
:param handler: coroutine
:param filters: list of filters
:param index: you can reorder handlers
"""
from .filters import get_filters_spec
spec = _get_spec(handler)
if filters and not isinstance(filters, (list, tuple, set)):
filters = [filters]
filters = get_filters_spec(self.dispatcher, filters)
record = Handler.HandlerObj(handler=handler, spec=spec, filters=filters)
if index is None:
self.handlers.append(record)
else:
self.handlers.insert(index, record)
def unregister(self, handler):
"""
Remove handler
:param handler: callback
:return:
"""
for handler_obj in self.handlers:
registered = handler_obj.handler
if handler is registered:
self.handlers.remove(handler_obj)
return True
raise ValueError('This handler is not registered!')
async def notify(self, *args):
"""
Notify handlers
:param args:
:return:
"""
from .filters import check_filters, FilterNotPassed
results = []
data = {}
ctx_data.set(data)
if self.middleware_key:
try:
await self.dispatcher.middleware.trigger(f"pre_process_{self.middleware_key}", args + (data,))
except CancelHandler: # Allow to cancel current event
return results
try:
for handler_obj in self.handlers:
try:
data.update(await check_filters(handler_obj.filters, args))
except FilterNotPassed:
continue
else:
ctx_token = current_handler.set(handler_obj.handler)
try:
if self.middleware_key:
await self.dispatcher.middleware.trigger(f"process_{self.middleware_key}", args + (data,))
partial_data = _check_spec(handler_obj.spec, data)
response = await handler_obj.handler(*args, **partial_data)
if response is not None:
results.append(response)
if self.once:
break
except SkipHandler:
continue
except CancelHandler:
break
finally:
current_handler.reset(ctx_token)
finally:
if self.middleware_key:
await self.dispatcher.middleware.trigger(f"post_process_{self.middleware_key}",
args + (results, data,))
return results
@dataclass
class HandlerObj:
handler: callable
spec: inspect.FullArgSpec
filters: Optional[Iterable[FilterObj]] = None

View file

@ -1,130 +0,0 @@
import logging
import typing
log = logging.getLogger("aiogram.Middleware")
class MiddlewareManager:
"""
Middlewares manager. Works only with dispatcher.
"""
def __init__(self, dispatcher):
"""
Init
:param dispatcher: instance of Dispatcher
"""
self.dispatcher = dispatcher
self.loop = dispatcher.loop
self.bot = dispatcher.bot
self.storage = dispatcher.storage
self.applications = []
def setup(self, middleware):
"""
Setup middleware
:param middleware:
:return:
"""
if not isinstance(middleware, BaseMiddleware):
raise TypeError(
f"`middleware` must be an instance of BaseMiddleware, not {type(middleware)}"
)
if middleware.is_configured():
raise ValueError("That middleware is already used!")
self.applications.append(middleware)
middleware.setup(self)
log.debug(f"Loaded middleware '{middleware.__class__.__name__}'")
return middleware
async def trigger(self, action: str, args: typing.Iterable):
"""
Call action to middlewares with args lilt.
:param action:
:param args:
:return:
"""
for app in self.applications:
await app.trigger(action, args)
class BaseMiddleware:
"""
Base class for middleware.
All methods on the middle always must be coroutines and name starts with "on_" like "on_process_message".
"""
def __init__(self):
self._configured = False
self._manager = None
@property
def manager(self) -> MiddlewareManager:
"""
Instance of MiddlewareManager
"""
if self._manager is None:
raise RuntimeError("Middleware is not configured!")
return self._manager
def setup(self, manager):
"""
Mark middleware as configured
:param manager:
:return:
"""
self._manager = manager
self._configured = True
def is_configured(self) -> bool:
"""
Check middleware is configured
:return:
"""
return self._configured
async def trigger(self, action, args):
"""
Trigger action.
:param action:
:param args:
:return:
"""
handler_name = f"on_{action}"
handler = getattr(self, handler_name, None)
if not handler:
return None
await handler(*args)
class LifetimeControllerMiddleware(BaseMiddleware):
# TODO: Rename class
skip_patterns = None
async def pre_process(self, obj, data, *args):
pass
async def post_process(self, obj, data, *args):
pass
async def trigger(self, action, args):
if self.skip_patterns is not None and any(item in action for item in self.skip_patterns):
return False
obj, *args, data = args
if action.startswith("pre_process_"):
await self.pre_process(obj, data, *args)
elif action.startswith("post_process_"):
await self.post_process(obj, data, *args)
else:
return False
return True

View file

@ -1,562 +0,0 @@
import copy
import typing
from ..utils.deprecated import warn_deprecated as warn
from ..utils.exceptions import FSMStorageWarning
# Leak bucket
KEY = "key"
LAST_CALL = "called_at"
RATE_LIMIT = "rate_limit"
RESULT = "result"
EXCEEDED_COUNT = "exceeded"
DELTA = "delta"
THROTTLE_MANAGER = "$throttle_manager"
class BaseStorage:
"""
You are able to save current user's state
and data for all steps in states-storage
"""
async def close(self):
"""
You have to override this method and use when application shutdowns.
Perhaps you would like to save data and etc.
:return:
"""
raise NotImplementedError
async def wait_closed(self):
"""
You have to override this method for all asynchronous storages (e.g., Redis).
:return:
"""
raise NotImplementedError
@classmethod
def check_address(
cls,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
) -> (typing.Union[str, int], typing.Union[str, int]):
"""
In all storage's methods chat or user is always required.
If one of them is not provided, you have to set missing value based on the provided one.
This method performs the check described above.
:param chat:
:param user:
:return:
"""
if chat is None and user is None:
raise ValueError("`user` or `chat` parameter is required but no one is provided!")
if user is None and chat is not None:
user = chat
elif user is not None and chat is None:
chat = user
return chat, user
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
"""
Get current state of user in chat. Return `default` if no record is found.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param default:
:return:
"""
raise NotImplementedError
async def get_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[typing.Dict] = None,
) -> typing.Dict:
"""
Get state-data for user in chat. Return `default` if no data is provided in storage.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param default:
:return:
"""
raise NotImplementedError
async def set_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None,
):
"""
Set new state for user in chat
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param state:
"""
raise NotImplementedError
async def set_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
):
"""
Set data for user in chat
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param data:
"""
raise NotImplementedError
async def update_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
**kwargs,
):
"""
Update data for user in chat
You can use data parameter or|and kwargs.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param data:
:param chat:
:param user:
:param kwargs:
:return:
"""
raise NotImplementedError
async def reset_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
):
"""
Reset data for user in chat.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:return:
"""
await self.set_data(chat=chat, user=user, data={})
async def reset_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
with_data: typing.Optional[bool] = True,
):
"""
Reset state for user in chat.
You may desire to use this method when finishing conversations.
Chat or user is always required. If one of this is not presented,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param with_data:
:return:
"""
chat, user = self.check_address(chat=chat, user=user)
await self.set_state(chat=chat, user=user, state=None)
if with_data:
await self.set_data(chat=chat, user=user, data={})
async def finish(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
):
"""
Finish conversation for user in chat.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:return:
"""
await self.reset_state(chat=chat, user=user, with_data=True)
def has_bucket(self):
return False
async def get_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[dict] = None,
) -> typing.Dict:
"""
Get bucket for user in chat. Return `default` if no data is provided in storage.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param default:
:return:
"""
raise NotImplementedError
async def set_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
):
"""
Set bucket for user in chat
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:param bucket:
"""
raise NotImplementedError
async def update_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
bucket: typing.Dict = None,
**kwargs,
):
"""
Update bucket for user in chat
You can use bucket parameter or|and kwargs.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param bucket:
:param chat:
:param user:
:param kwargs:
:return:
"""
raise NotImplementedError
async def reset_bucket(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
):
"""
Reset bucket dor user in chat.
Chat or user is always required. If one of them is not provided,
you have to set missing value based on the provided one.
:param chat:
:param user:
:return:
"""
await self.set_data(chat=chat, user=user, data={})
class FSMContext:
def __init__(self, storage, chat, user):
self.storage: BaseStorage = storage
self.chat, self.user = self.storage.check_address(chat=chat, user=user)
def proxy(self):
return FSMContextProxy(self)
@staticmethod
def _resolve_state(value):
from .filters.state import State
if value is None:
return
elif isinstance(value, str):
return value
elif isinstance(value, State):
return value.state
return str(value)
async def get_state(self, default: typing.Optional[str] = None) -> typing.Optional[str]:
return await self.storage.get_state(
chat=self.chat, user=self.user, default=self._resolve_state(default)
)
async def get_data(self, default: typing.Optional[str] = None) -> typing.Dict:
return await self.storage.get_data(chat=self.chat, user=self.user, default=default)
async def update_data(self, data: typing.Dict = None, **kwargs):
await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs)
async def set_state(self, state: typing.Union[typing.AnyStr, None] = None):
await self.storage.set_state(
chat=self.chat, user=self.user, state=self._resolve_state(state)
)
async def set_data(self, data: typing.Dict = None):
await self.storage.set_data(chat=self.chat, user=self.user, data=data)
async def reset_state(self, with_data: typing.Optional[bool] = True):
await self.storage.reset_state(chat=self.chat, user=self.user, with_data=with_data)
async def reset_data(self):
await self.storage.reset_data(chat=self.chat, user=self.user)
async def finish(self):
await self.storage.finish(chat=self.chat, user=self.user)
class FSMContextProxy:
def __init__(self, fsm_context: FSMContext):
super(FSMContextProxy, self).__init__()
self.fsm_context = fsm_context
self._copy = {}
self._data = {}
self._state = None
self._is_dirty = False
self._closed = True
async def __aenter__(self):
await self.load()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
await self.save()
self._closed = True
def _check_closed(self):
if self._closed:
raise LookupError("Proxy is closed!")
@classmethod
async def create(cls, fsm_context: FSMContext):
"""
:param fsm_context:
:return:
"""
proxy = cls(fsm_context)
await proxy.load()
return proxy
async def load(self):
self._closed = False
self.clear()
self._state = await self.fsm_context.get_state()
self.update(await self.fsm_context.get_data())
self._copy = copy.deepcopy(self._data)
self._is_dirty = False
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._check_closed()
self._state = value
self._is_dirty = True
@state.deleter
def state(self):
self._check_closed()
self._state = None
self._is_dirty = True
async def save(self, force=False):
self._check_closed()
if self._copy != self._data or force:
await self.fsm_context.set_data(data=self._data)
if self._is_dirty or force:
await self.fsm_context.set_state(self.state)
self._is_dirty = False
self._copy = copy.deepcopy(self._data)
def clear(self):
del self.state
return self._data.clear()
def get(self, value, default=None):
return self._data.get(value, default)
def setdefault(self, key, default):
self._check_closed()
self._data.setdefault(key, default)
def update(self, data=None, **kwargs):
self._check_closed()
self._data.update(data, **kwargs)
def pop(self, key, default=None):
self._check_closed()
return self._data.pop(key, default)
def keys(self):
return self._data.keys()
def values(self):
return self._data.values()
def items(self):
return self._data.items()
def as_dict(self):
return copy.deepcopy(self._data)
def __len__(self):
return len(self._data)
def __iter__(self):
return self._data.__iter__()
def __getitem__(self, item):
return self._data[item]
def __setitem__(self, key, value):
self._check_closed()
self._data[key] = value
def __delitem__(self, key):
self._check_closed()
del self._data[key]
def __contains__(self, item):
return item in self._data
def __str__(self):
readable_state = f"'{self.state}'" if self.state else "<default>"
result = f"{self.__class__.__name__} state = {readable_state}, data = {self._data}"
if self._closed:
result += ", closed = True"
return result
class DisabledStorage(BaseStorage):
"""
Empty storage. Use it if you don't want to use Finite-State Machine
"""
async def close(self):
pass
async def wait_closed(self):
pass
async def get_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None,
) -> typing.Optional[str]:
return None
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:
self._warn()
return {}
async def update_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
**kwargs,
):
self._warn()
async def set_state(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None,
):
self._warn()
async def set_data(
self,
*,
chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
data: typing.Dict = None,
):
self._warn()
@staticmethod
def _warn():
warn(
f"You havent set any storage yet so no states and no data will be saved. \n"
f"You can connect MemoryStorage for debug purposes or non-essential data.",
FSMStorageWarning,
5,
)

File diff suppressed because it is too large Load diff

View file

@ -1,206 +0,0 @@
from . import base
from . import fields
from .animation import Animation
from .audio import Audio
from .auth_widget_data import AuthWidgetData
from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
from .chat_member import ChatMember, ChatMemberStatus
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
from .document import Document
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
from .file import File
from .force_reply import ForceReply
from .game import Game
from .game_high_score import GameHighScore
from .inline_keyboard import InlineKeyboardButton, InlineKeyboardMarkup
from .inline_query import InlineQuery
from .inline_query_result import (
InlineQueryResult,
InlineQueryResultArticle,
InlineQueryResultAudio,
InlineQueryResultCachedAudio,
InlineQueryResultCachedDocument,
InlineQueryResultCachedGif,
InlineQueryResultCachedMpeg4Gif,
InlineQueryResultCachedPhoto,
InlineQueryResultCachedSticker,
InlineQueryResultCachedVideo,
InlineQueryResultCachedVoice,
InlineQueryResultContact,
InlineQueryResultDocument,
InlineQueryResultGame,
InlineQueryResultGif,
InlineQueryResultLocation,
InlineQueryResultMpeg4Gif,
InlineQueryResultPhoto,
InlineQueryResultVenue,
InlineQueryResultVideo,
InlineQueryResultVoice,
)
from .input_file import InputFile
from .input_media import (
InputMedia,
InputMediaAnimation,
InputMediaAudio,
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
MediaGroup,
)
from .input_message_content import (
InputContactMessageContent,
InputLocationMessageContent,
InputMessageContent,
InputTextMessageContent,
InputVenueMessageContent,
)
from .invoice import Invoice
from .labeled_price import LabeledPrice
from .location import Location
from .login_url import LoginUrl
from .mask_position import MaskPosition
from .message import ContentType, ContentTypes, Message, ParseMode
from .message_entity import MessageEntity, MessageEntityType
from .order_info import OrderInfo
from .passport_data import PassportData
from .passport_element_error import (
PassportElementError,
PassportElementErrorDataField,
PassportElementErrorFile,
PassportElementErrorFiles,
PassportElementErrorFrontSide,
PassportElementErrorReverseSide,
PassportElementErrorSelfie,
)
from .passport_file import PassportFile
from .photo_size import PhotoSize
from .poll import PollOption, Poll
from .pre_checkout_query import PreCheckoutQuery
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
from .response_parameters import ResponseParameters
from .shipping_address import ShippingAddress
from .shipping_option import ShippingOption
from .shipping_query import ShippingQuery
from .sticker import Sticker
from .sticker_set import StickerSet
from .successful_payment import SuccessfulPayment
from .update import AllowedUpdates, Update
from .user import User
from .user_profile_photos import UserProfilePhotos
from .venue import Venue
from .video import Video
from .video_note import VideoNote
from .voice import Voice
from .webhook_info import WebhookInfo
__all__ = (
"AllowedUpdates",
"Animation",
"Audio",
"AuthWidgetData",
"CallbackGame",
"CallbackQuery",
"Chat",
"ChatActions",
"ChatMember",
"ChatMemberStatus",
"ChatPhoto",
"ChatType",
"ChosenInlineResult",
"Contact",
"ContentType",
"ContentTypes",
"Document",
"EncryptedCredentials",
"EncryptedPassportElement",
"File",
"ForceReply",
"Game",
"GameHighScore",
"InlineKeyboardButton",
"InlineKeyboardMarkup",
"InlineQuery",
"InlineQueryResult",
"InlineQueryResultArticle",
"InlineQueryResultAudio",
"InlineQueryResultCachedAudio",
"InlineQueryResultCachedDocument",
"InlineQueryResultCachedGif",
"InlineQueryResultCachedMpeg4Gif",
"InlineQueryResultCachedPhoto",
"InlineQueryResultCachedSticker",
"InlineQueryResultCachedVideo",
"InlineQueryResultCachedVoice",
"InlineQueryResultContact",
"InlineQueryResultDocument",
"InlineQueryResultGame",
"InlineQueryResultGif",
"InlineQueryResultLocation",
"InlineQueryResultMpeg4Gif",
"InlineQueryResultPhoto",
"InlineQueryResultVenue",
"InlineQueryResultVideo",
"InlineQueryResultVoice",
"InputContactMessageContent",
"InputFile",
"InputLocationMessageContent",
"InputMedia",
"InputMediaAnimation",
"InputMediaAudio",
"InputMediaDocument",
"InputMediaPhoto",
"InputMediaVideo",
"InputMessageContent",
"InputTextMessageContent",
"InputVenueMessageContent",
"Invoice",
"KeyboardButton",
"LabeledPrice",
"Location",
"LoginUrl",
"MaskPosition",
"MediaGroup",
"Message",
"MessageEntity",
"MessageEntityType",
"OrderInfo",
"ParseMode",
"PassportData",
"PassportElementError",
"PassportElementErrorDataField",
"PassportElementErrorFile",
"PassportElementErrorFiles",
"PassportElementErrorFrontSide",
"PassportElementErrorReverseSide",
"PassportElementErrorSelfie",
"PassportFile",
"PhotoSize",
"Poll",
"PollOption",
"PreCheckoutQuery",
"ReplyKeyboardMarkup",
"ReplyKeyboardRemove",
"ResponseParameters",
"ShippingAddress",
"ShippingOption",
"ShippingQuery",
"Sticker",
"StickerSet",
"SuccessfulPayment",
"Update",
"User",
"UserProfilePhotos",
"Venue",
"Video",
"VideoNote",
"Voice",
"WebhookInfo",
"base",
"fields",
)

View file

@ -1,20 +0,0 @@
from . import base
from . import fields
from . import mixins
from .photo_size import PhotoSize
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).
This object represents an animation file to be displayed in the message containing a game.
https://core.telegram.org/bots/api#animation
"""
file_id: base.String = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()

View file

@ -1,20 +0,0 @@
from . import base
from . import fields
from . import mixins
from .photo_size import PhotoSize
class Audio(base.TelegramObject, mixins.Downloadable):
"""
This object represents an audio file to be treated as music by the Telegram clients.
https://core.telegram.org/bots/api#audio
"""
file_id: base.String = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)

View file

@ -1,49 +0,0 @@
from __future__ import annotations
from aiohttp import web
from . import base
from . import fields
class AuthWidgetData(base.TelegramObject):
id: base.Integer = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
username: base.String = fields.Field()
photo_url: base.String = fields.Field()
auth_date: base.String = fields.DateTimeField()
hash: base.String = fields.Field()
@classmethod
def parse(cls, request: web.Request) -> AuthWidgetData:
"""
Parse request as Telegram auth widget data.
:param request:
:return: :obj:`AuthWidgetData`
:raise: :obj:`aiohttp.web.HTTPBadRequest`
"""
try:
query = dict(request.query)
query["id"] = int(query["id"])
query["auth_date"] = int(query["auth_date"])
widget = AuthWidgetData(**query)
except (ValueError, KeyError):
raise web.HTTPBadRequest(text="Invalid auth data")
else:
return widget
def validate(self):
return self.bot.check_auth_widget(self.to_python())
@property
def full_name(self):
result = self.first_name
if self.last_name:
result += " "
result += self.last_name
return result
def __hash__(self):
return self.id

View file

@ -1,288 +0,0 @@
from __future__ import annotations
import io
import typing
from typing import TypeVar
from babel.support import LazyProxy
from .fields import BaseField
from ..utils import json
from ..utils.mixins import ContextInstanceMixin
if typing.TYPE_CHECKING:
from ..bot.bot import Bot
__all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean')
PROPS_ATTR_NAME = '_props'
VALUES_ATTR_NAME = '_values'
ALIASES_ATTR_NAME = '_aliases'
# Binding of builtin types
InputFile = TypeVar('InputFile', 'InputFile', io.BytesIO, io.FileIO, str)
String = TypeVar('String', bound=str)
Integer = TypeVar('Integer', bound=int)
Float = TypeVar('Float', bound=float)
Boolean = TypeVar('Boolean', bound=bool)
T = TypeVar('T')
class MetaTelegramObject(type):
"""
Metaclass for telegram objects
"""
_objects = {}
def __new__(mcs: typing.Type[T], name: str, bases: typing.Tuple[typing.Type], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any) -> T:
cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace)
props = {}
values = {}
aliases = {}
# Get props, values, aliases from parent objects
for base in bases:
if not isinstance(base, MetaTelegramObject):
continue
props.update(getattr(base, PROPS_ATTR_NAME))
# values.update(getattr(base, VALUES_ATTR_NAME))
aliases.update(getattr(base, ALIASES_ATTR_NAME))
# Scan current object for props
for name, prop in ((name, prop) for name, prop in namespace.items() if isinstance(prop, BaseField)):
props[prop.alias] = prop
if prop.default is not None:
values[prop.alias] = prop.default
aliases[name] = prop.alias
# Set attributes
setattr(cls, PROPS_ATTR_NAME, props)
# setattr(cls, VALUES_ATTR_NAME, values)
setattr(cls, ALIASES_ATTR_NAME, aliases)
mcs._objects[cls.__name__] = cls
return cls
@property
def telegram_types(cls):
return cls._objects
class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
Abstract class for telegram objects
"""
def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None:
"""
Deserialize object
:param conf:
:param kwargs:
"""
if conf is None:
conf = {}
self._conf = conf
# Load data
for key, value in kwargs.items():
if key in self.props:
self.props[key].set_value(self, value, parent=self)
else:
self.values[key] = value
# Load default values
for key, value in self.props.items():
if value.default and key not in self.values:
self.values[key] = value.default
@property
def conf(self) -> typing.Dict[str, typing.Any]:
return self._conf
@property
def props(self) -> typing.Dict[str, BaseField]:
"""
Get props
:return: dict with props
"""
return getattr(self, PROPS_ATTR_NAME, {})
@property
def props_aliases(self) -> typing.Dict[str, str]:
"""
Get aliases for props
:return:
"""
return getattr(self, ALIASES_ATTR_NAME, {})
@property
def values(self) -> typing.Tuple[str]:
"""
Get values
:return:
"""
if not hasattr(self, VALUES_ATTR_NAME):
setattr(self, VALUES_ATTR_NAME, {})
return getattr(self, VALUES_ATTR_NAME)
@property
def telegram_types(self) -> typing.List[TelegramObject]:
return type(self).telegram_types
@classmethod
def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T:
"""
Deserialize object
:param data:
:return:
"""
return cls(**data)
@property
def bot(self) -> Bot:
from ..bot.bot import Bot
bot = Bot.get_current()
if bot is None:
raise RuntimeError("Can't get bot instance from context. "
"You can fix it with setting current instance: "
"'Bot.set_current(bot_instance)'")
return bot
def to_python(self) -> typing.Dict[str, typing.Any]:
"""
Get object as JSON serializable
:return:
"""
self.clean()
result = {}
for name, value in self.values.items():
if name in self.props:
value = self.props[name].export(self)
if isinstance(value, TelegramObject):
value = value.to_python()
if isinstance(value, LazyProxy):
value = str(value)
result[self.props_aliases.get(name, name)] = value
return result
def clean(self) -> None:
"""
Remove empty values
"""
for key, value in self.values.copy().items():
if value is None:
del self.values[key]
def as_json(self) -> str:
"""
Get object as JSON string
:return: JSON
:rtype: :obj:`str`
"""
return json.dumps(self.to_python())
@classmethod
def create(cls: Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
raise NotImplemented
def __str__(self) -> str:
"""
Return object as string. Alias for '.as_json()'
:return: str
"""
return self.as_json()
def __getitem__(self, item: typing.Union[str, int]) -> typing.Any:
"""
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: str, value: typing.Any) -> None:
"""
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: typing.Dict[str, typing.Any]) -> bool:
"""
Check key contains in that object
:param item:
:return:
"""
self.clean()
return item in self.values
def __iter__(self) -> typing.Iterator[str]:
"""
Iterate over items
:return:
"""
for item in self.to_python().items():
yield item
def iter_keys(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over keys
:return:
"""
for key, _ in self:
yield key
def iter_values(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over values
:return:
"""
for _, value in self:
yield value
def __hash__(self) -> int:
def _hash(obj)-> int:
buf: int = 0
if isinstance(obj, list):
for item in obj:
buf += _hash(item)
elif isinstance(obj, dict):
for dict_key, dict_value in obj.items():
buf += hash(dict_key) + _hash(dict_value)
else:
try:
buf += hash(obj)
except TypeError: # Skip unhashable objects
pass
return buf
result = 0
for key, value in sorted(self.values.items()):
result += hash(key) + _hash(value)
return result
def __eq__(self, other: TelegramObject) -> bool:
return isinstance(other, self.__class__) and hash(other) == hash(self)

View file

@ -1,11 +0,0 @@
from . import base
class CallbackGame(base.TelegramObject):
"""
A placeholder, currently holds no information. Use BotFather to set up your game.
https://core.telegram.org/bots/api#callbackgame
"""
pass

View file

@ -1,70 +0,0 @@
import typing
from . import base
from . import fields
from .message import Message
from .user import User
class CallbackQuery(base.TelegramObject):
"""
This object represents an incoming callback query from a callback button in an inline keyboard.
If the button that originated the query was attached to a message sent by the bot,
the field message will be present.
If the button was attached to a message sent via the bot (in inline mode),
the field inline_message_id will be present.
Exactly one of the fields data or game_short_name will be present.
https://core.telegram.org/bots/api#callbackquery
"""
id: base.String = fields.Field()
from_user: User = fields.Field(alias="from", base=User)
message: Message = fields.Field(base=Message)
inline_message_id: base.String = fields.Field()
chat_instance: base.String = fields.Field()
data: base.String = fields.Field()
game_short_name: base.String = fields.Field()
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.
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,
)
def __hash__(self):
return hash(self.id)

View file

@ -1,637 +0,0 @@
from __future__ import annotations
import asyncio
import datetime
import typing
from . import base
from . import fields
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from ..utils import helper
from ..utils import markdown
class Chat(base.TelegramObject):
"""
This object represents a chat.
https://core.telegram.org/bots/api#chat
"""
id: base.Integer = fields.Field()
type: base.String = fields.Field()
title: base.String = fields.Field()
username: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
all_members_are_administrators: base.Boolean = fields.Field()
photo: ChatPhoto = fields.Field(base=ChatPhoto)
description: base.String = fields.Field()
invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field()
def __hash__(self):
return self.id
@property
def full_name(self):
if self.type == ChatType.PRIVATE:
full_name = self.first_name
if self.last_name:
full_name += ' ' + self.last_name
return full_name
return self.title
@property
def mention(self):
"""
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
if self.type == ChatType.PRIVATE:
return self.full_name
return None
@property
def user_url(self):
if self.type != ChatType.PRIVATE:
raise TypeError('`user_url` property is only available in private chats!')
return f"tg://user?id={self.id}"
def get_mention(self, name=None, as_html=False):
if name is None:
name = self.mention
if as_html:
return markdown.hlink(name, self.user_url)
return markdown.link(name, self.user_url)
async def get_url(self):
"""
Use this method to get chat link.
Private chat returns user link.
Other chat types return either username link (if they are public) or invite link (if they are private).
:return: link
:rtype: :obj:`base.String`
"""
if self.type == ChatType.PRIVATE:
return f"tg://user?id={self.id}"
if self.username:
return f'https://t.me/{self.username}'
if self.invite_link:
return self.invite_link
await self.update_chat()
return self.invite_link
async def update_chat(self):
"""
User this method to update Chat data
:return: None
"""
other = await self.bot.get_chat(self.id)
for key, value in other:
self[key] = value
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, datetime.datetime, datetime.timedelta, 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,
permissions: typing.Optional[ChatPermissions] = None,
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, 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 permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
: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 await self.bot.restrict_chat_member(self.id, user_id=user_id,
permissions=permissions,
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 await self.bot.promote_chat_member(self.id,
user_id=user_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)
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.
Source: https://core.telegram.org/bots/api#exportchatinvitelink
:return: Returns exported invite link as String on success.
:rtype: :obj:`base.String`
"""
if not self.invite_link:
self.invite_link = await self.bot.export_chat_invite_link(self.id)
return self.invite_link
def __int__(self):
return self.id
class ChatType(helper.Helper):
"""
List of chat types
:key: PRIVATE
:key: GROUP
:key: SUPER_GROUP
:key: CHANNEL
"""
mode = helper.HelperMode.lowercase
PRIVATE = helper.Item() # private
GROUP = helper.Item() # group
SUPER_GROUP = helper.Item() # supergroup
CHANNEL = helper.Item() # channel
@staticmethod
def _check(obj, chat_types) -> bool:
if hasattr(obj, 'chat'):
obj = obj.chat
if not hasattr(obj, 'type'):
return False
return obj.type in chat_types
@classmethod
def is_private(cls, obj) -> bool:
"""
Check chat is private
:param obj:
:return:
"""
return cls._check(obj, [cls.PRIVATE])
@classmethod
def is_group(cls, obj) -> bool:
"""
Check chat is group
:param obj:
:return:
"""
return cls._check(obj, [cls.GROUP])
@classmethod
def is_super_group(cls, obj) -> bool:
"""
Check chat is super-group
:param obj:
:return:
"""
return cls._check(obj, [cls.SUPER_GROUP])
@classmethod
def is_group_or_super_group(cls, obj) -> bool:
"""
Check chat is group or super-group
:param obj:
:return:
"""
return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP])
@classmethod
def is_channel(cls, obj) -> bool:
"""
Check chat is channel
:param obj:
:return:
"""
return cls._check(obj, [cls.CHANNEL])
class ChatActions(helper.Helper):
"""
List of chat actions
:key: TYPING
:key: UPLOAD_PHOTO
:key: RECORD_VIDEO
:key: UPLOAD_VIDEO
:key: RECORD_AUDIO
:key: UPLOAD_AUDIO
:key: UPLOAD_DOCUMENT
:key: FIND_LOCATION
:key: RECORD_VIDEO_NOTE
:key: UPLOAD_VIDEO_NOTE
"""
mode = helper.HelperMode.snake_case
TYPING: str = helper.Item() # typing
UPLOAD_PHOTO: str = helper.Item() # upload_photo
RECORD_VIDEO: str = helper.Item() # record_video
UPLOAD_VIDEO: str = helper.Item() # upload_video
RECORD_AUDIO: str = helper.Item() # record_audio
UPLOAD_AUDIO: str = helper.Item() # upload_audio
UPLOAD_DOCUMENT: str = helper.Item() # upload_document
FIND_LOCATION: str = helper.Item() # find_location
RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note
UPLOAD_VIDEO_NOTE: str = helper.Item() # upload_video_note
@classmethod
async def _do(cls, action: str, sleep=None):
from aiogram import Bot
await Bot.get_current().send_chat_action(Chat.get_current().id, action)
if sleep:
await asyncio.sleep(sleep)
@classmethod
def calc_timeout(cls, text, timeout=.8):
"""
Calculate timeout for text
:param text:
:param timeout:
:return:
"""
return min((len(str(text)) * timeout, 5.0))
@classmethod
async def typing(cls, sleep=None):
"""
Do typing
:param sleep: sleep timeout
:return:
"""
if isinstance(sleep, str):
sleep = cls.calc_timeout(sleep)
await cls._do(cls.TYPING, sleep)
@classmethod
async def upload_photo(cls, sleep=None):
"""
Do upload_photo
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_PHOTO, sleep)
@classmethod
async def record_video(cls, sleep=None):
"""
Do record video
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.RECORD_VIDEO, sleep)
@classmethod
async def upload_video(cls, sleep=None):
"""
Do upload video
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_VIDEO, sleep)
@classmethod
async def record_audio(cls, sleep=None):
"""
Do record audio
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.RECORD_AUDIO, sleep)
@classmethod
async def upload_audio(cls, sleep=None):
"""
Do upload audio
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_AUDIO, sleep)
@classmethod
async def upload_document(cls, sleep=None):
"""
Do upload document
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_DOCUMENT, sleep)
@classmethod
async def find_location(cls, sleep=None):
"""
Do find location
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.FIND_LOCATION, sleep)
@classmethod
async def record_video_note(cls, sleep=None):
"""
Do record video note
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.RECORD_VIDEO_NOTE, sleep)
@classmethod
async def upload_video_note(cls, sleep=None):
"""
Do upload video note
:param sleep: sleep timeout
:return:
"""
await cls._do(cls.UPLOAD_VIDEO_NOTE, sleep)

View file

@ -1,65 +0,0 @@
import datetime
import warnings
from typing import Optional
from . import base
from . import fields
from .user import User
from ..utils import helper
class ChatMember(base.TelegramObject):
"""
This object contains information about one member of a chat.
https://core.telegram.org/bots/api#chatmember
"""
user: User = fields.Field(base=User)
status: base.String = fields.Field()
until_date: datetime.datetime = fields.DateTimeField()
can_be_edited: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
can_post_messages: base.Boolean = fields.Field()
can_edit_messages: base.Boolean = fields.Field()
can_delete_messages: base.Boolean = fields.Field()
can_invite_users: base.Boolean = fields.Field()
can_restrict_members: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()
can_promote_members: base.Boolean = fields.Field()
is_member: base.Boolean = fields.Field()
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
def is_chat_admin(self) -> bool:
return ChatMemberStatus.is_chat_admin(self.status)
def is_chat_member(self) -> bool:
return ChatMemberStatus.is_chat_member(self.status)
def __int__(self) -> int:
return self.user.id
class ChatMemberStatus(helper.Helper):
"""
Chat member status
"""
mode = helper.HelperMode.lowercase
CREATOR = helper.Item() # creator
ADMINISTRATOR = helper.Item() # administrator
MEMBER = helper.Item() # member
RESTRICTED = helper.Item() # restricted
LEFT = helper.Item() # left
KICKED = helper.Item() # kicked
@classmethod
def is_chat_admin(cls, role: str) -> bool:
return role in [cls.ADMINISTRATOR, cls.CREATOR]
@classmethod
def is_chat_member(cls, role: str) -> bool:
return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED]

View file

@ -1,39 +0,0 @@
from . import base
from . import fields
class ChatPermissions(base.TelegramObject):
"""
Describes actions that a non-administrator user is allowed to take in a chat.
https://core.telegram.org/bots/api#chatpermissions
"""
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_polls: base.Boolean = fields.Field()
can_send_other_messages: base.Boolean = fields.Field()
can_add_web_page_previews: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
can_invite_users: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()
def __init__(self,
can_send_messages: base.Boolean = None,
can_send_media_messages: base.Boolean = None,
can_send_polls: base.Boolean = None,
can_send_other_messages: base.Boolean = None,
can_add_web_page_previews: base.Boolean = None,
can_change_info: base.Boolean = None,
can_invite_users: base.Boolean = None,
can_pin_messages: base.Boolean = None,
**kwargs):
super(ChatPermissions, self).__init__(
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
)

View file

@ -1,93 +0,0 @@
import os
import pathlib
from . import base
from . import fields
class ChatPhoto(base.TelegramObject):
"""
This object represents a chat photo.
https://core.telegram.org/bots/api#chatphoto
"""
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)
def __hash__(self):
return hash(self.small_file_id) + hash(self.big_file_id)

View file

@ -1,23 +0,0 @@
from . import base
from . import fields
from .location import Location
from .user import User
class ChosenInlineResult(base.TelegramObject):
"""
Represents a result of an inline query that was chosen by the user and sent to their chat partner.
Note: It is necessary to enable inline feedback via @Botfather in order to receive these objects in updates.
Your bot can accept payments from Telegram users.
Please see the introduction to payments for more details on the process and how to set up payments for your bot.
Please note that users will need Telegram v.4.0 or higher to use payments (released on May 18, 2017).
https://core.telegram.org/bots/api#choseninlineresult
"""
result_id: base.String = fields.Field()
from_user: User = fields.Field(alias="from", base=User)
location: Location = fields.Field(base=Location)
inline_message_id: base.String = fields.Field()
query: base.String = fields.Field()

View file

@ -1,26 +0,0 @@
from . import base
from . import fields
class Contact(base.TelegramObject):
"""
This object represents a phone contact.
https://core.telegram.org/bots/api#contact
"""
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
user_id: base.Integer = fields.Field()
vcard: base.String = fields.Field()
@property
def full_name(self):
name = self.first_name
if self.last_name is not None:
name += " " + self.last_name
return name
def __hash__(self):
return hash(self.phone_number)

View file

@ -1,18 +0,0 @@
from . import base
from . import fields
from . import mixins
from .photo_size import PhotoSize
class Document(base.TelegramObject, mixins.Downloadable):
"""
This object represents a general file (as opposed to photos, voice messages and audio files).
https://core.telegram.org/bots/api#document
"""
file_id: base.String = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()

View file

@ -1,16 +0,0 @@
from . import base
from . import fields
class EncryptedCredentials(base.TelegramObject):
"""
Contains data required for decrypting and authenticating EncryptedPassportElement.
See the Telegram Passport Documentation for a complete description of the data decryption
and authentication processes.
https://core.telegram.org/bots/api#encryptedcredentials
"""
data: base.String = fields.Field()
hash: base.String = fields.Field()
secret: base.String = fields.Field()

View file

@ -1,22 +0,0 @@
import typing
from . import base
from . import fields
from .passport_file import PassportFile
class EncryptedPassportElement(base.TelegramObject):
"""
Contains information about documents or other Telegram Passport elements shared with the bot by the user.
https://core.telegram.org/bots/api#encryptedpassportelement
"""
type: base.String = fields.Field()
data: base.String = fields.Field()
phone_number: base.String = fields.Field()
email: base.String = fields.Field()
files: typing.List[PassportFile] = fields.ListField(base=PassportFile)
front_side: PassportFile = fields.Field(base=PassportFile)
reverse_side: PassportFile = fields.Field(base=PassportFile)
selfie: PassportFile = fields.Field(base=PassportFile)

View file

@ -1,201 +0,0 @@
import abc
import datetime
__all__ = ("BaseField", "Field", "ListField", "DateTimeField", "TextField", "ListOfLists")
class BaseField(metaclass=abc.ABCMeta):
"""
Base field (prop)
"""
def __init__(self, *, base=None, default=None, alias=None, on_change=None):
"""
Init prop
:param base: class for child element
:param default: default value
:param alias: alias name (for e.g. field 'from' has to be named 'from_user'
as 'from' is a builtin Python keyword
:param on_change: callback will be called when value is changed
"""
self.base_object = base
self.default = default
self.alias = alias
self.on_change = on_change
def __set_name__(self, owner, name):
if self.alias is None:
self.alias = name
def resolve_base(self, instance):
if self.base_object is None or hasattr(self.base_object, "telegram_types"):
return
elif isinstance(self.base_object, str):
self.base_object = instance.telegram_types.get(self.base_object)
def get_value(self, instance):
"""
Get value for the current object instance
:param instance:
:return:
"""
return instance.values.get(self.alias, self.default)
def set_value(self, instance, value, parent=None):
"""
Set prop value
:param instance:
:param value:
:param parent:
:return:
"""
self.resolve_base(instance)
value = self.deserialize(value, parent)
instance.values[self.alias] = value
self._trigger_changed(instance, value)
def _trigger_changed(self, instance, value):
if not self.on_change and instance is not None:
return
callback = getattr(instance, self.on_change)
callback(value)
def __get__(self, instance, owner):
return self.get_value(instance)
def __set__(self, instance, value):
self.set_value(instance, value)
@abc.abstractmethod
def serialize(self, value):
"""
Serialize value to python
:param value:
:return:
"""
pass
@abc.abstractmethod
def deserialize(self, value, parent=None):
"""Deserialize python object value to TelegramObject value"""
pass
def export(self, instance):
"""
Alias for `serialize` but for current Object instance
:param instance:
:return:
"""
return self.serialize(self.get_value(instance))
class Field(BaseField):
"""
Simple field
"""
def serialize(self, value):
if self.base_object is not None and hasattr(value, "to_python"):
return value.to_python()
return value
def deserialize(self, value, parent=None):
if (
isinstance(value, dict)
and self.base_object is not None
and not hasattr(value, "base_object")
and not hasattr(value, "to_python")
):
return self.base_object(conf={"parent": parent}, **value)
return value
class ListField(Field):
"""
Field contains list ob objects
"""
def __init__(self, *args, **kwargs):
default = kwargs.pop("default", None)
if default is None:
default = []
super(ListField, self).__init__(*args, default=default, **kwargs)
def serialize(self, value):
result = []
serialize = super(ListField, self).serialize
for item in value:
result.append(serialize(item))
return result
def deserialize(self, value, parent=None):
result = []
deserialize = super(ListField, self).deserialize
for item in value:
result.append(deserialize(item, parent=parent))
return result
class ListOfLists(Field):
def serialize(self, value):
result = []
serialize = super(ListOfLists, self).serialize
for row in value:
row_result = []
for item in row:
row_result.append(serialize(item))
result.append(row_result)
return result
def deserialize(self, value, parent=None):
result = []
deserialize = super(ListOfLists, self).deserialize
if hasattr(value, "__iter__"):
for row in value:
row_result = []
for item in row:
row_result.append(deserialize(item, parent=parent))
result.append(row_result)
return result
class DateTimeField(Field):
"""
In this field st_ored datetime
in: unixtime
out: datetime
"""
def serialize(self, value: datetime.datetime):
return round(value.timestamp())
def deserialize(self, value, parent=None):
return datetime.datetime.fromtimestamp(value)
class TextField(Field):
def __init__(self, *, prefix=None, suffix=None, default=None, alias=None):
super(TextField, self).__init__(default=default, alias=alias)
self.prefix = prefix
self.suffix = suffix
def serialize(self, value):
if value is None:
return value
if self.prefix:
value = self.prefix + value
if self.suffix:
value += self.suffix
return value
def deserialize(self, value, parent=None):
if value is not None and not isinstance(value, str):
raise TypeError(f"Field '{self.alias}' should be str not {type(value).__name__}")
return value

View file

@ -1,22 +0,0 @@
from . import base
from . import fields
from . import mixins
class File(base.TelegramObject, mixins.Downloadable):
"""
This object represents a file ready to be downloaded.
The file can be downloaded via the link https://api.telegram.org/file/bot<token>/<file_path>.
It is guaranteed that the link will be valid for at least 1 hour.
When the link expires, a new one can be requested by calling getFile.
Maximum file size to download is 20 MB
https://core.telegram.org/bots/api#file
"""
file_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
file_path: base.String = fields.Field()

View file

@ -1,37 +0,0 @@
import typing
from . import base
from . import fields
class ForceReply(base.TelegramObject):
"""
Upon receiving a message with this object,
Telegram clients will display a reply interface to the user
(act as if the user has selected the bots message and tapped Reply').
This can be extremely useful if you want to create user-friendly step-by-step
interfaces without having to sacrifice privacy mode.
Example: A poll bot for groups runs in privacy mode
(only receives commands, replies to its messages and mentions).
There could be two ways to create a new poll
The last option is definitely more attractive.
And if you use ForceReply in your bots questions, it will receive the users answers even
if it only receives replies, commands and mentions without any extra work for the user.
https://core.telegram.org/bots/api#forcereply
"""
force_reply: base.Boolean = fields.Field(default=True)
selective: base.Boolean = fields.Field()
@classmethod
def create(cls, selective: typing.Optional[base.Boolean] = None):
"""
Create new force reply
:param selective:
:return:
"""
return cls(selective=selective)

View file

@ -1,24 +0,0 @@
import typing
from . import base
from . import fields
from .animation import Animation
from .message_entity import MessageEntity
from .photo_size import PhotoSize
class Game(base.TelegramObject):
"""
This object represents a game.
Use BotFather to create and edit games, their short names will act as unique identifiers.
https://core.telegram.org/bots/api#game
"""
title: base.String = fields.Field()
description: base.String = fields.Field()
photo: typing.List[PhotoSize] = fields.ListField(base=PhotoSize)
text: base.String = fields.Field()
text_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity)
animation: Animation = fields.Field(base=Animation)

View file

@ -1,17 +0,0 @@
from . import base
from . import fields
from .user import User
class GameHighScore(base.TelegramObject):
"""
This object represents one row of the high scores table for a game.
And thats about all weve got for now.
If you've got any questions, please check out our Bot FAQ
https://core.telegram.org/bots/api#gamehighscore
"""
position: base.Integer = fields.Field()
user: User = fields.Field(base=User)
score: base.Integer = fields.Field()

View file

@ -1,127 +0,0 @@
import typing
from . import base
from . import fields
from .callback_game import CallbackGame
from .login_url import LoginUrl
class InlineKeyboardMarkup(base.TelegramObject):
"""
This object represents an inline keyboard that appears right next to the message it belongs to.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will display unsupported message.
https://core.telegram.org/bots/api#inlinekeyboardmarkup
"""
inline_keyboard: "typing.List[typing.List[InlineKeyboardButton]]" = fields.ListOfLists(
base="InlineKeyboardButton"
)
def __init__(self, row_width=3, inline_keyboard=None, **kwargs):
if inline_keyboard is None:
inline_keyboard = []
conf = kwargs.pop("conf", {}) or {}
conf["row_width"] = row_width
super(InlineKeyboardMarkup, self).__init__(
**kwargs, conf=conf, inline_keyboard=inline_keyboard
)
@property
def row_width(self):
return self.conf.get("row_width", 3)
@row_width.setter
def row_width(self, value):
self.conf["row_width"] = value
def add(self, *args):
"""
Add buttons
:param args:
:return: self
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
row = []
for index, button in enumerate(args, start=1):
row.append(button)
if index % self.row_width == 0:
self.inline_keyboard.append(row)
row = []
if len(row) > 0:
self.inline_keyboard.append(row)
return self
def row(self, *args):
"""
Add row
:param args:
:return: self
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
btn_array = []
for button in args:
btn_array.append(button)
self.inline_keyboard.append(btn_array)
return self
def insert(self, button):
"""
Insert button to last row
:param button:
:return: self
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
if self.inline_keyboard and len(self.inline_keyboard[-1]) < self.row_width:
self.inline_keyboard[-1].append(button)
else:
self.add(button)
return self
class InlineKeyboardButton(base.TelegramObject):
"""
This object represents one button of an inline keyboard. You must use exactly one of the optional fields.
https://core.telegram.org/bots/api#inlinekeyboardbutton
"""
text: base.String = fields.Field()
url: base.String = fields.Field()
login_url: LoginUrl = fields.Field(base=LoginUrl)
callback_data: base.String = fields.Field()
switch_inline_query: base.String = fields.Field()
switch_inline_query_current_chat: base.String = fields.Field()
callback_game: CallbackGame = fields.Field(base=CallbackGame)
pay: base.Boolean = fields.Field()
def __init__(
self,
text: base.String,
url: base.String = None,
login_url: LoginUrl = None,
callback_data: base.String = None,
switch_inline_query: base.String = None,
switch_inline_query_current_chat: base.String = None,
callback_game: CallbackGame = None,
pay: base.Boolean = None,
**kwargs,
):
super(InlineKeyboardButton, self).__init__(
text=text,
url=url,
login_url=login_url,
callback_data=callback_data,
switch_inline_query=switch_inline_query,
switch_inline_query_current_chat=switch_inline_query_current_chat,
callback_game=callback_game,
pay=pay,
**kwargs,
)

View file

@ -1,71 +0,0 @@
import typing
from . import base
from . import fields
from .inline_query_result import InlineQueryResult
from .location import Location
from .user import User
class InlineQuery(base.TelegramObject):
"""
This object represents an incoming inline query.
When the user sends an empty query, your bot could return some default or trending results.
https://core.telegram.org/bots/api#inlinequery
"""
id: base.String = fields.Field()
from_user: User = fields.Field(alias="from", base=User)
location: Location = fields.Field(base=Location)
query: base.String = fields.Field()
offset: base.String = fields.Field()
async def answer(
self,
results: typing.List[InlineQueryResult],
cache_time: typing.Union[base.Integer, None] = None,
is_personal: typing.Union[base.Boolean, None] = None,
next_offset: typing.Union[base.String, None] = None,
switch_pm_text: typing.Union[base.String, None] = None,
switch_pm_parameter: typing.Union[base.String, None] = None,
):
"""
Use this method to send answers to an inline query.
No more than 50 results per query are allowed.
Source: https://core.telegram.org/bots/api#answerinlinequery
:param results: A JSON-serialized array of results for the inline query
:type results: :obj:`typing.List[types.InlineQueryResult]`
:param cache_time: The maximum amount of time in seconds that the result of the
inline query may be cached on the server. Defaults to 300.
:type cache_time: :obj:`typing.Union[base.Integer, None]`
:param is_personal: Pass True, if results may be cached on the server side only
for the user that sent the query. By default, results may be returned to any user who sends the same query
:type is_personal: :obj:`typing.Union[base.Boolean, None]`
:param next_offset: Pass the offset that a client should send in the
next query with the same text to receive more results.
Pass an empty string if there are no more results or if you dont support pagination.
Offset length cant exceed 64 bytes.
:type next_offset: :obj:`typing.Union[base.String, None]`
:param switch_pm_text: If passed, clients will display a button with specified text that
switches the user to a private chat with the bot and sends the bot a start message
with the parameter switch_pm_parameter
:type switch_pm_text: :obj:`typing.Union[base.String, None]`
:param switch_pm_parameter: Deep-linking parameter for the /start message sent to the bot when
user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed.
:type switch_pm_parameter: :obj:`typing.Union[base.String, None]`
:return: On success, True is returned
:rtype: :obj:`base.Boolean`
"""
return await self.bot.answer_inline_query(
self.id,
results=results,
cache_time=cache_time,
is_personal=is_personal,
next_offset=next_offset,
switch_pm_text=switch_pm_text,
switch_pm_parameter=switch_pm_parameter,
)

View file

@ -1,939 +0,0 @@
import typing
from . import base
from . import fields
from .inline_keyboard import InlineKeyboardMarkup
from .input_message_content import InputMessageContent
class InlineQueryResult(base.TelegramObject):
"""
This object represents one result of an inline query.
Telegram clients currently support results of the following 20 types
https://core.telegram.org/bots/api#inlinequeryresult
"""
id: base.String = fields.Field()
reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup)
def safe_get_parse_mode(self):
try:
return self.bot.parse_mode
except RuntimeError:
pass
def __init__(self, **kwargs):
if "parse_mode" in kwargs and kwargs["parse_mode"] is None:
kwargs["parse_mode"] = self.safe_get_parse_mode()
super(InlineQueryResult, self).__init__(**kwargs)
class InlineQueryResultArticle(InlineQueryResult):
"""
Represents a link to an article or web page.
https://core.telegram.org/bots/api#inlinequeryresultarticle
"""
type: base.String = fields.Field(alias="type", default="article")
title: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
url: base.String = fields.Field()
hide_url: base.Boolean = fields.Field()
description: base.String = fields.Field()
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
def __init__(
self,
*,
id: base.String,
title: base.String,
input_message_content: InputMessageContent,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
url: typing.Optional[base.String] = None,
hide_url: typing.Optional[base.Boolean] = None,
description: typing.Optional[base.String] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
):
super(InlineQueryResultArticle, self).__init__(
id=id,
title=title,
input_message_content=input_message_content,
reply_markup=reply_markup,
url=url,
hide_url=hide_url,
description=description,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height,
)
class InlineQueryResultPhoto(InlineQueryResult):
"""
Represents a link to a photo.
By default, this photo will be sent by the user with optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the photo.
https://core.telegram.org/bots/api#inlinequeryresultphoto
"""
type: base.String = fields.Field(alias="type", default="photo")
photo_url: base.String = fields.Field()
thumb_url: base.String = fields.Field()
photo_width: base.Integer = fields.Field()
photo_height: base.Integer = fields.Field()
title: base.String = fields.Field()
description: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
photo_url: base.String,
thumb_url: base.String,
photo_width: typing.Optional[base.Integer] = None,
photo_height: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultPhoto, self).__init__(
id=id,
photo_url=photo_url,
thumb_url=thumb_url,
photo_width=photo_width,
photo_height=photo_height,
title=title,
description=description,
caption=caption,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultGif(InlineQueryResult):
"""
Represents a link to an animated GIF file.
By default, this animated GIF file will be sent by the user with optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the animation.
https://core.telegram.org/bots/api#inlinequeryresultgif
"""
type: base.String = fields.Field(alias="type", default="gif")
gif_url: base.String = fields.Field()
gif_width: base.Integer = fields.Field()
gif_height: base.Integer = fields.Field()
gif_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
gif_url: base.String,
gif_width: typing.Optional[base.Integer] = None,
gif_height: typing.Optional[base.Integer] = None,
gif_duration: typing.Optional[base.Integer] = None,
thumb_url: typing.Optional[base.String] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultGif, self).__init__(
id=id,
gif_url=gif_url,
gif_width=gif_width,
gif_height=gif_height,
gif_duration=gif_duration,
thumb_url=thumb_url,
title=title,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultMpeg4Gif(InlineQueryResult):
"""
Represents a link to a video animation (H.264/MPEG-4 AVC video without sound).
By default, this animated MPEG-4 file will be sent by the user with optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the animation.
https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif
"""
type: base.String = fields.Field(alias="type", default="mpeg4_gif")
mpeg4_url: base.String = fields.Field()
mpeg4_width: base.Integer = fields.Field()
mpeg4_height: base.Integer = fields.Field()
mpeg4_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
mpeg4_url: base.String,
thumb_url: base.String,
mpeg4_width: typing.Optional[base.Integer] = None,
mpeg4_height: typing.Optional[base.Integer] = None,
mpeg4_duration: typing.Optional[base.Integer] = None,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultMpeg4Gif, self).__init__(
id=id,
mpeg4_url=mpeg4_url,
mpeg4_width=mpeg4_width,
mpeg4_height=mpeg4_height,
mpeg4_duration=mpeg4_duration,
thumb_url=thumb_url,
title=title,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultVideo(InlineQueryResult):
"""
Represents a link to a page containing an embedded video player or a video file.
By default, this video file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the video.
If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube),
you must replace its content using input_message_content.
https://core.telegram.org/bots/api#inlinequeryresultvideo
"""
type: base.String = fields.Field(alias="type", default="video")
video_url: base.String = fields.Field()
mime_type: base.String = fields.Field()
thumb_url: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
video_width: base.Integer = fields.Field()
video_height: base.Integer = fields.Field()
video_duration: base.Integer = fields.Field()
description: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
video_url: base.String,
mime_type: base.String,
thumb_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
video_width: typing.Optional[base.Integer] = None,
video_height: typing.Optional[base.Integer] = None,
video_duration: typing.Optional[base.Integer] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultVideo, self).__init__(
id=id,
video_url=video_url,
mime_type=mime_type,
thumb_url=thumb_url,
title=title,
caption=caption,
video_width=video_width,
video_height=video_height,
video_duration=video_duration,
description=description,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultAudio(InlineQueryResult):
"""
Represents a link to an mp3 audio file. By default, this audio file will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the audio.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultaudio
"""
type: base.String = fields.Field(alias="type", default="audio")
audio_url: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
performer: base.String = fields.Field()
audio_duration: base.Integer = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
audio_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
performer: typing.Optional[base.String] = None,
audio_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultAudio, self).__init__(
id=id,
audio_url=audio_url,
title=title,
caption=caption,
parse_mode=parse_mode,
performer=performer,
audio_duration=audio_duration,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultVoice(InlineQueryResult):
"""
Represents a link to a voice recording in an .ogg container encoded with OPUS.
By default, this voice recording will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the the voice message.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultvoice
"""
type: base.String = fields.Field(alias="type", default="voice")
voice_url: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
voice_duration: base.Integer = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
voice_url: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
voice_duration: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultVoice, self).__init__(
id=id,
voice_url=voice_url,
title=title,
caption=caption,
voice_duration=voice_duration,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultDocument(InlineQueryResult):
"""
Represents a link to a file.
By default, this file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the file. Currently, only .PDF and .ZIP files can be sent using this method.
Note: This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultdocument
"""
type: base.String = fields.Field(alias="type", default="document")
title: base.String = fields.Field()
caption: base.String = fields.Field()
document_url: base.String = fields.Field()
mime_type: base.String = fields.Field()
description: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
def __init__(
self,
*,
id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
document_url: typing.Optional[base.String] = None,
mime_type: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
):
super(InlineQueryResultDocument, self).__init__(
id=id,
title=title,
caption=caption,
document_url=document_url,
mime_type=mime_type,
description=description,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height,
parse_mode=parse_mode,
)
class InlineQueryResultLocation(InlineQueryResult):
"""
Represents a location on a map.
By default, the location will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the location.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultlocation
"""
type: base.String = fields.Field(alias="type", default="location")
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
title: base.String = fields.Field()
live_period: base.Integer = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
def __init__(
self,
*,
id: base.String,
latitude: base.Float,
longitude: base.Float,
title: base.String,
live_period: typing.Optional[base.Integer] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
):
super(InlineQueryResultLocation, self).__init__(
id=id,
latitude=latitude,
longitude=longitude,
title=title,
live_period=live_period,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height,
)
class InlineQueryResultVenue(InlineQueryResult):
"""
Represents a venue. By default, the venue will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the venue.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultvenue
"""
type: base.String = fields.Field(alias="type", default="venue")
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
title: base.String = fields.Field()
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
foursquare_type: base.String = fields.Field()
def __init__(
self,
*,
id: base.String,
latitude: base.Float,
longitude: base.Float,
title: base.String,
address: base.String,
foursquare_id: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
foursquare_type: typing.Optional[base.String] = None,
):
super(InlineQueryResultVenue, self).__init__(
id=id,
latitude=latitude,
longitude=longitude,
title=title,
address=address,
foursquare_id=foursquare_id,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height,
foursquare_type=foursquare_type,
)
class InlineQueryResultContact(InlineQueryResult):
"""
Represents a contact with a phone number.
By default, this contact will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the contact.
Note: This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultcontact
"""
type: base.String = fields.Field(alias="type", default="contact")
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
vcard: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
foursquare_type: base.String = fields.Field()
def __init__(
self,
*,
id: base.String,
phone_number: base.String,
first_name: base.String,
last_name: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None,
foursquare_type: typing.Optional[base.String] = None,
):
super(InlineQueryResultContact, self).__init__(
id=id,
phone_number=phone_number,
first_name=first_name,
last_name=last_name,
reply_markup=reply_markup,
input_message_content=input_message_content,
thumb_url=thumb_url,
thumb_width=thumb_width,
thumb_height=thumb_height,
foursquare_type=foursquare_type,
)
class InlineQueryResultGame(InlineQueryResult):
"""
Represents a Game.
Note: This will only work in Telegram versions released after October 1, 2016.
Older clients will not display any inline results if a game result is among them.
https://core.telegram.org/bots/api#inlinequeryresultgame
"""
type: base.String = fields.Field(alias="type", default="game")
game_short_name: base.String = fields.Field()
def __init__(
self,
*,
id: base.String,
game_short_name: base.String,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
):
super(InlineQueryResultGame, self).__init__(
id=id, game_short_name=game_short_name, reply_markup=reply_markup
)
class InlineQueryResultCachedPhoto(InlineQueryResult):
"""
Represents a link to a photo stored on the Telegram servers.
By default, this photo will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the photo.
https://core.telegram.org/bots/api#inlinequeryresultcachedphoto
"""
type: base.String = fields.Field(alias="type", default="photo")
photo_file_id: base.String = fields.Field()
title: base.String = fields.Field()
description: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
photo_file_id: base.String,
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedPhoto, self).__init__(
id=id,
photo_file_id=photo_file_id,
title=title,
description=description,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedGif(InlineQueryResult):
"""
Represents a link to an animated GIF file stored on the Telegram servers.
By default, this animated GIF file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with specified content
instead of the animation.
https://core.telegram.org/bots/api#inlinequeryresultcachedgif
"""
type: base.String = fields.Field(alias="type", default="gif")
gif_file_id: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
gif_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedGif, self).__init__(
id=id,
gif_file_id=gif_file_id,
title=title,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
"""
Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers.
By default, this animated MPEG-4 file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the animation.
https://core.telegram.org/bots/api#inlinequeryresultcachedmpeg4gif
"""
type: base.String = fields.Field(alias="type", default="mpeg4_gif")
mpeg4_file_id: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
mpeg4_file_id: base.String,
title: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedMpeg4Gif, self).__init__(
id=id,
mpeg4_file_id=mpeg4_file_id,
title=title,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedSticker(InlineQueryResult):
"""
Represents a link to a sticker stored on the Telegram servers.
By default, this sticker will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the sticker.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultcachedsticker
"""
type: base.String = fields.Field(alias="type", default="sticker")
sticker_file_id: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
sticker_file_id: base.String,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedSticker, self).__init__(
id=id,
sticker_file_id=sticker_file_id,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedDocument(InlineQueryResult):
"""
Represents a link to a file stored on the Telegram servers.
By default, this file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the file.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultcacheddocument
"""
type: base.String = fields.Field(alias="type", default="document")
title: base.String = fields.Field()
document_file_id: base.String = fields.Field()
description: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
title: base.String,
document_file_id: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedDocument, self).__init__(
id=id,
title=title,
document_file_id=document_file_id,
description=description,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedVideo(InlineQueryResult):
"""
Represents a link to a video file stored on the Telegram servers.
By default, this video file will be sent by the user with an optional caption.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the video.
https://core.telegram.org/bots/api#inlinequeryresultcachedvideo
"""
type: base.String = fields.Field(alias="type", default="video")
video_file_id: base.String = fields.Field()
title: base.String = fields.Field()
description: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
video_file_id: base.String,
title: base.String,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedVideo, self).__init__(
id=id,
video_file_id=video_file_id,
title=title,
description=description,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedVoice(InlineQueryResult):
"""
Represents a link to a voice message stored on the Telegram servers.
By default, this voice message will be sent by the user.
Alternatively, you can use input_message_content to send a message with the specified content
instead of the voice message.
Note: This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultcachedvoice
"""
type: base.String = fields.Field(alias="type", default="voice")
voice_file_id: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
voice_file_id: base.String,
title: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedVoice, self).__init__(
id=id,
voice_file_id=voice_file_id,
title=title,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)
class InlineQueryResultCachedAudio(InlineQueryResult):
"""
Represents a link to an mp3 audio file stored on the Telegram servers.
By default, this audio file will be sent by the user.
Alternatively, you can use input_message_content to send a message with
the specified content instead of the audio.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inlinequeryresultcachedaudio
"""
type: base.String = fields.Field(alias="type", default="audio")
audio_file_id: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
def __init__(
self,
*,
id: base.String,
audio_file_id: base.String,
caption: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None,
):
super(InlineQueryResultCachedAudio, self).__init__(
id=id,
audio_file_id=audio_file_id,
caption=caption,
parse_mode=parse_mode,
reply_markup=reply_markup,
input_message_content=input_message_content,
)

View file

@ -1,219 +0,0 @@
import asyncio
import inspect
import io
import logging
import os
import secrets
import aiohttp
from . import base
from ..bot import api
CHUNK_SIZE = 65536
log = logging.getLogger("aiogram")
class InputFile(base.TelegramObject):
"""
This object represents the contents of a file to be uploaded.
Must be posted using multipart/form-data in the usual way that files are uploaded via the browser.
Also that is not typical TelegramObject!
https://core.telegram.org/bots/api#inputfile
"""
def __init__(self, path_or_bytesio, filename=None, conf=None):
"""
:param path_or_bytesio:
:param filename:
:param conf:
"""
super(InputFile, self).__init__(conf=conf)
if isinstance(path_or_bytesio, str):
# As path
self._file = open(path_or_bytesio, "rb")
self._path = path_or_bytesio
if filename is None:
filename = os.path.split(path_or_bytesio)[-1]
elif isinstance(path_or_bytesio, io.IOBase):
self._path = None
self._file = path_or_bytesio
elif isinstance(path_or_bytesio, _WebPipe):
self._path = None
self._file = path_or_bytesio
else:
raise TypeError("Not supported file type.")
self._filename = filename
self.attachment_key = secrets.token_urlsafe(16)
def __del__(self):
"""
Close file descriptor
"""
if not hasattr(self, "_file"):
return
if inspect.iscoroutinefunction(self._file.close):
return asyncio.ensure_future(self._file.close())
self._file.close()
@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
@property
def attach(self):
return f"attach://{self.attachment_key}"
def get_filename(self) -> str:
"""
Get file name
:return: name
"""
return self.filename
@property
def file(self):
return self._file
def get_file(self):
"""
Get file object
:return:
"""
return self.file
@classmethod
def from_url(cls, url, filename=None, chunk_size=CHUNK_SIZE):
"""
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 chunk_size:
:return: InputFile
"""
pipe = _WebPipe(url, chunk_size=chunk_size)
if filename is None:
filename = pipe.name
return cls(pipe, filename, chunk_size)
def save(self, filename, chunk_size=CHUNK_SIZE):
"""
Write file to disk
:param filename:
:param chunk_size:
"""
with open(filename, "wb") as fp:
while True:
# Chunk writer
data = self.file.read(chunk_size)
if not data:
break
fp.write(data)
# Flush all data
fp.flush()
# Go to start of file.
if self.file.seekable():
self.file.seek(0)
def __str__(self):
return f"<InputFile 'attach://{self.attachment_key}' with file='{self.file}'>"
__repr__ = __str__
def to_python(self):
raise TypeError("Object of this type is not exportable!")
@classmethod
def to_object(cls, data):
raise TypeError("Object of this type is not importable!")
class _WebPipe:
def __init__(self, url, chunk_size=-1):
self.url = url
self.chunk_size = chunk_size
self._session: aiohttp.ClientSession = None
self._response: aiohttp.ClientResponse = None
self._reader = None
self._name = None
self._lock = asyncio.Lock()
@property
def name(self):
if not self._name:
*_, part = self.url.rpartition("/")
if part:
self._name = part
else:
self._name = secrets.token_urlsafe(24)
return self._name
async def open(self):
session = self._session = aiohttp.ClientSession()
self._response = await session.get(self.url) # type: aiohttp.ClientResponse
await self._lock.acquire()
return self
async def close(self):
if self._response and not self._response.closed:
await self._response.close()
if self._session and not self._session.closed:
await self._session.close()
if self._lock.locked():
self._lock.release()
@property
def closed(self):
return not self._session or self._session.closed
def __aiter__(self):
return self
async def __anext__(self):
if self.closed:
await self.open()
chunk = await self.read(self.chunk_size)
if not chunk:
await self.close()
raise StopAsyncIteration
return chunk
async def read(self, chunk_size=-1):
if not self._response:
raise LookupError("I/O operation on closed stream")
response: aiohttp.ClientResponse = self._response
reader: aiohttp.StreamReader = response.content
return await reader.read(chunk_size)
def __str__(self):
result = f"WebPipe url='{self.url}', name='{self.name}'"
return "<" + result + ">"
__repr__ = __str__

View file

@ -1,364 +0,0 @@
import io
import secrets
import typing
from . import base
from . import fields
from .input_file import InputFile
ATTACHMENT_PREFIX = 'attach://'
class InputMedia(base.TelegramObject):
"""
This object represents the content of a media message to be sent. It should be one of
- InputMediaAnimation
- InputMediaDocument
- InputMediaAudio
- InputMediaPhoto
- InputMediaVideo
That is only base class.
https://core.telegram.org/bots/api#inputmedia
"""
type: base.String = fields.Field(default='photo')
media: base.String = fields.Field(alias='media', on_change='_media_changed')
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
caption: base.String = fields.Field()
parse_mode: base.String = fields.Field()
def __init__(self, *args, **kwargs):
self._thumb_file = None
self._media_file = None
media = kwargs.pop('media', None)
if isinstance(media, (io.IOBase, InputFile)):
self.file = media
elif media is not None:
self.media = media
thumb = kwargs.pop('thumb', None)
if isinstance(thumb, (io.IOBase, InputFile)):
self.thumb_file = thumb
elif thumb is not None:
self.thumb = thumb
super(InputMedia, self).__init__(*args, **kwargs)
try:
if self.parse_mode is None and self.bot and self.bot.parse_mode:
self.parse_mode = self.bot.parse_mode
except RuntimeError:
pass
@property
def file(self):
return self._media_file
@file.setter
def file(self, file: io.IOBase):
self.media = 'attach://' + secrets.token_urlsafe(16)
self._media_file = file
@file.deleter
def file(self):
self.media = None
self._media_file = None
def _media_changed(self, value):
if value is None or isinstance(value, str) and not value.startswith('attach://'):
self._media_file = None
@property
def thumb_file(self):
return self._thumb_file
@thumb_file.setter
def thumb_file(self, file: io.IOBase):
self.thumb = 'attach://' + secrets.token_urlsafe(16)
self._thumb_file = file
@thumb_file.deleter
def thumb_file(self):
self.thumb = None
self._thumb_file = None
def _thumb_changed(self, value):
if value is None or isinstance(value, str) and not value.startswith('attach://'):
self._thumb_file = None
def get_files(self):
if self._media_file:
yield self.media[9:], self._media_file
if self._thumb_file:
yield self.thumb[9:], self._thumb_file
class InputMediaAnimation(InputMedia):
"""
Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
https://core.telegram.org/bots/api#inputmediaanimation
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.String = None, **kwargs):
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, conf=kwargs)
class InputMediaDocument(InputMedia):
"""
Represents a photo to be sent.
https://core.telegram.org/bots/api#inputmediadocument
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
class InputMediaAudio(InputMedia):
"""
Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
https://core.telegram.org/bots/api#inputmediaanimation
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.String = None, **kwargs):
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode, conf=kwargs)
class InputMediaPhoto(InputMedia):
"""
Represents a photo to be sent.
https://core.telegram.org/bots/api#inputmediaphoto
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.String = None, **kwargs):
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
class InputMediaVideo(InputMedia):
"""
Represents a video to be sent.
https://core.telegram.org/bots/api#inputmediavideo
"""
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,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.String = None,
supports_streaming: base.Boolean = None, **kwargs):
super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode,
supports_streaming=supports_streaming, conf=kwargs)
class MediaGroup(base.TelegramObject):
"""
Helper for sending media group
"""
def __init__(self, medias: typing.Optional[typing.List[typing.Union[InputMedia, typing.Dict]]] = None):
super(MediaGroup, self).__init__()
self.media = []
if medias:
self.attach_many(*medias)
def attach_many(self, *medias: typing.Union[InputMedia, typing.Dict]):
"""
Attach list of media
:param medias:
"""
for media in medias:
self.attach(media)
def attach(self, media: typing.Union[InputMedia, typing.Dict]):
"""
Attach media
:param media:
"""
if isinstance(media, dict):
if 'type' not in media:
raise ValueError(f"Invalid media!")
media_type = media['type']
if media_type == 'photo':
media = InputMediaPhoto(**media)
elif media_type == 'video':
media = InputMediaVideo(**media)
# elif media_type == 'document':
# media = InputMediaDocument(**media)
# elif media_type == 'audio':
# media = InputMediaAudio(**media)
# elif media_type == 'animation':
# media = InputMediaAnimation(**media)
else:
raise TypeError(f"Invalid media type '{media_type}'!")
elif not isinstance(media, InputMedia):
raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}")
elif media.type in ['document', 'audio', 'animation']:
raise ValueError(f"This type of media is not supported by media groups!")
self.media.append(media)
'''
def attach_animation(self, animation: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.Boolean = None):
"""
Attach animation
:param animation:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param parse_mode:
"""
if not isinstance(animation, InputMedia):
animation = InputMediaAnimation(media=animation, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode)
self.attach(animation)
def attach_audio(self, audio: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.String = None):
"""
Attach animation
:param audio:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param performer:
:param title:
:param parse_mode:
"""
if not isinstance(audio, InputMedia):
audio = InputMediaAudio(media=audio, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode)
self.attach(audio)
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.String = None):
"""
Attach document
:param parse_mode:
:param caption:
:param thumb:
:param document:
"""
if not isinstance(document, InputMedia):
document = InputMediaDocument(media=document, thumb=thumb, caption=caption, parse_mode=parse_mode)
self.attach(document)
'''
def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile],
caption: base.String = None):
"""
Attach photo
:param photo:
:param caption:
"""
if not isinstance(photo, InputMedia):
photo = InputMediaPhoto(media=photo, caption=caption)
self.attach(photo)
def attach_video(self, video: typing.Union[InputMediaVideo, base.InputFile],
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None):
"""
Attach video
:param video:
:param caption:
:param width:
:param height:
:param duration:
"""
if not isinstance(video, InputMedia):
video = InputMediaVideo(media=video, thumb=thumb, caption=caption,
width=width, height=height, duration=duration)
self.attach(video)
def to_python(self) -> typing.List:
"""
Get object as JSON serializable
:return:
"""
self.clean()
result = []
for obj in self.media:
if isinstance(obj, base.TelegramObject):
obj = obj.to_python()
result.append(obj)
return result
def get_files(self):
for inputmedia in self.media:
if not isinstance(inputmedia, InputMedia) or not inputmedia.file:
continue
yield from inputmedia.get_files()

View file

@ -1,125 +0,0 @@
import typing
from . import base
from . import fields
class InputMessageContent(base.TelegramObject):
"""
This object represents the content of a message to be sent as a result of an inline query.
Telegram clients currently support the following 4 types
https://core.telegram.org/bots/api#inputmessagecontent
"""
pass
class InputContactMessageContent(InputMessageContent):
"""
Represents the content of a contact message to be sent as the result of an inline query.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inputcontactmessagecontent
"""
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
vcard: base.String = fields.Field()
def __init__(
self,
phone_number: base.String,
first_name: typing.Optional[base.String] = None,
last_name: typing.Optional[base.String] = None,
):
super(InputContactMessageContent, self).__init__(
phone_number=phone_number, first_name=first_name, last_name=last_name
)
class InputLocationMessageContent(InputMessageContent):
"""
Represents the content of a location message to be sent as the result of an inline query.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inputlocationmessagecontent
"""
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
def __init__(self, latitude: base.Float, longitude: base.Float):
super(InputLocationMessageContent, self).__init__(latitude=latitude, longitude=longitude)
class InputTextMessageContent(InputMessageContent):
"""
Represents the content of a text message to be sent as the result of an inline query.
https://core.telegram.org/bots/api#inputtextmessagecontent
"""
message_text: base.String = fields.Field()
parse_mode: base.String = fields.Field()
disable_web_page_preview: base.Boolean = fields.Field()
def safe_get_parse_mode(self):
try:
return self.bot.parse_mode
except RuntimeError:
pass
def __init__(
self,
message_text: typing.Optional[base.String] = None,
parse_mode: typing.Optional[base.String] = None,
disable_web_page_preview: typing.Optional[base.Boolean] = None,
):
if parse_mode is None:
parse_mode = self.safe_get_parse_mode()
super(InputTextMessageContent, self).__init__(
message_text=message_text,
parse_mode=parse_mode,
disable_web_page_preview=disable_web_page_preview,
)
class InputVenueMessageContent(InputMessageContent):
"""
Represents the content of a venue message to be sent as the result of an inline query.
Note: This will only work in Telegram versions released after 9 April, 2016.
Older clients will ignore them.
https://core.telegram.org/bots/api#inputvenuemessagecontent
"""
latitude: base.Float = fields.Field()
longitude: base.Float = fields.Field()
title: base.String = fields.Field()
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
def __init__(
self,
latitude: typing.Optional[base.Float] = None,
longitude: typing.Optional[base.Float] = None,
title: typing.Optional[base.String] = None,
address: typing.Optional[base.String] = None,
foursquare_id: typing.Optional[base.String] = None,
):
super(InputVenueMessageContent, self).__init__(
latitude=latitude,
longitude=longitude,
title=title,
address=address,
foursquare_id=foursquare_id,
)

View file

@ -1,16 +0,0 @@
from . import base
from . import fields
class Invoice(base.TelegramObject):
"""
This object contains basic information about an invoice.
https://core.telegram.org/bots/api#invoice
"""
title: base.String = fields.Field()
description: base.String = fields.Field()
start_parameter: base.String = fields.Field()
currency: base.String = fields.Field()
total_amount: base.Integer = fields.Field()

View file

@ -1,16 +0,0 @@
from . import base
from . import fields
class LabeledPrice(base.TelegramObject):
"""
This object represents a portion of the price for goods or services.
https://core.telegram.org/bots/api#labeledprice
"""
label: base.String = fields.Field()
amount: base.Integer = fields.Field()
def __init__(self, label: base.String, amount: base.Integer):
super(LabeledPrice, self).__init__(label=label, amount=amount)

View file

@ -1,13 +0,0 @@
from . import base
from . import fields
class Location(base.TelegramObject):
"""
This object represents a point on the map.
https://core.telegram.org/bots/api#location
"""
longitude: base.Float = fields.Field()
latitude: base.Float = fields.Field()

View file

@ -1,33 +0,0 @@
from . import base
from . import fields
class LoginUrl(base.TelegramObject):
"""
This object represents a parameter of the inline keyboard button used to automatically authorize a user.
Serves as a great replacement for the Telegram Login Widget when the user is coming from Telegram.
All the user needs to do is tap/click a button and confirm that they want to log in.
https://core.telegram.org/bots/api#loginurl
"""
url: base.String = fields.Field()
forward_text: base.String = fields.Field()
bot_username: base.String = fields.Field()
request_write_access: base.Boolean = fields.Field()
def __init__(
self,
url: base.String,
forward_text: base.String = None,
bot_username: base.String = None,
request_write_access: base.Boolean = None,
**kwargs,
):
super(LoginUrl, self).__init__(
url=url,
forward_text=forward_text,
bot_username=bot_username,
request_write_access=request_write_access,
**kwargs,
)

View file

@ -1,15 +0,0 @@
from . import base
from . import fields
class MaskPosition(base.TelegramObject):
"""
This object describes the position on faces where a mask should be placed by default.
https://core.telegram.org/bots/api#maskposition
"""
point: base.String = fields.Field()
x_shift: base.Float = fields.Field()
y_shift: base.Float = fields.Field()
scale: base.Float = fields.Field()

File diff suppressed because it is too large Load diff

View file

@ -1,109 +0,0 @@
import sys
from . import base
from . import fields
from .user import User
from ..utils import helper, markdown
class MessageEntity(base.TelegramObject):
"""
This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc.
https://core.telegram.org/bots/api#messageentity
"""
type: base.String = fields.Field()
offset: base.Integer = fields.Field()
length: base.Integer = fields.Field()
url: base.String = fields.Field()
user: User = fields.Field(base=User)
def get_text(self, text):
"""
Get value of entity
:param text: full text
:return: part of text
"""
if sys.maxunicode == 0xFFFF:
return text[self.offset : self.offset + self.length]
if not isinstance(text, bytes):
entity_text = text.encode("utf-16-le")
else:
entity_text = text
entity_text = entity_text[self.offset * 2 : (self.offset + self.length) * 2]
return entity_text.decode("utf-16-le")
def parse(self, text, as_html=True):
"""
Get entity value with markup
:param text: original text
:param as_html: as html?
:return: entity text with markup
"""
if not text:
return text
entity_text = self.get_text(text)
if self.type == MessageEntityType.BOLD:
method = markdown.hbold if as_html else markdown.bold
return method(entity_text)
if self.type == MessageEntityType.ITALIC:
method = markdown.hitalic if as_html else markdown.italic
return method(entity_text)
if self.type == MessageEntityType.PRE:
method = markdown.hpre if as_html else markdown.pre
return method(entity_text)
if self.type == MessageEntityType.CODE:
method = markdown.hcode if as_html else markdown.code
return method(entity_text)
if self.type == MessageEntityType.URL:
method = markdown.hlink if as_html else markdown.link
return method(entity_text, entity_text)
if self.type == MessageEntityType.TEXT_LINK:
method = markdown.hlink if as_html else markdown.link
return method(entity_text, self.url)
if self.type == MessageEntityType.TEXT_MENTION and self.user:
return self.user.get_mention(entity_text, as_html=as_html)
return entity_text
class MessageEntityType(helper.Helper):
"""
List of entity types
:key: MENTION
:key: HASHTAG
:key: CASHTAG
:key: BOT_COMMAND
:key: URL
:key: EMAIL
:key: PHONE_NUMBER
:key: BOLD
:key: ITALIC
:key: CODE
:key: PRE
:key: TEXT_LINK
:key: TEXT_MENTION
"""
mode = helper.HelperMode.snake_case
MENTION = helper.Item() # mention - @username
HASHTAG = helper.Item() # hashtag
CASHTAG = helper.Item() # cashtag
BOT_COMMAND = helper.Item() # bot_command
URL = helper.Item() # url
EMAIL = helper.Item() # email
PHONE_NUMBER = helper.Item() # phone_number
BOLD = helper.Item() # bold - bold text
ITALIC = helper.Item() # italic - italic text
CODE = helper.Item() # code - monowidth string
PRE = helper.Item() # pre - monowidth block
TEXT_LINK = helper.Item() # text_link - for clickable text URLs
TEXT_MENTION = helper.Item() # text_mention - for users without usernames

View file

@ -1,69 +0,0 @@
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):
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)
async def get_url(self):
"""
Get file url.
Attention!!
This method has security vulnerabilities for the reason that result
contains bot's *access token* in open form. Use at your own risk!
:return: url
"""
file = await self.get_file()
return self.bot.get_file_url(file.file_path)
def __hash__(self):
return hash(self.file_id)

View file

@ -1,16 +0,0 @@
from . import base
from . import fields
from .shipping_address import ShippingAddress
class OrderInfo(base.TelegramObject):
"""
This object represents information about an order.
https://core.telegram.org/bots/api#orderinfo
"""
name: base.String = fields.Field()
phone_number: base.String = fields.Field()
email: base.String = fields.Field()
shipping_address: ShippingAddress = fields.Field(base=ShippingAddress)

View file

@ -1,17 +0,0 @@
import typing
from . import base
from . import fields
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
class PassportData(base.TelegramObject):
"""
Contains information about Telegram Passport data shared with the bot by the user.
https://core.telegram.org/bots/api#passportdata
"""
data: typing.List[EncryptedPassportElement] = fields.ListField(base=EncryptedPassportElement)
credentials: EncryptedCredentials = fields.Field(base=EncryptedCredentials)

View file

@ -1,135 +0,0 @@
import typing
from . import base
from . import fields
class PassportElementError(base.TelegramObject):
"""
This object represents an error in the Telegram Passport element which was submitted that
should be resolved by the user.
https://core.telegram.org/bots/api#passportelementerror
"""
source: base.String = fields.Field()
type: base.String = fields.Field()
message: base.String = fields.Field()
class PassportElementErrorDataField(PassportElementError):
"""
Represents an issue in one of the data fields that was provided by the user.
The error is considered resolved when the field's value changes.
https://core.telegram.org/bots/api#passportelementerrordatafield
"""
field_name: base.String = fields.Field()
data_hash: base.String = fields.Field()
def __init__(
self,
source: base.String,
type: base.String,
field_name: base.String,
data_hash: base.String,
message: base.String,
):
super(PassportElementErrorDataField, self).__init__(
source=source, type=type, field_name=field_name, data_hash=data_hash, message=message
)
class PassportElementErrorFile(PassportElementError):
"""
Represents an issue with a document scan.
The error is considered resolved when the file with the document scan changes.
https://core.telegram.org/bots/api#passportelementerrorfile
"""
file_hash: base.String = fields.Field()
def __init__(
self, source: base.String, type: base.String, file_hash: base.String, message: base.String
):
super(PassportElementErrorFile, self).__init__(
source=source, type=type, file_hash=file_hash, message=message
)
class PassportElementErrorFiles(PassportElementError):
"""
Represents an issue with a list of scans.
The error is considered resolved when the list of files containing the scans changes.
https://core.telegram.org/bots/api#passportelementerrorfiles
"""
file_hashes: typing.List[base.String] = fields.ListField()
def __init__(
self,
source: base.String,
type: base.String,
file_hashes: typing.List[base.String],
message: base.String,
):
super(PassportElementErrorFiles, self).__init__(
source=source, type=type, file_hashes=file_hashes, message=message
)
class PassportElementErrorFrontSide(PassportElementError):
"""
Represents an issue with the front side of a document.
The error is considered resolved when the file with the front side of the document changes.
https://core.telegram.org/bots/api#passportelementerrorfrontside
"""
file_hash: base.String = fields.Field()
def __init__(
self, source: base.String, type: base.String, file_hash: base.String, message: base.String
):
super(PassportElementErrorFrontSide, self).__init__(
source=source, type=type, file_hash=file_hash, message=message
)
class PassportElementErrorReverseSide(PassportElementError):
"""
Represents an issue with the reverse side of a document.
The error is considered resolved when the file with reverse side of the document changes.
https://core.telegram.org/bots/api#passportelementerrorreverseside
"""
file_hash: base.String = fields.Field()
def __init__(
self, source: base.String, type: base.String, file_hash: base.String, message: base.String
):
super(PassportElementErrorReverseSide, self).__init__(
source=source, type=type, file_hash=file_hash, message=message
)
class PassportElementErrorSelfie(PassportElementError):
"""
Represents an issue with the selfie with a document.
The error is considered resolved when the file with the selfie changes.
https://core.telegram.org/bots/api#passportelementerrorselfie
"""
file_hash: base.String = fields.Field()
def __init__(
self, source: base.String, type: base.String, file_hash: base.String, message: base.String
):
super(PassportElementErrorSelfie, self).__init__(
source=source, type=type, file_hash=file_hash, message=message
)

View file

@ -1,15 +0,0 @@
from . import base
from . import fields
class PassportFile(base.TelegramObject):
"""
This object represents a file uploaded to Telegram Passport.
Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB.
https://core.telegram.org/bots/api#passportfile
"""
file_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
file_date: base.Integer = fields.Field()

View file

@ -1,16 +0,0 @@
from . import base
from . import fields
from . import mixins
class PhotoSize(base.TelegramObject, mixins.Downloadable):
"""
This object represents one size of a photo or a file / sticker thumbnail.
https://core.telegram.org/bots/api#photosize
"""
file_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
file_size: base.Integer = fields.Field()

View file

@ -1,16 +0,0 @@
import typing
from . import base
from . import fields
class PollOption(base.TelegramObject):
text: base.String = fields.Field()
voter_count: base.Integer = fields.Field()
class Poll(base.TelegramObject):
id: base.String = fields.Field()
question: base.String = fields.Field()
options: typing.List[PollOption] = fields.ListField(base=PollOption)
is_closed: base.Boolean = fields.Field()

View file

@ -1,35 +0,0 @@
from . import base
from . import fields
from .order_info import OrderInfo
from .user import User
class PreCheckoutQuery(base.TelegramObject):
"""
This object contains information about an incoming pre-checkout query.
Your bot can offer users HTML5 games to play solo or to compete against
each other in groups and one-on-one chats.
Create games via @BotFather using the /newgame command.
Please note that this kind of power requires responsibility:
you will need to accept the terms for each game that your bots will be offering.
https://core.telegram.org/bots/api#precheckoutquery
"""
id: base.String = fields.Field()
from_user: User = fields.Field(alias="from", base=User)
currency: base.String = fields.Field()
total_amount: base.Integer = fields.Field()
invoice_payload: base.String = fields.Field()
shipping_option_id: base.String = fields.Field()
order_info: OrderInfo = fields.Field(base=OrderInfo)
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,126 +0,0 @@
import typing
from . import base
from . import fields
class ReplyKeyboardMarkup(base.TelegramObject):
"""
This object represents a custom keyboard with reply options (see Introduction to bots for details and examples).
https://core.telegram.org/bots/api#replykeyboardmarkup
"""
keyboard: "typing.List[typing.List[KeyboardButton]]" = fields.ListOfLists(
base="KeyboardButton", default=[]
)
resize_keyboard: base.Boolean = fields.Field()
one_time_keyboard: base.Boolean = fields.Field()
selective: base.Boolean = fields.Field()
def __init__(
self,
keyboard: "typing.List[typing.List[KeyboardButton]]" = None,
resize_keyboard: base.Boolean = None,
one_time_keyboard: base.Boolean = None,
selective: base.Boolean = None,
row_width: base.Integer = 3,
):
super(ReplyKeyboardMarkup, self).__init__(
keyboard=keyboard,
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
conf={"row_width": row_width},
)
@property
def row_width(self):
return self.conf.get("row_width", 3)
@row_width.setter
def row_width(self, value):
self.conf["row_width"] = value
def add(self, *args):
"""
Add buttons
:param args:
:return: self
:rtype: :obj:`types.ReplyKeyboardMarkup`
"""
row = []
for index, button in enumerate(args, start=1):
row.append(button)
if index % self.row_width == 0:
self.keyboard.append(row)
row = []
if len(row) > 0:
self.keyboard.append(row)
return self
def row(self, *args):
"""
Add row
:param args:
:return: self
:rtype: :obj:`types.ReplyKeyboardMarkup`
"""
btn_array = []
for button in args:
btn_array.append(button)
self.keyboard.append(btn_array)
return self
def insert(self, button):
"""
Insert button to last row
:param button:
:return: self
:rtype: :obj:`types.ReplyKeyboardMarkup`
"""
if self.keyboard and len(self.keyboard[-1]) < self.row_width:
self.keyboard[-1].append(button)
else:
self.add(button)
return self
class KeyboardButton(base.TelegramObject):
"""
This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields are mutually exclusive.
Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
https://core.telegram.org/bots/api#keyboardbutton
"""
text: base.String = fields.Field()
request_contact: base.Boolean = fields.Field()
request_location: base.Boolean = fields.Field()
def __init__(
self,
text: base.String,
request_contact: base.Boolean = None,
request_location: base.Boolean = None,
):
super(KeyboardButton, self).__init__(
text=text, request_contact=request_contact, request_location=request_location
)
class ReplyKeyboardRemove(base.TelegramObject):
"""
Upon receiving a message with this object, Telegram clients will remove the current custom keyboard and display the default letter-keyboard. By default, custom keyboards are displayed until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup).
https://core.telegram.org/bots/api#replykeyboardremove
"""
remove_keyboard: base.Boolean = fields.Field(default=True)
selective: base.Boolean = fields.Field()
def __init__(self, selective: base.Boolean = None):
super(ReplyKeyboardRemove, self).__init__(remove_keyboard=True, selective=selective)

View file

@ -1,13 +0,0 @@
from . import base
from . import fields
class ResponseParameters(base.TelegramObject):
"""
Contains information about why a request was unsuccessful.
https://core.telegram.org/bots/api#responseparameters
"""
migrate_to_chat_id: base.Integer = fields.Field()
retry_after: base.Integer = fields.Field()

View file

@ -1,17 +0,0 @@
from . import base
from . import fields
class ShippingAddress(base.TelegramObject):
"""
This object represents a shipping address.
https://core.telegram.org/bots/api#shippingaddress
"""
country_code: base.String = fields.Field()
state: base.String = fields.Field()
city: base.String = fields.Field()
street_line1: base.String = fields.Field()
street_line2: base.String = fields.Field()
post_code: base.String = fields.Field()

View file

@ -1,35 +0,0 @@
import typing
from . import base
from . import fields
from .labeled_price import LabeledPrice
class ShippingOption(base.TelegramObject):
"""
This object represents one shipping option.
https://core.telegram.org/bots/api#shippingoption
"""
id: base.String = fields.Field()
title: base.String = fields.Field()
prices: typing.List[LabeledPrice] = fields.ListField(base=LabeledPrice)
def __init__(
self, id: base.String, title: base.String, prices: typing.List[LabeledPrice] = None
):
if prices is None:
prices = []
super(ShippingOption, self).__init__(id=id, title=title, prices=prices)
def add(self, price: LabeledPrice):
"""
Add price
:param price:
:return:
"""
self.prices.append(price)
return self

View file

@ -1,25 +0,0 @@
from . import base
from . import fields
from .shipping_address import ShippingAddress
from .user import User
class ShippingQuery(base.TelegramObject):
"""
This object contains information about an incoming shipping query.
https://core.telegram.org/bots/api#shippingquery
"""
id: base.String = fields.Field()
from_user: User = fields.Field(alias="from", base=User)
invoice_payload: base.String = fields.Field()
shipping_address: ShippingAddress = fields.Field(base=ShippingAddress)
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,23 +0,0 @@
from . import base
from . import fields
from . import mixins
from .mask_position import MaskPosition
from .photo_size import PhotoSize
class Sticker(base.TelegramObject, mixins.Downloadable):
"""
This object represents a sticker.
https://core.telegram.org/bots/api#sticker
"""
file_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
is_animated: base.Boolean = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
emoji: base.String = fields.Field()
set_name: base.String = fields.Field()
mask_position: MaskPosition = fields.Field(base=MaskPosition)
file_size: base.Integer = fields.Field()

Some files were not shown because too many files have changed in this diff Show more