Merge branch 'dev-2.x'

This commit is contained in:
Alex Root Junior 2022-08-13 23:50:47 +03:00
commit 92ea40c9f9
No known key found for this signature in database
GPG key ID: 074C1D455EBEA4AC
27 changed files with 483 additions and 127 deletions

98
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View file

@ -0,0 +1,98 @@
name: Bug report
description: Report issues affecting the framework or the documentation.
labels:
- bug
body:
- type: checkboxes
attributes:
label: Checklist
options:
- label: I am sure the error is coming from aiogram code
required: true
- label: I have searched in the issue tracker for similar bug reports, including closed ones
required: true
- type: markdown
attributes:
value: |
## Context
Please provide as much information as possible. This will help us to reproduce the issue and fix it.
- type: input
attributes:
label: Operating system
placeholder: e.g. Ubuntu 20.04.2 LTS
validations:
required: true
- type: input
attributes:
label: Python version
placeholder: e.g. 3.10.1
validations:
required: true
- type: input
attributes:
label: aiogram version
placeholder: e.g. 2.21 or 3.0b3
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: Please describe the behavior you are expecting.
placeholder: E.g. the bot should send a message with the text "Hello, world!".
validations:
required: true
- type: textarea
attributes:
label: Current behavior
description: Please describe the behavior you are currently experiencing.
placeholder: E.g. the bot doesn't send any message.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Please describe the steps you took to reproduce the behavior.
placeholder: |
1. step 1
2. step 2
3. ...
4. you get it...
validations:
required: true
- type: textarea
attributes:
label: Code example
description: Provide a [minimal, reproducible](https://stackoverflow.com/help/minimal-reproducible-example) and properly formatted example (if applicable).
placeholder: |
from aiogram import Bot, Dispatcher
...
render: python3
- type: textarea
attributes:
label: Logs
description: Provide the complete traceback (if applicable) or other kind of logs.
placeholder: |
Traceback (most recent call last):
File "main.py", line 1, in <module>
...
SomeException: ...
render: sh
- type: textarea
attributes:
label: Additional information
description: Please provide any additional information that may help us to reproduce the issue.
placeholder: |
E.g. this behavior is reproducible only in group chats.
You can also attach additional screenshots, logs, or other files.

View file

@ -1,45 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
---
name: Bug report
about: Create a report to help us improve
---
## Context
Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
* Operating System:
* Python Version:
* aiogram version:
* aiohttp version:
* uvloop version (if installed):
## Expected Behavior
Please describe the behavior you are expecting
## Current Behavior
What is the current behavior?
## Failure Information (for bugs)
Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template.
### Steps to Reproduce
Please provide detailed steps for reproducing the issue.
1. step 1
2. step 2
3. you get it...
### Failure Logs
Please include any relevant log snippets or files here.

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: Discuss anything related to the framework
url: https://github.com/aiogram/aiogram/discussions
about: Ask a question about aiogram or share your code snippets and ideas.
- name: Join our Telegram channel
url: https://t.me/aiogram_live
about: Get the latest updates about the framework.
- name: Join our Telegram chat
url: https://t.me/aiogram
about: Get help, ask questions, and discuss the framework in real-time.

44
.github/ISSUE_TEMPLATE/feature.yaml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Feature request
description: Report features you would like to see or improve in the framework.
labels:
- enhancement
body:
- type: textarea
attributes:
label: Problem
description: Is your feature request related to a specific problem? If not, please describe the general idea of your request.
placeholder: e.g. I want to send a photo to a user by url.
validations:
required: true
- type: textarea
attributes:
label: Possible solution
description: Describe the solution you would like to see in the framework.
placeholder: e.g. Add a method to send a photo to a user by url.
validations:
required: true
- type: textarea
attributes:
label: Alternatives
description: What other solutions do you have in mind?
placeholder: e.g. I'm sending a text message with photo url.
- type: textarea
attributes:
label: Code example
description: A small code example that demonstrates the behavior you would like to see.
placeholder: |
await bot.send_photo(user_id, photo_url)
...
render: python3
- type: textarea
attributes:
label: Additional information
description: Any additional information you would like to provide.
placeholder: |
E.g. this method should also cache images to speed up further sending.
You can also attach additional pictures or other files.

View file

@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,35 +1,52 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
<!--
Please include a summary of the change.
Fixes # (issue)
e.g. Add a new awesome feature or Fix documentation typo.
Please also include relevant motivation and context.
If you are fixing an issue, specify what issue is fixed.
e.g. Fix #12345
-->
Type here...
## Type of change
<!--
Please delete options that are not relevant.
-->
- [ ] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Bug fix (non-breaking change that fixes an issue)
- New feature (non-breaking change that adds functionality)
- Documentation (typos, code examples or any documentation update)
# How Has This Been Tested?
# How has this been tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
<!--
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
Please also list any relevant details for your test configuration.
-->
- [ ] Test A
- [ ] Test B
Type here...
**Test Configuration**:
* Operating System:
* Python version:
## Test Configuration
- Operating system: e.g. Ubuntu 20.04.2 LTS
-Python version: e.g. 3.10.1
# Checklist:
<!--
Please delete options that are not relevant to your change.
-->
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added tests that prove my fix is effective or that my feature works as expected
- [ ] New and existing unit tests pass locally with my changes
- [ ] My changes generate no new warnings or errors
- [ ] My changes are compatible with minimum requirements of the project
- [ ] I have made corresponding changes to the documentation

17
.github/workflows/label_pr.yaml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Label new pull request
on:
pull_request_target:
types:
- opened
branches:
- dev-2.x
jobs:
put-label:
runs-on: ubuntu-latest
steps:
- name: Add 2.x label
uses: andymckay/labeler@master
with:
add-labels: 2.x

View file

@ -6,7 +6,7 @@
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-6.0-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT)

View file

@ -21,7 +21,7 @@ AIOGramBot
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.1-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -43,5 +43,5 @@ __all__ = (
'utils',
)
__version__ = '2.21'
__api_version__ = '6.1'
__version__ = '2.22'
__api_version__ = '6.2'

View file

@ -269,6 +269,7 @@ class Methods(Helper):
SEND_STICKER = Item() # sendSticker
GET_STICKER_SET = Item() # getStickerSet
UPLOAD_STICKER_FILE = Item() # uploadStickerFile
GET_CUSTOM_EMOJI_STICKERS = Item() # getCustomEmojiStickers
CREATE_NEW_STICKER_SET = Item() # createNewStickerSet
ADD_STICKER_TO_SET = Item() # addStickerToSet
SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet

View file

@ -38,6 +38,7 @@ class BaseBot:
validate_token: Optional[base.Boolean] = True,
parse_mode: typing.Optional[base.String] = None,
disable_web_page_preview: Optional[base.Boolean] = None,
protect_content: Optional[base.Boolean] = None,
timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None,
server: TelegramAPIServer = TELEGRAM_PRODUCTION
):
@ -60,6 +61,9 @@ class BaseBot:
:type parse_mode: :obj:`str`
:param disable_web_page_preview: You can set default disable web page preview parameter
:type disable_web_page_preview: :obj:`bool`
:param protect_content: Protects the contents of sent messages
from forwarding and saving
:type protect_content: :obj:`typing.Optional[base.Boolean]`
:param timeout: Request timeout
:type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]`
:param server: Telegram Bot API Server endpoint.
@ -111,6 +115,7 @@ class BaseBot:
self.parse_mode = parse_mode
self.disable_web_page_preview = disable_web_page_preview
self.protect_content = protect_content
async def get_new_session(self) -> aiohttp.ClientSession:
return aiohttp.ClientSession(
@ -361,5 +366,22 @@ class BaseBot:
def disable_web_page_preview(self):
self.disable_web_page_preview = None
@property
def protect_content(self):
return getattr(self, "_protect_content", None)
@protect_content.setter
def protect_content(self, value):
if value is None:
setattr(self, "_protect_content", None)
return
if not isinstance(value, bool):
raise TypeError(f"Protect content must be bool, not {type(value)}")
setattr(self, "_protect_content", value)
@protect_content.deleter
def protect_content(self):
self.protect_content = None
def check_auth_widget(self, data):
return check_integrity(self.__token, data)

View file

@ -335,6 +335,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload.setdefault('parse_mode', self.parse_mode)
if self.disable_web_page_preview:
payload.setdefault('disable_web_page_preview', self.disable_web_page_preview)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_MESSAGE, payload)
return types.Message(**result)
@ -375,6 +377,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`types.Message`
"""
payload = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.FORWARD_MESSAGE, payload)
return types.Message(**result)
@ -457,6 +461,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals())
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.COPY_MESSAGE, payload)
return types.MessageId(**result)
@ -525,6 +531,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=['photo'])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'photo', photo)
@ -615,6 +623,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=['audio', 'thumb'])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'audio', audio)
@ -705,6 +715,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=['document'])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'document', document)
@ -797,6 +809,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=['video', 'thumb'])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'video', video)
@ -892,6 +906,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=["animation", "thumb"])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'animation', animation)
@ -972,6 +988,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals(), exclude=['voice'])
if self.parse_mode and caption_entities is None:
payload.setdefault('parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'voice', voice)
@ -1038,6 +1056,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=['video_note'])
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'video_note', video_note)
@ -1101,6 +1121,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
media = prepare_arg(media)
payload = generate_payload(**locals(), exclude=['files'])
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_MEDIA_GROUP, payload, files)
return [types.Message(**message) for message in result]
@ -1174,6 +1196,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_LOCATION, payload)
return types.Message(**result)
@ -1352,6 +1376,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_VENUE, payload)
return types.Message(**result)
@ -1415,6 +1441,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals())
result = await self.request(api.Methods.SEND_CONTACT, payload)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
return types.Message(**result)
async def send_poll(self,
@ -1533,6 +1561,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
payload = generate_payload(**locals())
if self.parse_mode and explanation_entities is None:
payload.setdefault('explanation_parse_mode', self.parse_mode)
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_POLL, payload)
return types.Message(**result)
@ -1592,6 +1622,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_DICE, payload)
return types.Message(**result)
@ -2966,6 +2998,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=['sticker'])
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
files = {}
prepare_file(payload, files, 'sticker', sticker)
@ -3012,6 +3046,23 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files)
return types.File(**result)
async def get_custom_emoji_stickers(self, custom_emoji_ids: typing.List[base.String]) -> typing.List[types.Sticker]:
"""
Use this method to get information about custom emoji stickers by their identifiers.
Source: https://core.telegram.org/bots/api#uploadstickerfile
:param custom_emoji_ids: User identifier of sticker file owner
:type custom_emoji_ids: :obj:`typing.List[base.String]`
:return: Returns an Array of Sticker objects.
:rtype: :obj:`typing.List[types.Sticker]`
"""
payload = generate_payload(**locals())
result = await self.request(api.Methods.GET_CUSTOM_EMOJI_STICKERS, payload)
return [types.Sticker(**item) for item in result]
async def create_new_sticker_set(self,
user_id: base.Integer,
name: base.String,
@ -3021,6 +3072,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
tgs_sticker: base.InputFile = None,
webm_sticker: base.InputFile = None,
contains_masks: typing.Optional[base.Boolean] = None,
sticker_type: typing.Optional[base.String] = None,
mask_position: typing.Optional[types.MaskPosition] = None) -> base.Boolean:
"""
Use this method to create a new sticker set owned by a user.
@ -3049,7 +3101,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type tgs_sticker: :obj:`base.InputFile`
:param webm_sticker: WEBM video with the sticker, uploaded using multipart/form-data.
See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements
:type webm_sticker: :obj:`base.InputFile`
:type webm_sticker: :obj:`base.String`
:param sticker_type: Type of stickers in the set, pass regular or mask.
Custom emoji sticker sets can't be created via the Bot API at the moment.
By default, a regular sticker set is created.
:type sticker_type: :obj:`base.InputFile`
:param emojis: One or more emoji corresponding to the sticker
:type emojis: :obj:`base.String`
:param contains_masks: Pass True, if a set of mask stickers should be created
@ -3061,6 +3117,12 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
mask_position = prepare_arg(mask_position)
payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker', 'webm_sticker'])
if contains_masks is not None:
warnings.warn(
message="The parameter `contains_masks` deprecated, use `sticker_type` instead.",
category=DeprecationWarning,
stacklevel=2
)
files = {}
prepare_file(payload, files, 'png_sticker', png_sticker)
@ -3398,6 +3460,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
reply_markup = prepare_arg(reply_markup)
provider_data = prepare_arg(provider_data)
payload_ = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_INVOICE, payload_)
return types.Message(**result)
@ -3603,6 +3667,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.protect_content is not None:
payload.setdefault('protect_content', self.protect_content)
result = await self.request(api.Methods.SEND_GAME, payload)
return types.Message(**result)

View file

@ -213,5 +213,5 @@ class MongoStorage(BaseStorage):
:return: list of tuples where first element is chat id and second is user id
"""
db = await self.get_db()
items = await db[STATE].find().to_list()
items = await db[STATE].find().to_list(length=None)
return [(int(item['chat']), int(item['user'])) for item in items]

View file

@ -10,7 +10,7 @@ from babel.support import LazyProxy
from aiogram import types
from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated
from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated, BotCommand
ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
@ -34,7 +34,7 @@ class Command(Filter):
By default this filter is registered for messages and edited messages handlers.
"""
def __init__(self, commands: Union[Iterable, str],
def __init__(self, commands: Union[Iterable[Union[str, BotCommand]], str, BotCommand],
prefixes: Union[Iterable, str] = '/',
ignore_case: bool = True,
ignore_mention: bool = False,
@ -66,8 +66,19 @@ class Command(Filter):
@dp.message_handler(commands=['myCommand'], commands_ignore_caption=False, content_types=ContentType.ANY)
@dp.message_handler(Command(['myCommand'], ignore_caption=False), content_types=[ContentType.TEXT, ContentType.DOCUMENT])
"""
if isinstance(commands, str):
if isinstance(commands, (str, BotCommand)):
commands = (commands,)
elif isinstance(commands, Iterable):
if not all(isinstance(cmd, (str, BotCommand)) for cmd in commands):
raise ValueError(
"Command filter only supports str, BotCommand object or their Iterable"
)
else:
raise ValueError(
"Command filter doesn't support {} as input. "
"It only supports str, BotCommand object or their Iterable".format(type(commands))
)
commands = [cmd.command if isinstance(cmd, BotCommand) else cmd for cmd in commands]
self.commands = list(map(str.lower, commands)) if ignore_case else commands
self.prefixes = prefixes

View file

@ -110,6 +110,19 @@ class LifetimeControllerMiddleware(BaseMiddleware):
# TODO: Rename class
skip_patterns = None
_skip_actions = None
@property
def skip_actions(self):
if self._skip_actions is None:
self._skip_actions = []
if self.skip_patterns:
self._skip_actions.extend([
f"pre_process_{item}",
f"process_{item}",
f"post_process_{item}",
])
return self._skip_actions
async def pre_process(self, obj, data, *args):
pass
@ -118,7 +131,7 @@ class LifetimeControllerMiddleware(BaseMiddleware):
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):
if action in self.skip_actions:
return False
obj, *args, data = args

View file

@ -271,7 +271,7 @@ class BaseStorage:
:param user:
:return:
"""
await self.set_data(chat=chat, user=user, data={})
await self.set_bucket(chat=chat, user=user, bucket={})
@staticmethod
def resolve_state(value):

View file

@ -458,6 +458,18 @@ class ProtectContentMixin:
setattr(self, "protect_content", True)
return self
@staticmethod
def _global_protect_content():
"""
Detect global protect content value
:return:
"""
from aiogram import Bot
bot = Bot.get_current()
if bot is not None:
return bot.protect_content
class ParseModeMixin:
def as_html(self):
@ -536,6 +548,8 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin,
parse_mode = self._global_parse_mode()
if disable_web_page_preview is None:
disable_web_page_preview = self._global_disable_web_page_preview()
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.text = text
@ -607,6 +621,9 @@ class ForwardMessage(BaseResponse, ReplyToMixin, DisableNotificationMixin, Prote
from forwarding and saving
:param message_id: Integer - Message identifier in the chat specified in from_chat_id
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.from_chat_id = from_chat_id
self.message_id = message_id
@ -669,6 +686,9 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCon
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.photo = photo
self.caption = caption
@ -731,6 +751,9 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCon
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.audio = audio
self.caption = caption
@ -793,6 +816,9 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin, Protect
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.document = document
self.caption = caption
@ -856,6 +882,9 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCon
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.video = video
self.duration = duration
@ -919,6 +948,9 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCon
- Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.voice = voice
self.caption = caption
@ -977,6 +1009,9 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin, Protec
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.video_note = video_note
self.duration = duration
@ -1037,6 +1072,8 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin, Prote
elif isinstance(media, list):
# Convert list to MediaGroup
media = types.MediaGroup(media)
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.media = media
@ -1117,6 +1154,9 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin, Protect
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.latitude = latitude
self.longitude = longitude
@ -1177,6 +1217,9 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCon
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.latitude = latitude
self.longitude = longitude
@ -1237,6 +1280,9 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectC
- Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.phone_number = phone_number
self.first_name = first_name
@ -1845,6 +1891,9 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectC
Additional interface options. A JSON-serialized object for an inline keyboard,
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.sticker = sticker
self.disable_notification = disable_notification
@ -2288,6 +2337,9 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin, ProtectCont
:param reply_markup: types.InlineKeyboardMarkup (Optional) - A JSON-serialized object for an inline keyboard.
If empty, one Play game_title button will be shown. If not empty, the first button must launch the game.
"""
if protect_content is None:
protect_content = self._global_protect_content()
self.chat_id = chat_id
self.game_short_name = game_short_name
self.disable_notification = disable_notification

View file

@ -31,6 +31,7 @@ class Chat(base.TelegramObject):
photo: ChatPhoto = fields.Field(base=ChatPhoto)
bio: base.String = fields.Field()
has_private_forwards: base.Boolean = fields.Field()
has_restricted_voice_and_video_messages: base.Boolean = fields.Field()
join_to_send_messages: base.Boolean = fields.Field()
join_by_request: base.Boolean = fields.Field()
description: base.String = fields.Field()

View file

@ -19,3 +19,28 @@ class ChatAdministratorRights(base.TelegramObject):
can_post_messages: base.Boolean = fields.Field()
can_edit_messages: base.Boolean = fields.Field()
can_pin_messages: base.Boolean = fields.Field()
def __init__(self,
is_anonymous: base.Boolean = None,
can_manage_chat: base.Boolean = None,
can_delete_messages: base.Boolean = None,
can_manage_video_chats: base.Boolean = None,
can_restrict_members: base.Boolean = None,
can_promote_members: base.Boolean = None,
can_change_info: base.Boolean = None,
can_invite_users: base.Boolean = None,
can_post_messages: base.Boolean = None,
can_edit_messages: base.Boolean = None,
can_pin_messages: base.Boolean = None):
super(ChatAdministratorRights, self).__init__(
is_anonymous=is_anonymous,
can_manage_chat=can_manage_chat,
can_delete_messages=can_delete_messages,
can_manage_video_chats=can_manage_video_chats,
can_restrict_members=can_restrict_members,
can_promote_members=can_promote_members,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages,
can_pin_messages=can_pin_messages)

View file

@ -1,9 +1,9 @@
import sys
from ..utils import helper, markdown
from ..utils.deprecated import deprecated
from . import base, fields
from .user import User
from ..utils import helper, markdown
from ..utils.deprecated import deprecated
class MessageEntity(base.TelegramObject):
@ -19,16 +19,18 @@ class MessageEntity(base.TelegramObject):
url: base.String = fields.Field()
user: User = fields.Field(base=User)
language: base.String = fields.Field()
custom_emoji_id: base.String = fields.Field()
def __init__(
self,
type: base.String,
offset: base.Integer,
length: base.Integer,
url: base.String = None,
user: User = None,
language: base.String = None,
**kwargs
self,
type: base.String,
offset: base.Integer,
length: base.Integer,
url: base.String = None,
user: User = None,
language: base.String = None,
custom_emoji_id: base.String = None,
**kwargs
):
super().__init__(
type=type,
@ -37,6 +39,7 @@ class MessageEntity(base.TelegramObject):
url=url,
user=user,
language=language,
custom_emoji_id=custom_emoji_id,
**kwargs
)
@ -94,6 +97,8 @@ class MessageEntity(base.TelegramObject):
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)
if self.type == MessageEntityType.CUSTOM_EMOJI and self.user:
return entity_text
return entity_text
@ -118,6 +123,7 @@ class MessageEntityType(helper.Helper):
:key: PRE
:key: TEXT_LINK
:key: TEXT_MENTION
:key: CUSTOM_EMOJI
"""
mode = helper.HelperMode.snake_case
@ -138,3 +144,4 @@ class MessageEntityType(helper.Helper):
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
CUSTOM_EMOJI = helper.Item() # custom_emoji

View file

@ -5,6 +5,7 @@ from .mask_position import MaskPosition
from .photo_size import PhotoSize
from .file import File
class Sticker(base.TelegramObject, mixins.Downloadable):
"""
This object represents a sticker.
@ -13,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
"""
file_id: base.String = fields.Field()
file_unique_id: base.String = fields.Field()
type: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
is_animated: base.Boolean = fields.Field()
@ -22,6 +24,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
set_name: base.String = fields.Field()
premium_animation: File = fields.Field(base=File)
mask_position: MaskPosition = fields.Field(base=MaskPosition)
custom_emoji_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
async def set_position_in_set(self, position: base.Integer) -> base.Boolean:

View file

@ -14,8 +14,9 @@ class StickerSet(base.TelegramObject):
"""
name: base.String = fields.Field()
title: base.String = fields.Field()
sticker_type: base.String = fields.Field()
is_animated: base.Boolean = fields.Field()
is_video: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field() # Deprecated
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
thumb: PhotoSize = fields.Field(base=PhotoSize)

View file

@ -44,6 +44,8 @@ class TextDecoration(ABC):
return self.link(value=text, link=f"tg://user?id={user.id}")
if entity.type == "text_link":
return self.link(value=text, link=cast(str, entity.url))
if entity.type == "custom_emoji":
return self.custom_emoji(value=text, custom_emoji_id=entity.custom_emoji_id)
return self.quote(text)
@ -143,6 +145,10 @@ class TextDecoration(ABC):
def quote(self, value: str) -> str: # pragma: no cover
pass
@abstractmethod
def custom_emoji(self, value: str, custom_emoji_id: str) -> str: # pragma: no cover
pass
class HtmlDecoration(TextDecoration):
def link(self, value: str, link: str) -> str:
@ -175,6 +181,9 @@ class HtmlDecoration(TextDecoration):
def quote(self, value: str) -> str:
return html.escape(value, quote=False)
def custom_emoji(self, value: str, custom_emoji_id: str) -> str:
return f'<tg-emoji emoji-id="{custom_emoji_id}">{value}</tg-emoji>'
class MarkdownDecoration(TextDecoration):
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
@ -209,6 +218,9 @@ class MarkdownDecoration(TextDecoration):
def quote(self, value: str) -> str:
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
def custom_emoji(self, value: str, custom_emoji_id: str) -> str:
return self.link(value=value, link=f"tg://emoji?id={custom_emoji_id}")
html_decoration = HtmlDecoration()
markdown_decoration = MarkdownDecoration()

View file

@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.1-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -1,22 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<div id="content">not inited</div>
<button onclick="validateData()" type="button" class="btn btn-primary">Show</button>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script src="static/app.js"></script>
</body>
</html>

View file

@ -1,4 +1,4 @@
from typing import Set
from typing import Set, Union, Iterable
from datetime import datetime
import pytest
@ -6,9 +6,9 @@ import pytest
from aiogram.dispatcher.filters.builtin import (
Text,
extract_chat_ids,
ChatIDArgumentType, ForwardedMessageFilter, IDFilter,
ChatIDArgumentType, ForwardedMessageFilter, IDFilter, Command,
)
from aiogram.types import Message
from aiogram.types import Message, BotCommand
from tests.types.dataset import MESSAGE, MESSAGE_FROM_CHANNEL
@ -108,3 +108,42 @@ class TestIDFilter:
filter = IDFilter(chat_id=message_from_channel.chat.id)
assert await filter.check(message_from_channel)
@pytest.mark.parametrize("command", [
"/start",
"/start some args",
])
@pytest.mark.parametrize("cmd_filter", [
"start",
("start",),
BotCommand(command="start", description="my desc"),
(BotCommand(command="start", description="bar"),),
(BotCommand(command="start", description="foo"), "help"),
])
@pytest.mark.asyncio
async def test_commands_filter(command: str, cmd_filter: Union[Iterable[Union[str, BotCommand]], str, BotCommand]):
message_with_command = Message(**MESSAGE)
message_with_command.text = command
start_filter = Command(commands=cmd_filter)
assert await start_filter.check(message_with_command)
@pytest.mark.asyncio
async def test_commands_filter_not_checked():
message_with_command = Message(**MESSAGE)
message_with_command.text = "/start"
start_filter = Command(commands=["help", BotCommand("about", "my desc")])
assert not await start_filter.check(message_with_command)
def test_commands_filter_raises_error():
with pytest.raises(ValueError):
start_filter = Command(commands=42) # noqa
with pytest.raises(ValueError):
start_filter = Command(commands=[42]) # noqa