mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-08 17:13:56 +00:00
Clean project
This commit is contained in:
parent
a83dd3ca63
commit
bdae5fb026
259 changed files with 1303 additions and 21135 deletions
2
.flake8
2
.flake8
|
|
@ -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
69
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
77
Makefile
77
Makefile
|
|
@ -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
|
||||
|
|
|
|||
57
README.md
57
README.md
|
|
@ -1,57 +0,0 @@
|
|||
# AIOGram
|
||||
|
||||
[](https://opencollective.com/aiogram)
|
||||
[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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)
|
||||
|
||||
# =============================================================================================
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
from . import api
|
||||
from .base import BaseBot
|
||||
from .bot import Bot
|
||||
|
||||
__all__ = ["BaseBot", "Bot", "api"]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
2166
aiogram/bot/bot.py
2166
aiogram/bot/bot.py
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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',
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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()) + "'")
|
||||
|
|
@ -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))
|
||||
|
|
@ -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='*')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 haven’t 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
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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]
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 bot‘s 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 bot‘s questions, it will receive the user’s 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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 that‘s about all we’ve 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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 don‘t support pagination.
|
||||
Offset length can’t 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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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__
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue