Merge branch 'aiogram:dev-2.x' into dev-2.x

This commit is contained in:
andriy 2023-03-20 14:08:53 +02:00 committed by GitHub
commit 56f4f2749e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 618 additions and 348 deletions

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.3-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.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://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 @@ aiogram
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.3-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.4-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.23.1'
__api_version__ = '6.3'
__version__ = '2.25.1'
__api_version__ = '6.5'

View file

@ -259,6 +259,11 @@ class Methods(Helper):
REOPEN_FORUM_TOPIC = Item() # reopenForumTopic
DELETE_FORUM_TOPIC = Item() # deleteForumTopic
UNPIN_ALL_FORUM_TOPIC_MESSAGES = Item() # unpinAllForumTopicMessages
EDIT_GENERAL_FORUM_TOPIC = Item() # editGeneralForumTopic
CLOSE_GENERAL_FORUM_TOPIC = Item() # closeGeneralForumTopic
REOPEN_GENERAL_FORUM_TOPIC = Item() # reopenGeneralForumTopic
HIDE_GENERAL_FORUM_TOPIC = Item() # hideGeneralForumTopic
UNHIDE_GENERAL_FORUM_TOPIC = Item() # unhideGeneralForumTopic
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
SET_MY_COMMANDS = Item() # setMyCommands
DELETE_MY_COMMANDS = Item() # deleteMyCommands

View file

@ -498,6 +498,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
types.ForceReply, None] = None,
has_spoiler: typing.Optional[base.Boolean] = None,
) -> types.Message:
"""
Use this method to send photos.
@ -544,6 +545,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param has_spoiler: Pass True if the photo needs to be covered with a spoiler animation
:type has_spoiler: :obj:`typing.Optional[base.Boolean]`
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
@ -776,6 +780,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
types.ForceReply, None] = None,
has_spoiler: typing.Optional[base.Boolean] = None,
) -> types.Message:
"""
Use this method to send video files, Telegram clients support mp4 videos
@ -838,6 +843,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
:param has_spoiler: Pass True if the video needs to be covered with a spoiler animation
:type has_spoiler: :obj:`typing.Optional[base.Boolean]`
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
@ -875,6 +883,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
types.ForceReply], None] = None,
has_spoiler: typing.Optional[base.Boolean] = None,
) -> types.Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@ -940,6 +949,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove, types.ForceReply], None]`
:param has_spoiler: Pass True if the animation needs to be covered with a spoiler animation
:type has_spoiler: :obj:`typing.Optional[base.Boolean]`
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
@ -1711,7 +1723,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return types.Message(**result)
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
action: base.String) -> base.Boolean:
action: base.String, message_thread_id: typing.Optional[base.Integer] = None) -> base.Boolean:
"""
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
@ -1743,6 +1755,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
`upload_video_note` for video notes.
:type action: :obj:`base.String`
:param message_thread_id: Unique identifier for the target message thread; supergroups only
:type message_thread_id: :obj:`typing.Optional[base.Integer]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
@ -1885,16 +1900,20 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return await self.request(api.Methods.UNBAN_CHAT_MEMBER, payload)
async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
user_id: base.Integer,
permissions: typing.Optional[types.ChatPermissions] = None,
# permissions argument need to be required after removing other `can_*` arguments
until_date: typing.Union[
base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Optional[base.Boolean] = None,
can_send_media_messages: typing.Optional[base.Boolean] = None,
can_send_other_messages: typing.Optional[base.Boolean] = None,
can_add_web_page_previews: typing.Optional[base.Boolean] = None) -> base.Boolean:
async def restrict_chat_member(
self,
chat_id: typing.Union[base.Integer, base.String],
user_id: base.Integer,
permissions: typing.Optional[types.ChatPermissions],
use_independent_chat_permissions: typing.Optional[base.Boolean] = None,
# permissions argument need to be required after removing other `can_*` arguments
until_date: typing.Union[
base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Optional[base.Boolean] = None,
can_send_media_messages: typing.Optional[base.Boolean] = None,
can_send_other_messages: typing.Optional[base.Boolean] = None,
can_add_web_page_previews: typing.Optional[base.Boolean] = 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.
@ -1908,6 +1927,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type user_id: :obj:`base.Integer`
:param permissions: New user permissions
:type permissions: :obj:`ChatPermissions`
:param use_independent_chat_permissions: Pass True if chat
permissions are set independently. Otherwise,
the can_send_other_messages and can_add_web_page_previews
permissions will imply the can_send_messages,
can_send_audios, can_send_documents, can_send_photos,
can_send_videos, can_send_video_notes, and
can_send_voice_notes permissions; the can_send_polls
permission will imply the can_send_messages permission.
:type use_independent_chat_permissions: :obj:`typing.Optional[base.Boolean]`
:param until_date: Date when restrictions will be lifted for the user, unix time
:type until_date: :obj:`typing.Optional[base.Integer]`
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
@ -2091,8 +2119,12 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return await self.request(api.Methods.UNBAN_CHAT_SENDER_CHAT, payload)
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
permissions: types.ChatPermissions) -> base.Boolean:
async def set_chat_permissions(
self,
chat_id: typing.Union[base.Integer, base.String],
permissions: types.ChatPermissions,
use_independent_chat_permissions: base.Boolean = None,
) -> base.Boolean:
"""
Use this method to set default chat permissions for all members.
The bot must be an administrator in the group or a supergroup for this to work and must have the
@ -2102,6 +2134,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:param chat_id: Unique identifier for the target chat or username of the target supergroup
:param permissions: New default chat permissions
:param use_independent_chat_permissions: Pass True if chat
permissions are set independently. Otherwise,
the can_send_other_messages and can_add_web_page_previews
permissions will imply the can_send_messages,
can_send_audios, can_send_documents, can_send_photos,
can_send_videos, can_send_video_notes, and
can_send_voice_notes permissions; the can_send_polls
permission will imply the can_send_messages permission.
:type use_independent_chat_permissions: :obj:`typing.Optional[base.Boolean]`
:return: True on success.
"""
permissions = prepare_arg(permissions)
@ -2458,6 +2499,97 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return await self.request(api.Methods.UNPIN_ALL_CHAT_MESSAGES, payload)
async def close_general_forum_topic(
self,
chat_id: typing.Union[base.Integer, base.String],
) -> base.Boolean:
"""
Use this method to close an open 'General' topic in a forum supergroup chat.
The bot must be an administrator in the chat for this to work and must have the *can_manage_topics* administrator rights.
Returns :code:`True` on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
:return: Returns :code:`True` on success.
"""
payload = generate_payload(**locals())
return await self.request(api.Methods.CLOSE_GENERAL_FORUM_TOPIC, payload)
async def edit_general_forum_topic(
self,
chat_id: typing.Union[base.Integer, base.String],
name: base.String,
) -> base.Boolean:
"""
Use this method to edit the name of the 'General' topic in a forum supergroup chat.
The bot must be an administrator in the chat for this to work and must have *can_manage_topics* administrator rights.
Returns :code:`True` on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
:param name: New topic name, 1-128 characters
:return: Returns :code:`True` on success.
"""
payload = generate_payload(**locals())
return await self.request(api.Methods.EDIT_GENERAL_FORUM_TOPIC, payload)
async def hide_general_forum_topic(
self,
chat_id: typing.Union[base.Integer, base.String],
) -> base.Boolean:
"""
Use this method to hide the 'General' topic in a forum supergroup chat.
The bot must be an administrator in the chat for this to work and must have the *can_manage_topics* administrator rights.
The topic will be automatically closed if it was open. Returns :code:`True` on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
:return: Returns :code:`True` on success.
"""
payload = generate_payload(**locals())
return await self.request(api.Methods.HIDE_GENERAL_FORUM_TOPIC, payload)
async def reopen_general_forum_topic(
self,
chat_id: typing.Union[base.Integer, base.String],
) -> base.Boolean:
"""
Use this method to reopen a closed 'General' topic in a forum supergroup chat.
The bot must be an administrator in the chat for this to work and must have the *can_manage_topics* administrator rights.
The topic will be automatically unhidden if it was hidden. Returns :code:`True` on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
:return: Returns :code:`True` on success.
"""
payload = generate_payload(**locals())
return await self.request(api.Methods.REOPEN_GENERAL_FORUM_TOPIC, payload)
async def unhide_general_forum_topic(
self,
chat_id: typing.Union[base.Integer, base.String],
) -> base.Boolean:
"""
Use this method to unhide the 'General' topic in a forum supergroup chat.
The bot must be an administrator in the chat for this to work and must have the *can_manage_topics* administrator rights.
Returns :code:`True` on success.
:param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
:return: Returns :code:`True` on success.
"""
payload = generate_payload(**locals())
return await self.request(api.Methods.UNPIN_ALL_CHAT_MESSAGES, payload)
async def leave_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean:
"""
Use this method for your bot to leave a group, supergroup or channel.
@ -2631,7 +2763,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
return types.ForumTopic(**result)
async def edit_forum_topic(self, chat_id: typing.Union[int, str],
name: base.String,
name: typing.Optional[base.String] = None,
message_thread_id: typing.Optional[base.Integer] = None,
icon_custom_emoji_id: typing.Optional[base.String] = None) -> base.Boolean:
"""

View file

@ -1,18 +1,18 @@
"""
This module has redis storage for finite-state machine based on `aioredis <https://github.com/aio-libs/aioredis>`_ driver
This module has redis storage for finite-state machine based on `redis <https://pypi.org/project/redis/>`_ driver.
"""
import asyncio
import logging
import typing
from abc import ABC, abstractmethod
import aioredis
from ...dispatcher.storage import BaseStorage
from ...utils import json
from ...utils.deprecated import deprecated
if typing.TYPE_CHECKING:
import aioredis
STATE_KEY = 'state'
STATE_DATA_KEY = 'data'
STATE_BUCKET_KEY = 'bucket'
@ -67,6 +67,8 @@ class RedisStorage(BaseStorage):
Get Redis connection
"""
# Use thread-safe asyncio Lock because this method without that is not safe
import aioredis
async with self._connection_lock:
if self._redis is None or self._redis.closed:
self._redis = await aioredis.create_connection((self._host, self._port),
@ -207,138 +209,6 @@ class RedisStorage(BaseStorage):
await self.set_record(chat=chat, user=user, state=record['state'], data=record_bucket, bucket=bucket)
class AioRedisAdapterBase(ABC):
"""Base aioredis adapter class."""
def __init__(
self,
host: str = "localhost",
port: int = 6379,
db: typing.Optional[int] = None,
password: typing.Optional[str] = None,
ssl: typing.Optional[bool] = None,
pool_size: int = 10,
loop: typing.Optional[asyncio.AbstractEventLoop] = None,
prefix: str = "fsm",
state_ttl: typing.Optional[int] = None,
data_ttl: typing.Optional[int] = None,
bucket_ttl: typing.Optional[int] = None,
**kwargs,
):
self._host = host
self._port = port
self._db = db
self._password = password
self._ssl = ssl
self._pool_size = pool_size
self._kwargs = kwargs
self._prefix = (prefix,)
self._state_ttl = state_ttl
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
self._redis: typing.Optional["aioredis.Redis"] = None
self._connection_lock = asyncio.Lock()
@abstractmethod
async def get_redis(self) -> aioredis.Redis:
"""Get Redis connection."""
pass
async def close(self):
"""Grace shutdown."""
pass
async def wait_closed(self):
"""Wait for grace shutdown finishes."""
pass
async def set(self, name, value, ex=None, **kwargs):
"""Set the value at key ``name`` to ``value``."""
if ex == 0:
ex = None
return await self._redis.set(name, value, ex=ex, **kwargs)
async def get(self, name, **kwargs):
"""Return the value at key ``name`` or None."""
return await self._redis.get(name, **kwargs)
async def delete(self, *names):
"""Delete one or more keys specified by ``names``"""
return await self._redis.delete(*names)
async def keys(self, pattern, **kwargs):
"""Returns a list of keys matching ``pattern``."""
return await self._redis.keys(pattern, **kwargs)
async def flushdb(self):
"""Delete all keys in the current database."""
return await self._redis.flushdb()
class AioRedisAdapterV1(AioRedisAdapterBase):
"""Redis adapter for aioredis v1."""
async def get_redis(self) -> aioredis.Redis:
"""Get Redis connection."""
async with self._connection_lock: # to prevent race
if self._redis is None or self._redis.closed:
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,
**self._kwargs,
)
return self._redis
async def close(self):
async with self._connection_lock:
if self._redis and not self._redis.closed:
self._redis.close()
async def wait_closed(self):
async with self._connection_lock:
if self._redis:
return await self._redis.wait_closed()
return True
async def get(self, name, **kwargs):
return await self._redis.get(name, encoding="utf8", **kwargs)
async def set(self, name, value, ex=None, **kwargs):
if ex == 0:
ex = None
return await self._redis.set(name, value, expire=ex, **kwargs)
async def keys(self, pattern, **kwargs):
"""Returns a list of keys matching ``pattern``."""
return await self._redis.keys(pattern, encoding="utf8", **kwargs)
class AioRedisAdapterV2(AioRedisAdapterBase):
"""Redis adapter for aioredis v2."""
async def get_redis(self) -> aioredis.Redis:
"""Get Redis connection."""
async with self._connection_lock: # to prevent race
if self._redis is None:
self._redis = aioredis.Redis(
host=self._host,
port=self._port,
db=self._db,
password=self._password,
ssl=self._ssl,
max_connections=self._pool_size,
decode_responses=True,
**self._kwargs,
)
return self._redis
class RedisStorage2(BaseStorage):
"""
Busted Redis-base storage for FSM.
@ -356,7 +226,6 @@ class RedisStorage2(BaseStorage):
.. code-block:: python3
await dp.storage.close()
await dp.storage.wait_closed()
"""
@ -375,75 +244,49 @@ class RedisStorage2(BaseStorage):
bucket_ttl: typing.Optional[int] = None,
**kwargs,
):
self._host = host
self._port = port
self._db = db
self._password = password
self._ssl = ssl
self._pool_size = pool_size
self._kwargs = kwargs
self._prefix = (prefix,)
from redis.asyncio import Redis
self._redis: typing.Optional[Redis] = Redis(
host=host,
port=port,
db=db,
password=password,
ssl=ssl,
max_connections=pool_size,
decode_responses=True,
**kwargs,
)
self._prefix = (prefix,)
self._state_ttl = state_ttl
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
self._redis: typing.Optional[AioRedisAdapterBase] = None
self._connection_lock = asyncio.Lock()
@deprecated("This method will be removed in aiogram v3.0. "
"You should use your own instance of Redis.", stacklevel=3)
async def redis(self) -> aioredis.Redis:
adapter = await self._get_adapter()
return await adapter.get_redis()
async def _get_adapter(self) -> AioRedisAdapterBase:
"""Get adapter based on aioredis version."""
if self._redis is None:
redis_version = int(aioredis.__version__.split(".")[0])
connection_data = dict(
host=self._host,
port=self._port,
db=self._db,
password=self._password,
ssl=self._ssl,
pool_size=self._pool_size,
**self._kwargs,
)
if redis_version == 1:
self._redis = AioRedisAdapterV1(**connection_data)
elif redis_version == 2:
self._redis = AioRedisAdapterV2(**connection_data)
else:
raise RuntimeError(f"Unsupported aioredis version: {redis_version}")
await self._redis.get_redis()
async def redis(self) -> "aioredis.Redis":
return self._redis
def generate_key(self, *parts):
return ':'.join(self._prefix + tuple(map(str, parts)))
async def close(self):
if self._redis:
return await self._redis.close()
await self._redis.close()
async def wait_closed(self):
if self._redis:
await self._redis.wait_closed()
self._redis = None
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 = self.check_address(chat=chat, user=user)
key = self.generate_key(chat, user, STATE_KEY)
redis = await self._get_adapter()
return await redis.get(key) or self.resolve_state(default)
return await self._redis.get(key) or self.resolve_state(default)
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._get_adapter()
raw_result = await redis.get(key)
raw_result = await self._redis.get(key)
if raw_result:
return json.loads(raw_result)
return default or {}
@ -452,21 +295,19 @@ class RedisStorage2(BaseStorage):
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._get_adapter()
if state is None:
await redis.delete(key)
await self._redis.delete(key)
else:
await redis.set(key, self.resolve_state(state), ex=self._state_ttl)
await self._redis.set(key, self.resolve_state(state), ex=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._get_adapter()
if data:
await redis.set(key, json.dumps(data), ex=self._data_ttl)
await self._redis.set(key, json.dumps(data), ex=self._data_ttl)
else:
await redis.delete(key)
await self._redis.delete(key)
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs):
@ -483,8 +324,7 @@ class RedisStorage2(BaseStorage):
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._get_adapter()
raw_result = await redis.get(key)
raw_result = await self._redis.get(key)
if raw_result:
return json.loads(raw_result)
return default or {}
@ -493,11 +333,10 @@ class RedisStorage2(BaseStorage):
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._get_adapter()
if bucket:
await redis.set(key, json.dumps(bucket), ex=self._bucket_ttl)
await self._redis.set(key, json.dumps(bucket), ex=self._bucket_ttl)
else:
await redis.delete(key)
await self._redis.delete(key)
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None,
@ -515,13 +354,11 @@ class RedisStorage2(BaseStorage):
:param full: clean DB or clean only states
:return:
"""
redis = await self._get_adapter()
if full:
await redis.flushdb()
await self._redis.flushdb()
else:
keys = await redis.keys(self.generate_key('*'))
await redis.delete(*keys)
keys = await self._redis.keys(self.generate_key('*'))
await self._redis.delete(*keys)
async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
"""
@ -529,10 +366,9 @@ class RedisStorage2(BaseStorage):
:return: list of tuples where first element is chat id and second is user id
"""
redis = await self._get_adapter()
result = []
keys = await redis.keys(self.generate_key('*', '*', STATE_KEY))
keys = await self._redis.keys(self.generate_key('*', '*', STATE_KEY))
for item in keys:
*_, chat, user, _ = item.split(':')
result.append((chat, user))

View file

@ -21,6 +21,7 @@ from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberBanned,
from .chat_member_updated import ChatMemberUpdated
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chat_shared import ChatShared
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
from .dice import Dice, DiceEmoji
@ -32,9 +33,12 @@ from .force_reply import ForceReply
from .forum_topic import ForumTopic
from .forum_topic_closed import ForumTopicClosed
from .forum_topic_created import ForumTopicCreated
from .forum_topic_edited import ForumTopicEdited
from .forum_topic_reopened import ForumTopicReopened
from .game import Game
from .game_high_score import GameHighScore
from .general_forum_topic_hidden import GeneralForumTopicHidden
from .general_forum_topic_unhidden import GeneralForumTopicUnhidden
from .inline_keyboard import InlineKeyboardButton, InlineKeyboardMarkup
from .inline_query import InlineQuery
from .inline_query_result import InlineQueryResult, InlineQueryResultArticle, InlineQueryResultAudio, \
@ -68,7 +72,8 @@ from .photo_size import PhotoSize
from .poll import PollOption, Poll, PollAnswer, PollType
from .pre_checkout_query import PreCheckoutQuery
from .proximity_alert_triggered import ProximityAlertTriggered
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType, \
KeyboardButtonRequestChat, KeyboardButtonRequestUser
from .response_parameters import ResponseParameters
from .sent_web_app_message import SentWebAppMessage
from .shipping_address import ShippingAddress
@ -80,6 +85,7 @@ from .successful_payment import SuccessfulPayment
from .update import AllowedUpdates, Update
from .user import User
from .user_profile_photos import UserProfilePhotos
from .user_shared import UserShared
from .venue import Venue
from .video import Video
from .video_chat_ended import VideoChatEnded
@ -95,6 +101,7 @@ from .voice_chat_started import VoiceChatStarted
from .web_app_data import WebAppData
from .web_app_info import WebAppInfo
from .webhook_info import WebhookInfo
from .write_access_allowed import WriteAccessAllowed
__all__ = (
'AllowedUpdates',
@ -185,6 +192,8 @@ __all__ = (
'Invoice',
'KeyboardButton',
'KeyboardButtonPollType',
'KeyboardButtonRequestChat',
'KeyboardButtonRequestUser',
'LabeledPrice',
'Location',
'LoginUrl',
@ -248,6 +257,12 @@ __all__ = (
'ForumTopicCreated',
'ForumTopicClosed',
'ForumTopicReopened',
'ForumTopicEdited',
'GeneralForumTopicHidden',
'GeneralForumTopicUnhidden',
'WriteAccessAllowed',
"ChatShared",
"UserShared",
'base',
'fields',
)

View file

@ -48,6 +48,8 @@ class Chat(base.TelegramObject):
can_set_sticker_set: base.Boolean = fields.Field()
linked_chat_id: base.Integer = fields.Field()
location: ChatLocation = fields.Field()
has_hidden_members: base.Boolean = fields.Field()
has_aggressive_anti_spam_enabled: base.Boolean = fields.Field()
def __hash__(self):
return self.id
@ -312,14 +314,18 @@ class Chat(base.TelegramObject):
async def promote(self,
user_id: base.Integer,
is_anonymous: typing.Optional[base.Boolean] = None,
can_manage_chat: typing.Optional[base.Boolean] = None,
can_change_info: typing.Optional[base.Boolean] = None,
can_post_messages: typing.Optional[base.Boolean] = None,
can_edit_messages: typing.Optional[base.Boolean] = None,
can_delete_messages: typing.Optional[base.Boolean] = None,
can_manage_voice_chats: typing.Optional[base.Boolean] = None,
can_invite_users: typing.Optional[base.Boolean] = None,
can_restrict_members: typing.Optional[base.Boolean] = None,
can_pin_messages: typing.Optional[base.Boolean] = None,
can_promote_members: typing.Optional[base.Boolean] = None) -> base.Boolean:
can_promote_members: typing.Optional[base.Boolean] = None,
can_manage_video_chats: typing.Optional[base.Boolean] = None,
can_manage_topics: typing.Optional[base.Boolean] = 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.
@ -362,6 +368,7 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
return await self.bot.promote_chat_member(self.id,
user_id=user_id,
is_anonymous=is_anonymous,
@ -372,7 +379,12 @@ class Chat(base.TelegramObject):
can_invite_users=can_invite_users,
can_restrict_members=can_restrict_members,
can_pin_messages=can_pin_messages,
can_promote_members=can_promote_members)
can_promote_members=can_promote_members,
can_manage_chat=can_manage_chat,
can_manage_voice_chats=can_manage_voice_chats,
can_manage_video_chats=can_manage_video_chats,
can_manage_topics=can_manage_topics
)
async def set_permissions(self, permissions: ChatPermissions) -> base.Boolean:
"""
@ -552,7 +564,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.delete_chat_sticker_set(self.id)
async def do(self, action: base.String) -> base.Boolean:
async def do(self, action: base.String, message_thread_id: typing.Optional[base.Integer] = None) -> base.Boolean:
"""
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
@ -565,6 +577,8 @@ class Chat(base.TelegramObject):
:param action: Type of action to broadcast.
:type action: :obj:`base.String`
:param message_thread_id: Unique identifier for the target message thread; supergroups only
:type message_thread_id: :obj:`typing.Optional[base.Integer]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""

View file

@ -16,6 +16,7 @@ class ChatJoinRequest(base.TelegramObject):
chat: Chat = fields.Field(base=Chat)
from_user: User = fields.Field(alias="from", base=User)
user_chat_id: base.Integer = fields.Field()
date: datetime = fields.DateTimeField()
bio: base.String = fields.Field()
invite_link: ChatInviteLink = fields.Field(base=ChatInviteLink)

View file

@ -189,7 +189,16 @@ class ChatMemberRestricted(ChatMember):
can_pin_messages: base.Boolean = fields.Field()
can_manage_topics: base.Boolean = fields.Field()
can_send_messages: base.Boolean = fields.Field()
can_send_audios: base.Boolean = fields.Field()
can_send_documents: base.Boolean = fields.Field()
can_send_photos: base.Boolean = fields.Field()
can_send_videos: base.Boolean = fields.Field()
can_send_video_notes: base.Boolean = fields.Field()
can_send_voice_notes: base.Boolean = fields.Field()
# warning! field was replaced: https://core.telegram.org/bots/api#february-3-2023
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()

View file

@ -9,7 +9,13 @@ class ChatPermissions(base.TelegramObject):
https://core.telegram.org/bots/api#chatpermissions
"""
can_send_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field()
can_send_media_messages: base.Boolean = fields.Field() # Deprecated since Bot API 6.5
can_send_audios: base.Boolean = fields.Field()
can_send_documents: base.Boolean = fields.Field()
can_send_photos: base.Boolean = fields.Field()
can_send_videos: base.Boolean = fields.Field()
can_send_video_notes: base.Boolean = fields.Field()
can_send_voice_notes: 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()
@ -21,6 +27,12 @@ class ChatPermissions(base.TelegramObject):
def __init__(self,
can_send_messages: base.Boolean = None,
can_send_media_messages: base.Boolean = None,
can_send_audios: base.Boolean = None,
can_send_documents: base.Boolean = None,
can_send_photos: base.Boolean = None,
can_send_videos: base.Boolean = None,
can_send_video_notes: base.Boolean = None,
can_send_voice_notes: base.Boolean = None,
can_send_polls: base.Boolean = None,
can_send_other_messages: base.Boolean = None,
can_add_web_page_previews: base.Boolean = None,
@ -32,6 +44,12 @@ class ChatPermissions(base.TelegramObject):
super(ChatPermissions, self).__init__(
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_audios=can_send_audios,
can_send_documents=can_send_documents,
can_send_photos=can_send_photos,
can_send_videos=can_send_videos,
can_send_video_notes=can_send_video_notes,
can_send_voice_notes=can_send_voice_notes,
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
@ -39,4 +57,5 @@ class ChatPermissions(base.TelegramObject):
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
**kwargs
)

View file

@ -0,0 +1,12 @@
from . import base, fields
class ChatShared(base.TelegramObject):
"""
This object contains information about the chat whose identifier was
shared with the bot using a KeyboardButtonRequestChat button.
https://core.telegram.org/bots/api#chatshared
"""
request_id: base.Integer = fields.Field()
user_id: base.Integer = fields.Field()

View file

@ -0,0 +1,14 @@
import typing
from . import base
from . import fields
class ForumTopicEdited(base.TelegramObject):
"""
This object represents a service message about an edited forum topic.
https://core.telegram.org/bots/api#forumtopicedited
"""
name: typing.Optional[base.String] = fields.Field()
icon_custom_emoji_id: typing.Optional[base.String] = fields.Field()

View file

@ -0,0 +1,9 @@
from . import base
class GeneralForumTopicHidden(base.TelegramObject):
"""
This object represents a service message about General forum topic hidden in the chat. Currently holds no information.
https://core.telegram.org/bots/api#generalforumtopichidden
"""

View file

@ -0,0 +1,9 @@
from . import base
class GeneralForumTopicUnhidden(base.TelegramObject):
"""
This object represents a service message about General forum topic unhidden in the chat. Currently holds no information.
https://core.telegram.org/bots/api#generalforumtopicunhidden
"""

View file

@ -107,6 +107,7 @@ class InputMediaAnimation(InputMedia):
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
has_spoiler: typing.Optional[base.Boolean] = fields.Field()
def __init__(
self,
@ -118,12 +119,13 @@ class InputMediaAnimation(InputMedia):
duration: base.Integer = None,
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
has_spoiler: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
type='animation', media=media, thumb=thumb, caption=caption, width=width,
height=height, duration=duration, parse_mode=parse_mode,
caption_entities=caption_entities, conf=kwargs,
caption_entities=caption_entities, has_spoiler=has_spoiler, conf=kwargs,
)
@ -188,6 +190,7 @@ class InputMediaPhoto(InputMedia):
https://core.telegram.org/bots/api#inputmediaphoto
"""
has_spoiler: typing.Optional[base.Boolean] = fields.Field()
def __init__(
self,
@ -195,11 +198,12 @@ class InputMediaPhoto(InputMedia):
caption: base.String = None,
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
has_spoiler: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
type='photo', media=media, caption=caption, parse_mode=parse_mode,
caption_entities=caption_entities, conf=kwargs,
caption_entities=caption_entities, has_spoiler=has_spoiler, conf=kwargs,
)
@ -213,6 +217,7 @@ class InputMediaVideo(InputMedia):
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
supports_streaming: base.Boolean = fields.Field()
has_spoiler: typing.Optional[base.Boolean] = fields.Field()
def __init__(
self,
@ -225,13 +230,14 @@ class InputMediaVideo(InputMedia):
parse_mode: base.String = None,
caption_entities: typing.Optional[typing.List[MessageEntity]] = None,
supports_streaming: base.Boolean = None,
has_spoiler: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
type='video', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, caption_entities=caption_entities,
supports_streaming=supports_streaming, conf=kwargs
supports_streaming=supports_streaming, has_spoiler=has_spoiler, conf=kwargs
)

View file

@ -8,11 +8,18 @@ from . import base, fields
from .animation import Animation
from .audio import Audio
from .chat import Chat, ChatType
from .chat_shared import ChatShared
from .contact import Contact
from .dice import Dice
from .document import Document
from .force_reply import ForceReply
from .forum_topic_closed import ForumTopicClosed
from .forum_topic_created import ForumTopicCreated
from .forum_topic_edited import ForumTopicEdited
from .forum_topic_reopened import ForumTopicReopened
from .game import Game
from .general_forum_topic_hidden import GeneralForumTopicHidden
from .general_forum_topic_unhidden import GeneralForumTopicUnhidden
from .inline_keyboard import InlineKeyboardMarkup
from .input_media import InputMedia, MediaGroup
from .invoice import Invoice
@ -28,6 +35,7 @@ from .reply_keyboard import ReplyKeyboardMarkup, ReplyKeyboardRemove
from .sticker import Sticker
from .successful_payment import SuccessfulPayment
from .user import User
from .user_shared import UserShared
from .venue import Venue
from .video import Video
from .video_chat_ended import VideoChatEnded
@ -41,9 +49,7 @@ from .voice_chat_participants_invited import VoiceChatParticipantsInvited
from .voice_chat_scheduled import VoiceChatScheduled
from .voice_chat_started import VoiceChatStarted
from .web_app_data import WebAppData
from .forum_topic_created import ForumTopicCreated
from .forum_topic_closed import ForumTopicClosed
from .forum_topic_reopened import ForumTopicReopened
from .write_access_allowed import WriteAccessAllowed
from ..utils import helper
from ..utils import markdown as md
from ..utils.text_decorations import html_decoration, markdown_decoration
@ -108,6 +114,8 @@ class Message(base.TelegramObject):
pinned_message: Message = fields.Field(base="Message")
invoice: Invoice = fields.Field(base=Invoice)
successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment)
user_shared: UserShared = fields.Field(base=UserShared)
chat_shared: ChatShared = fields.Field(base=ChatShared)
connected_website: base.String = fields.Field()
passport_data: PassportData = fields.Field(base=PassportData)
proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered)
@ -124,6 +132,11 @@ class Message(base.TelegramObject):
video_chat_started: VideoChatStarted = fields.Field(base=VideoChatStarted)
video_chat_ended: VideoChatEnded = fields.Field(base=VideoChatEnded)
video_chat_participants_invited: VideoChatParticipantsInvited = fields.Field(base=VideoChatParticipantsInvited)
forum_topic_edited: ForumTopicEdited = fields.Field(base=ForumTopicEdited)
general_forum_topic_hidden: GeneralForumTopicHidden = fields.Field(base=GeneralForumTopicHidden)
general_forum_topic_unhidden: GeneralForumTopicUnhidden = fields.Field(base=GeneralForumTopicUnhidden)
write_access_allowed: WriteAccessAllowed = fields.Field(base=WriteAccessAllowed)
has_media_spoiler: base.Boolean = fields.Field()
@property
@functools.lru_cache()
@ -212,6 +225,18 @@ class Message(base.TelegramObject):
return ContentType.VIDEO_CHAT_ENDED
if self.video_chat_participants_invited:
return ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED
if self.forum_topic_edited:
return ContentType.FORUM_TOPIC_EDITED
if self.general_forum_topic_hidden:
return ContentType.GENERAL_FORUM_TOPIC_HIDDEN
if self.general_forum_topic_unhidden:
return ContentType.GENERAL_FORUM_TOPIC_UNHIDDEN
if self.write_access_allowed:
return ContentType.WRITE_ACCESS_ALLOWED
if self.chat_shared:
return ContentType.CHAT_SHARED
if self.user_shared:
return ContentType.USER_SHARED
return ContentType.UNKNOWN
@ -1578,7 +1603,7 @@ class Message(base.TelegramObject):
async def answer_chat_action(
self,
action: base.String,
action: base.String, message_thread_id: typing.Optional[base.Integer] = None
) -> base.Boolean:
"""
Use this method when you need to tell the user that something is happening on the bot's side.
@ -1592,6 +1617,8 @@ class Message(base.TelegramObject):
:param action: Type of action to broadcast
:type action: :obj:`base.String`
:param message_thread_id: Unique identifier for the target message thread; supergroups only
:type message_thread_id: :obj:`typing.Optional[base.Integer]`
:return: Returns True on success
:rtype: :obj:`base.Boolean`
"""
@ -3245,9 +3272,9 @@ class Message(base.TelegramObject):
reply_to_message_id: typing.Optional[base.Integer] = None,
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
reply_markup: typing.Union[InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply, None] = None,
) -> MessageId:
return await self.bot.copy_message(
chat_id=chat_id,
@ -3343,6 +3370,12 @@ class ContentType(helper.Helper):
VIDEO_CHAT_STARTED = helper.Item() # video_chat_started
VIDEO_CHAT_ENDED = helper.Item() # video_chat_ended
VIDEO_CHAT_PARTICIPANTS_INVITED = helper.Item() # video_chat_participants_invited
FORUM_TOPIC_EDITED = helper.Item() # forum_topic_edited
GENERAL_FORUM_TOPIC_HIDDEN = helper.Item() # general_forum_topic_hidden
GENERAL_FORUM_TOPIC_UNHIDDEN = helper.Item() # general_forum_topic_unhidden
WRITE_ACCESS_ALLOWED = helper.Item() # write_access_allowed
CHAT_SHARED = helper.Item() # chat_shared
USER_SHARED = helper.Item() # user_shared
UNKNOWN = helper.Item() # unknown
ANY = helper.Item() # any
@ -3409,14 +3442,20 @@ class ContentTypes(helper.Helper):
DELETE_CHAT_PHOTO = helper.ListItem() # delete_chat_photo
GROUP_CHAT_CREATED = helper.ListItem() # group_chat_created
PASSPORT_DATA = helper.ListItem() # passport_data
WEB_APP_DATA = helper.Item() # web_app_data
WEB_APP_DATA = helper.ListItem() # web_app_data
FORUM_TOPIC_CREATED = helper.ListItem() # forum_topic_created
FORUM_TOPIC_CLOSED = helper.ListItem() # forum_topic_closed
FORUM_TOPIC_REOPENED = helper.ListItem() # forum_topic_reopened
VIDEO_CHAT_SCHEDULED = helper.Item() # video_chat_scheduled
VIDEO_CHAT_STARTED = helper.Item() # video_chat_started
VIDEO_CHAT_ENDED = helper.Item() # video_chat_ended
VIDEO_CHAT_PARTICIPANTS_INVITED = helper.Item() # video_chat_participants_invited
VIDEO_CHAT_SCHEDULED = helper.ListItem() # video_chat_scheduled
VIDEO_CHAT_STARTED = helper.ListItem() # video_chat_started
VIDEO_CHAT_ENDED = helper.ListItem() # video_chat_ended
VIDEO_CHAT_PARTICIPANTS_INVITED = helper.ListItem() # video_chat_participants_invited
FORUM_TOPIC_EDITED = helper.ListItem() # forum_topic_edited
GENERAL_FORUM_TOPIC_HIDDEN = helper.ListItem() # general_forum_topic_hidden
GENERAL_FORUM_TOPIC_UNHIDDEN = helper.ListItem() # general_forum_topic_unhidden
WRITE_ACCESS_ALLOWED = helper.ListItem() # write_access_allowed
CHAT_SHARED = helper.ListItem() # chat_shared
USER_SHARED = helper.ListItem() # user_shared
UNKNOWN = helper.ListItem() # unknown
ANY = helper.ListItem() # any

View file

@ -2,6 +2,7 @@ import typing
from . import base
from . import fields
from .chat_administrator_rights import ChatAdministratorRights
from .web_app_info import WebAppInfo
@ -30,6 +31,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
one_time_keyboard: base.Boolean = fields.Field()
input_field_placeholder: base.String = fields.Field()
selective: base.Boolean = fields.Field()
is_persistent: base.Boolean = fields.Field()
def __init__(self, keyboard: 'typing.List[typing.List[KeyboardButton]]' = None,
resize_keyboard: base.Boolean = None,
@ -37,6 +39,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
input_field_placeholder: base.String = None,
selective: base.Boolean = None,
row_width: base.Integer = 3,
is_persistent: base.Boolean = None,
conf=None):
if conf is None:
conf = {}
@ -46,6 +49,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
one_time_keyboard=one_time_keyboard,
input_field_placeholder=input_field_placeholder,
selective=selective,
is_persistent=is_persistent,
conf={'row_width': row_width, **conf},
)
@ -102,6 +106,76 @@ class ReplyKeyboardMarkup(base.TelegramObject):
return self
class KeyboardButtonRequestUser(base.TelegramObject):
"""
This object defines the criteria used to request a suitable user.
The identifier of the selected user will be shared with the bot when
the corresponding button is pressed.
https://core.telegram.org/bots/api#keyboardbuttonrequestuser
"""
request_id: base.Integer = fields.Field()
user_is_bot: base.Boolean = fields.Field()
user_is_premium: base.Boolean = fields.Field()
def __init__(
self,
request_id: base.Integer,
user_is_bot: typing.Optional[base.Boolean] = None,
user_is_premium: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
request_id=request_id,
user_is_bot=user_is_bot,
user_is_premium=user_is_premium,
**kwargs,
)
class KeyboardButtonRequestChat(base.TelegramObject):
"""
This object defines the criteria used to request a suitable chat.
The identifier of the selected chat will be shared with the bot when
the corresponding button is pressed.
https://core.telegram.org/bots/api#keyboardbuttonrequestchat
"""
request_id: base.Integer = fields.Field()
chat_is_channel: base.Boolean = fields.Field()
chat_is_forum: base.Boolean = fields.Field()
chat_has_username: base.Boolean = fields.Field()
chat_is_created: base.Boolean = fields.Field()
user_administrator_rights: ChatAdministratorRights = fields.Field()
bot_administrator_rights: ChatAdministratorRights = fields.Field()
bot_is_member: base.Boolean = fields.Field()
def __init__(
self,
request_id: base.Integer,
chat_is_channel: base.Boolean,
chat_is_forum: typing.Optional[base.Boolean] = None,
chat_has_username: typing.Optional[base.Boolean] = None,
chat_is_created: typing.Optional[base.Boolean] = None,
user_administrator_rights: typing.Optional[ChatAdministratorRights] = None,
bot_administrator_rights: typing.Optional[ChatAdministratorRights] = None,
bot_is_member: typing.Optional[base.Boolean] = None,
**kwargs,
):
super().__init__(
request_id=request_id,
chat_is_channel=chat_is_channel,
chat_is_forum=chat_is_forum,
chat_has_username=chat_has_username,
chat_is_created=chat_is_created,
user_administrator_rights=user_administrator_rights,
bot_administrator_rights=bot_administrator_rights,
bot_is_member=bot_is_member,
**kwargs,
)
class KeyboardButton(base.TelegramObject):
"""
This object represents one button of the reply keyboard.
@ -115,18 +189,24 @@ class KeyboardButton(base.TelegramObject):
https://core.telegram.org/bots/api#keyboardbutton
"""
text: base.String = fields.Field()
request_user: KeyboardButtonRequestUser = fields.Field()
request_chat: KeyboardButtonRequestChat = fields.Field()
request_contact: base.Boolean = fields.Field()
request_location: base.Boolean = fields.Field()
request_poll: KeyboardButtonPollType = fields.Field()
web_app: WebAppInfo = fields.Field(base=WebAppInfo)
def __init__(self, text: base.String,
request_user: typing.Optional[KeyboardButtonRequestUser] = None,
request_chat: typing.Optional[KeyboardButtonRequestChat] = None,
request_contact: base.Boolean = None,
request_location: base.Boolean = None,
request_poll: KeyboardButtonPollType = None,
web_app: WebAppInfo = None,
**kwargs):
super(KeyboardButton, self).__init__(text=text,
request_user=request_user,
request_chat=request_chat,
request_contact=request_contact,
request_location=request_location,
request_poll=request_poll,
@ -134,6 +214,7 @@ class KeyboardButton(base.TelegramObject):
**kwargs)
class ReplyKeyboardRemove(base.TelegramObject):
"""
Upon receiving a message with this object, Telegram clients will remove the current custom keyboard

View file

@ -0,0 +1,12 @@
from . import base, fields
class UserShared(base.TelegramObject):
"""
This object contains information about the user whose identifier was
shared with the bot using a KeyboardButtonRequestUser button.
https://core.telegram.org/bots/api#usershared
"""
request_id: base.Integer = fields.Field()
user_id: base.Integer = fields.Field()

View file

@ -0,0 +1,9 @@
from . import base
class WriteAccessAllowed(base.TelegramObject):
"""
This object represents a service message about a user allowing a bot added to the attachment menu to write messages. Currently holds no information.
https://core.telegram.org/bots/api#writeaccessallowed
"""

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.3-blue.svg?style=flat-square&logo=telegram
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.4-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API

View file

@ -20,6 +20,7 @@ def pytest_addoption(parser):
default=None,
help="run tests which require redis connection",
)
parser.addini("asyncio_mode", "", default='auto')
def pytest_configure(config):

View file

@ -1,89 +0,0 @@
import aioredis
import pytest
import pytest_asyncio
from pytest_lazyfixture import lazy_fixture
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.contrib.fsm_storage.redis import RedisStorage, RedisStorage2
@pytest_asyncio.fixture()
@pytest.mark.redis
async def redis_store(redis_options):
if int(aioredis.__version__.split(".")[0]) == 2:
pytest.skip('aioredis v2 is not supported.')
return
s = RedisStorage(**redis_options)
try:
yield s
finally:
conn = await s.redis()
await conn.execute('FLUSHDB')
await s.close()
await s.wait_closed()
@pytest_asyncio.fixture()
@pytest.mark.redis
async def redis_store2(redis_options):
s = RedisStorage2(**redis_options)
try:
yield s
finally:
conn = await s.redis()
await conn.flushdb()
await s.close()
await s.wait_closed()
@pytest_asyncio.fixture()
async def memory_store():
yield MemoryStorage()
@pytest.mark.parametrize(
"store", [
lazy_fixture('redis_store'),
lazy_fixture('redis_store2'),
lazy_fixture('memory_store'),
]
)
class TestStorage:
@pytest.mark.asyncio
async def test_set_get(self, store):
assert await store.get_data(chat='1234') == {}
await store.set_data(chat='1234', data={'foo': 'bar'})
assert await store.get_data(chat='1234') == {'foo': 'bar'}
@pytest.mark.asyncio
async def test_reset(self, store):
await store.set_data(chat='1234', data={'foo': 'bar'})
await store.reset_data(chat='1234')
assert await store.get_data(chat='1234') == {}
@pytest.mark.asyncio
async def test_reset_empty(self, store):
await store.reset_data(chat='1234')
assert await store.get_data(chat='1234') == {}
@pytest.mark.parametrize(
"store", [
lazy_fixture('redis_store'),
lazy_fixture('redis_store2'),
]
)
class TestRedisStorage2:
@pytest.mark.asyncio
async def test_close_and_open_connection(self, store):
await store.set_data(chat='1234', data={'foo': 'bar'})
assert await store.get_data(chat='1234') == {'foo': 'bar'}
pool_id = id(store._redis)
await store.close()
await store.wait_closed()
# new pool will be open at this point
assert await store.get_data(chat='1234') == {
'foo': 'bar',
}
assert id(store._redis) != pool_id

View file

@ -3,8 +3,6 @@ import pytest
from aiogram import Bot, types
from . import BOT_ID, FakeTelegram
pytestmark = pytest.mark.asyncio
async def test_get_me(bot: Bot):
""" getMe method test """

View file

@ -13,8 +13,6 @@ from aiogram.utils.json import json
from tests import TOKEN
from tests.types.dataset import FILE
pytestmark = pytest.mark.asyncio
@pytest_asyncio.fixture(name='bot')
async def bot_fixture():

View file

@ -0,0 +1,157 @@
import aioredis
import pytest
import pytest_asyncio
from pytest_lazyfixture import lazy_fixture
from redis.asyncio.connection import Connection, ConnectionPool
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.contrib.fsm_storage.redis import RedisStorage, RedisStorage2
@pytest_asyncio.fixture()
@pytest.mark.redis
async def redis_store(redis_options):
if int(aioredis.__version__.split(".")[0]) == 2:
pytest.skip('aioredis v2 is not supported.')
return
s = RedisStorage(**redis_options)
try:
yield s
finally:
conn = await s.redis()
await conn.execute('FLUSHDB')
await s.close()
await s.wait_closed()
@pytest_asyncio.fixture()
@pytest.mark.redis
async def redis_store2(redis_options):
s = RedisStorage2(**redis_options)
try:
yield s
finally:
conn = await s.redis()
await conn.flushdb()
await s.close()
await s.wait_closed()
@pytest_asyncio.fixture()
async def memory_store():
yield MemoryStorage()
@pytest.mark.parametrize(
"store", [
lazy_fixture('redis_store'),
lazy_fixture('redis_store2'),
lazy_fixture('memory_store'),
]
)
class TestStorage:
async def test_set_get(self, store):
assert await store.get_data(chat='1234') == {}
await store.set_data(chat='1234', data={'foo': 'bar'})
assert await store.get_data(chat='1234') == {'foo': 'bar'}
async def test_reset(self, store):
await store.set_data(chat='1234', data={'foo': 'bar'})
await store.reset_data(chat='1234')
assert await store.get_data(chat='1234') == {}
async def test_reset_empty(self, store):
await store.reset_data(chat='1234')
assert await store.get_data(chat='1234') == {}
@pytest.mark.parametrize(
"store", [
lazy_fixture('redis_store'),
lazy_fixture('redis_store2'),
]
)
class TestRedisStorage2:
async def test_close_and_open_connection(self, store: RedisStorage2):
await store.set_data(chat='1234', data={'foo': 'bar'})
assert await store.get_data(chat='1234') == {'foo': 'bar'}
await store.close()
await store.wait_closed()
pool: ConnectionPool = store._redis.connection_pool
# noinspection PyUnresolvedReferences
assert not pool._in_use_connections
# noinspection PyUnresolvedReferences
if pool._available_connections:
# noinspection PyUnresolvedReferences
connection: Connection = pool._available_connections[0]
assert connection.is_connected is False
@pytest.mark.parametrize(
"chat_id,user_id,state",
[
[12345, 54321, "foo"],
[12345, 54321, None],
[12345, None, "foo"],
[None, 54321, "foo"],
],
)
async def test_set_get_state(self, chat_id, user_id, state, store):
await store.reset_state(chat=chat_id, user=user_id, with_data=False)
await store.set_state(chat=chat_id, user=user_id, state=state)
s = await store.get_state(chat=chat_id, user=user_id)
assert s == state
@pytest.mark.parametrize(
"chat_id,user_id,data,new_data",
[
[12345, 54321, {"foo": "bar"}, {"bar": "foo"}],
[12345, 54321, None, None],
[12345, 54321, {"foo": "bar"}, None],
[12345, 54321, None, {"bar": "foo"}],
[12345, None, {"foo": "bar"}, {"bar": "foo"}],
[None, 54321, {"foo": "bar"}, {"bar": "foo"}],
],
)
async def test_set_get_update_data(self, chat_id, user_id, data, new_data, store):
await store.reset_state(chat=chat_id, user=user_id, with_data=True)
await store.set_data(chat=chat_id, user=user_id, data=data)
d = await store.get_data(chat=chat_id, user=user_id)
assert d == (data or {})
await store.update_data(chat=chat_id, user=user_id, data=new_data)
d = await store.get_data(chat=chat_id, user=user_id)
updated_data = (data or {})
updated_data.update(new_data or {})
assert d == updated_data
async def test_has_bucket(self, store):
assert store.has_bucket()
@pytest.mark.parametrize(
"chat_id,user_id,data,new_data",
[
[12345, 54321, {"foo": "bar"}, {"bar": "foo"}],
[12345, 54321, None, None],
[12345, 54321, {"foo": "bar"}, None],
[12345, 54321, None, {"bar": "foo"}],
[12345, None, {"foo": "bar"}, {"bar": "foo"}],
[None, 54321, {"foo": "bar"}, {"bar": "foo"}],
],
)
async def test_set_get_update_bucket(self, chat_id, user_id, data, new_data, store):
await store.reset_state(chat=chat_id, user=user_id, with_data=True)
await store.set_bucket(chat=chat_id, user=user_id, bucket=data)
d = await store.get_bucket(chat=chat_id, user=user_id)
assert d == (data or {})
await store.update_bucket(chat=chat_id, user=user_id, bucket=new_data)
d = await store.get_bucket(chat=chat_id, user=user_id)
updated_bucket = (data or {})
updated_bucket.update(new_data or {})
assert d == updated_bucket

View file

@ -2,8 +2,6 @@ import pytest
from aiogram import Dispatcher, Bot
pytestmark = pytest.mark.asyncio
class TestDispatcherInit:
async def test_successful_init(self, bot):

View file

@ -1,10 +1,7 @@
from unittest.mock import AsyncMock
import pytest
from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware
pytestmark = pytest.mark.asyncio
async def test_no_skip():
class Middleware(LifetimeControllerMiddleware):

View file

@ -6,9 +6,6 @@ import pytest
from aiogram.dispatcher.filters import Text, CommandStart
from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
# enable asyncio mode
pytestmark = pytest.mark.asyncio
def data_sample_1():
return [

View file

@ -4,8 +4,6 @@ import pytest_asyncio
from aiogram import Bot, types
from . import FakeTelegram
pytestmark = pytest.mark.asyncio
@pytest_asyncio.fixture(name="message")
async def message_fixture(bot: Bot):

View file

@ -8,8 +8,6 @@ from aiogram.utils.deep_linking import (
)
from tests.types import dataset
# enable asyncio mode
pytestmark = pytest.mark.asyncio
PAYLOADS = [
'foo',

View file

@ -1,10 +1,7 @@
import pytest
from aiogram import Bot, types
from .dataset import CHAT, FULL_CHAT
from .. import FakeTelegram
pytestmark = pytest.mark.asyncio
chat = types.Chat(**CHAT)

View file

@ -14,8 +14,6 @@ from aiogram.utils.json import json
from tests import TOKEN
from tests.types.dataset import FILE
pytestmark = pytest.mark.asyncio
@pytest_asyncio.fixture(name='bot')
async def bot_fixture():