mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x'
This commit is contained in:
commit
88e3d0503d
26 changed files with 417 additions and 150 deletions
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -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-5.1-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = (
|
|||
'utils',
|
||||
)
|
||||
|
||||
__version__ = '2.12.1'
|
||||
__api_version__ = '5.1'
|
||||
__version__ = '2.13'
|
||||
__api_version__ = '5.2'
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 5.1
|
||||
List is updated to Bot API 5.2
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
|
|||
|
|
@ -1484,19 +1484,36 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
action: base.String) -> 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
|
||||
(when a message arrives from your bot, Telegram clients clear its typing status).
|
||||
Use this method when you need to tell the user that something is
|
||||
happening on the bot's side. The status is set for 5 seconds or
|
||||
less (when a message arrives from your bot, Telegram clients
|
||||
clear its typing status). Returns True on success.
|
||||
|
||||
We only recommend using this method when a response from the bot will take
|
||||
a noticeable amount of time to arrive.
|
||||
Example: The ImageBot needs some time to process a request and
|
||||
upload the image. Instead of sending a text message along the
|
||||
lines of “Retrieving image, please wait…”, the bot may use
|
||||
sendChatAction with action = upload_photo. The user will see a
|
||||
“sending photo” status for the bot.
|
||||
|
||||
We only recommend using this method when a response from the bot
|
||||
will take a noticeable amount of time to arrive.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendchataction
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param action: Type of action to broadcast
|
||||
|
||||
:param action: Type of action to broadcast. Choose one,
|
||||
depending on what the user is about to receive: `typing` for
|
||||
text messages, `upload_photo` for photos, `record_video` or
|
||||
`upload_video` for videos, `record_voice` or `upload_voice`
|
||||
for voice notes, `upload_document` for general files,
|
||||
`find_location` for location data, `record_video_note` or
|
||||
`upload_video_note` for video notes.
|
||||
:type action: :obj:`base.String`
|
||||
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
|
|
@ -1834,7 +1851,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date = prepare_arg(expire_date)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def edit_chat_invite_link(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
|
|
@ -1870,7 +1888,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date = prepare_arg(expire_date)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def revoke_chat_invite_link(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
|
|
@ -1891,7 +1910,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
photo: base.InputFile) -> base.Boolean:
|
||||
|
|
@ -2780,10 +2800,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
# === Payments ===
|
||||
# https://core.telegram.org/bots/api#payments
|
||||
|
||||
async def send_invoice(self, chat_id: base.Integer, title: base.String,
|
||||
description: base.String, payload: base.String,
|
||||
provider_token: base.String, start_parameter: base.String,
|
||||
currency: base.String, prices: typing.List[types.LabeledPrice],
|
||||
async def send_invoice(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
title: base.String,
|
||||
description: base.String,
|
||||
payload: base.String,
|
||||
provider_token: base.String,
|
||||
currency: base.String,
|
||||
prices: typing.List[types.LabeledPrice],
|
||||
max_tip_amount: typing.Optional[base.Integer] = None,
|
||||
suggested_tip_amounts: typing.Optional[
|
||||
typing.List[base.Integer]
|
||||
] = None,
|
||||
start_parameter: typing.Optional[base.String] = None,
|
||||
provider_data: typing.Optional[typing.Dict] = None,
|
||||
photo_url: typing.Optional[base.String] = None,
|
||||
photo_size: typing.Optional[base.Integer] = None,
|
||||
|
|
@ -2799,14 +2828,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None) -> types.Message:
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send invoices.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendinvoice
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat
|
||||
:type chat_id: :obj:`base.Integer`
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
|
||||
:param title: Product name, 1-32 characters
|
||||
:type title: :obj:`base.String`
|
||||
|
|
@ -2821,10 +2853,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param provider_token: Payments provider token, obtained via Botfather
|
||||
:type provider_token: :obj:`base.String`
|
||||
|
||||
:param start_parameter: Unique deep-linking parameter that can be used to generate this
|
||||
invoice when used as a start parameter
|
||||
:type start_parameter: :obj:`base.String`
|
||||
|
||||
:param currency: Three-letter ISO 4217 currency code, see more on currencies
|
||||
:type currency: :obj:`base.String`
|
||||
|
||||
|
|
@ -2832,6 +2860,32 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
|
||||
:type prices: :obj:`typing.List[types.LabeledPrice]`
|
||||
|
||||
:param max_tip_amount: The maximum accepted amount for tips in
|
||||
the smallest units of the currency (integer, not
|
||||
float/double). For example, for a maximum tip of US$ 1.45
|
||||
pass max_tip_amount = 145. See the exp parameter in
|
||||
currencies.json, it shows the number of digits past the
|
||||
decimal point for each currency (2 for the majority of
|
||||
currencies). Defaults to 0
|
||||
:type max_tip_amount: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
:param suggested_tip_amounts: A JSON-serialized array of suggested
|
||||
amounts of tips in the smallest units of the currency
|
||||
(integer, not float/double). At most 4 suggested tip amounts
|
||||
can be specified. The suggested tip amounts must be
|
||||
positive, passed in a strictly increased order and must not
|
||||
exceed max_tip_amount.
|
||||
:type suggested_tip_amounts: :obj:`typing.Optional[typing.List[base.Integer]]`
|
||||
|
||||
:param start_parameter: Unique deep-linking parameter. If left
|
||||
empty, forwarded copies of the sent message will have a Pay
|
||||
button, allowing multiple users to pay directly from the
|
||||
forwarded message, using the same invoice. If non-empty,
|
||||
forwarded copies of the sent message will have a URL button
|
||||
with a deep link to the bot (instead of a Pay button), with
|
||||
the value used as the start parameter
|
||||
:type start_parameter: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param provider_data: JSON-encoded data about the invoice, which will be shared with the payment provider
|
||||
:type provider_data: :obj:`typing.Optional[typing.Dict]`
|
||||
|
||||
|
|
@ -2887,6 +2941,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices])
|
||||
reply_markup = prepare_arg(reply_markup)
|
||||
provider_data = prepare_arg(provider_data)
|
||||
payload_ = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SEND_INVOICE, payload_)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class MemoryStorage(BaseStorage):
|
|||
user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
return self.data[chat][user]['state']
|
||||
return self.data[chat][user].get("state", self.resolve_state(default))
|
||||
|
||||
async def get_data(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
@ -58,7 +58,7 @@ class MemoryStorage(BaseStorage):
|
|||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.AnyStr = None):
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
self.data[chat][user]['state'] = state
|
||||
self.data[chat][user]['state'] = self.resolve_state(state)
|
||||
|
||||
async def set_data(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class MongoStorage(BaseStorage):
|
|||
try:
|
||||
self._mongo = AsyncIOMotorClient(self._uri)
|
||||
except pymongo.errors.ConfigurationError as e:
|
||||
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
|
||||
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
|
||||
import logging
|
||||
logger = logging.getLogger("aiogram")
|
||||
logger.warning("Run `pip install dnspython==1.16.0` in order to fix ConfigurationError. More information: https://github.com/mongodb/mongo-python-driver/pull/423#issuecomment-528998245")
|
||||
|
|
@ -114,7 +114,9 @@ class MongoStorage(BaseStorage):
|
|||
async def wait_closed(self):
|
||||
return True
|
||||
|
||||
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: Union[str, int, None] = None,
|
||||
user: Union[str, int, None] = None,
|
||||
state: Optional[AnyStr] = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
|
@ -122,8 +124,11 @@ class MongoStorage(BaseStorage):
|
|||
if state is None:
|
||||
await db[STATE].delete_one(filter={'chat': chat, 'user': user})
|
||||
else:
|
||||
await db[STATE].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'state': state}}, upsert=True)
|
||||
await db[STATE].update_one(
|
||||
filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'state': self.resolve_state(state)}},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[str] = None) -> Optional[str]:
|
||||
|
|
@ -131,7 +136,7 @@ class MongoStorage(BaseStorage):
|
|||
db = await self.get_db()
|
||||
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
|
||||
|
||||
return result.get('state') if result else default
|
||||
return result.get('state') if result else self.resolve_state(default)
|
||||
|
||||
async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
data: Dict = None):
|
||||
|
|
|
|||
|
|
@ -118,16 +118,19 @@ class RedisStorage(BaseStorage):
|
|||
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
return record['state']
|
||||
return record.get('state', 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[str] = None) -> typing.Dict:
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
return record['data']
|
||||
|
||||
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.Optional[typing.AnyStr] = None):
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
state = self.resolve_state(state)
|
||||
await self.set_record(chat=chat, user=user, state=state, data=record['data'])
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
|
|
@ -274,7 +277,7 @@ class RedisStorage2(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_KEY)
|
||||
redis = await self.redis()
|
||||
return await redis.get(key, encoding='utf8') or None
|
||||
return await redis.get(key, encoding='utf8') 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:
|
||||
|
|
@ -294,7 +297,7 @@ class RedisStorage2(BaseStorage):
|
|||
if state is None:
|
||||
await redis.delete(key)
|
||||
else:
|
||||
await redis.set(key, state, expire=self._state_ttl)
|
||||
await redis.set(key, self.resolve_state(state), expire=self._state_ttl)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ class RethinkDBStorage(BaseStorage):
|
|||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
chat, user = map(str, self.check_address(chat=chat, user=user))
|
||||
async with self.connection() as conn:
|
||||
return await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn)
|
||||
return await r.table(self._table).get(chat)[user]['state'].default(
|
||||
self.resolve_state(default) or None
|
||||
).run(conn)
|
||||
|
||||
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Dict:
|
||||
|
|
@ -103,11 +105,16 @@ class RethinkDBStorage(BaseStorage):
|
|||
async with self.connection() as conn:
|
||||
return await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn)
|
||||
|
||||
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.Optional[typing.AnyStr] = None):
|
||||
chat, user = map(str, self.check_address(chat=chat, user=user))
|
||||
async with self.connection() as conn:
|
||||
await r.table(self._table).insert({'id': chat, user: {'state': state}}, conflict="update").run(conn)
|
||||
await r.table(self._table).insert(
|
||||
{'id': chat, user: {'state': self.resolve_state(state)}},
|
||||
conflict="update",
|
||||
).run(conn)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import gettext
|
||||
import os
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict, Tuple, Optional
|
||||
|
||||
from babel import Locale
|
||||
from babel.support import LazyProxy
|
||||
|
|
@ -119,22 +119,24 @@ class I18nMiddleware(BaseMiddleware):
|
|||
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> Optional[str]:
|
||||
"""
|
||||
User locale getter
|
||||
You can override the method if you want to use different way of getting user language.
|
||||
You can override the method if you want to use different way of
|
||||
getting user language.
|
||||
|
||||
:param action: event name
|
||||
:param args: event arguments
|
||||
:return: locale name
|
||||
:return: locale name or None
|
||||
"""
|
||||
user: types.User = types.User.get_current()
|
||||
locale: Locale = user.locale
|
||||
user: Optional[types.User] = types.User.get_current()
|
||||
locale: Optional[Locale] = user.locale if user else None
|
||||
|
||||
if locale:
|
||||
*_, data = args
|
||||
language = data['locale'] = locale.language
|
||||
return language
|
||||
return None
|
||||
|
||||
async def trigger(self, action, args):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -340,7 +340,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
limit=None,
|
||||
reset_webhook=None,
|
||||
fast: typing.Optional[bool] = True,
|
||||
error_sleep: int = 5):
|
||||
error_sleep: int = 5,
|
||||
allowed_updates: typing.Optional[typing.List[str]] = None):
|
||||
"""
|
||||
Start long-polling
|
||||
|
||||
|
|
@ -349,6 +350,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
:param limit:
|
||||
:param reset_webhook:
|
||||
:param fast:
|
||||
:param error_sleep:
|
||||
:param allowed_updates:
|
||||
:return:
|
||||
"""
|
||||
if self._polling:
|
||||
|
|
@ -377,10 +380,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
while self._polling:
|
||||
try:
|
||||
with self.bot.request_timeout(request_timeout):
|
||||
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
|
||||
updates = await self.bot.get_updates(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
timeout=timeout,
|
||||
allowed_updates=allowed_updates
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except:
|
||||
except Exception as e:
|
||||
log.exception('Cause exception while getting updates.')
|
||||
await asyncio.sleep(error_sleep)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import typing
|
|||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterable, Optional, Union
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
|
|
@ -110,7 +110,8 @@ class Command(Filter):
|
|||
if not text:
|
||||
return False
|
||||
|
||||
full_command = text.split()[0]
|
||||
full_command, *args_list = text.split(maxsplit=1)
|
||||
args = args_list[0] if args_list else None
|
||||
prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@')
|
||||
|
||||
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
|
||||
|
|
@ -120,7 +121,7 @@ class Command(Filter):
|
|||
if (command.lower() if ignore_case else command) not in commands:
|
||||
return False
|
||||
|
||||
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention)}
|
||||
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention, args=args)}
|
||||
|
||||
@dataclass
|
||||
class CommandObj:
|
||||
|
|
|
|||
|
|
@ -40,24 +40,27 @@ class BaseStorage:
|
|||
@classmethod
|
||||
def check_address(cls, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None) -> (typing.Union[str, int], typing.Union[str, int]):
|
||||
user: typing.Union[str, int, None] = None,
|
||||
) -> (typing.Union[str, int], typing.Union[str, int]):
|
||||
"""
|
||||
In all storage's methods chat or user is always required.
|
||||
If one of them is not provided, you have to set missing value based on the provided one.
|
||||
|
||||
This method performs the check described above.
|
||||
|
||||
:param chat:
|
||||
:param user:
|
||||
:param chat: chat_id
|
||||
:param user: user_id
|
||||
:return:
|
||||
"""
|
||||
if chat is None and user is None:
|
||||
raise ValueError('`user` or `chat` parameter is required but no one is provided!')
|
||||
|
||||
if user is None and chat is not None:
|
||||
if user is None:
|
||||
user = chat
|
||||
elif user is not None and chat is None:
|
||||
|
||||
elif chat is None:
|
||||
chat = user
|
||||
|
||||
return chat, user
|
||||
|
||||
async def get_state(self, *,
|
||||
|
|
@ -270,6 +273,21 @@ class BaseStorage:
|
|||
"""
|
||||
await self.set_data(chat=chat, user=user, data={})
|
||||
|
||||
@staticmethod
|
||||
def resolve_state(value):
|
||||
from .filters.state import State
|
||||
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
if isinstance(value, State):
|
||||
return value.state
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
class FSMContext:
|
||||
def __init__(self, storage, chat, user):
|
||||
|
|
@ -279,20 +297,8 @@ class FSMContext:
|
|||
def proxy(self):
|
||||
return FSMContextProxy(self)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_state(value):
|
||||
from .filters.state import State
|
||||
|
||||
if value is None:
|
||||
return
|
||||
elif isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, State):
|
||||
return value.state
|
||||
return str(value)
|
||||
|
||||
async def get_state(self, default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
return await self.storage.get_state(chat=self.chat, user=self.user, default=self._resolve_state(default))
|
||||
return await self.storage.get_state(chat=self.chat, user=self.user, default=default)
|
||||
|
||||
async def get_data(self, default: typing.Optional[str] = None) -> typing.Dict:
|
||||
return await self.storage.get_data(chat=self.chat, user=self.user, default=default)
|
||||
|
|
@ -301,7 +307,7 @@ class FSMContext:
|
|||
await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs)
|
||||
|
||||
async def set_state(self, state: typing.Optional[typing.AnyStr] = None):
|
||||
await self.storage.set_state(chat=self.chat, user=self.user, state=self._resolve_state(state))
|
||||
await self.storage.set_state(chat=self.chat, user=self.user, state=state)
|
||||
|
||||
async def set_data(self, data: typing.Dict = None):
|
||||
await self.storage.set_data(chat=self.chat, user=self.user, data=data)
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for
|
||||
Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data.
|
||||
:param caption: String (Optional) - Photo caption (may also be used when resending photos by file_id),
|
||||
0-200 characters
|
||||
0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive
|
||||
a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -672,7 +672,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL
|
||||
as a String for Telegram to get an audio file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Audio caption, 0-200 characters
|
||||
:param caption: String (Optional) - Audio caption, 0-1024 characters after entities parsing
|
||||
:param duration: Integer (Optional) - Duration of the audio in seconds
|
||||
:param performer: String (Optional) - Performer
|
||||
:param title: String (Optional) - Track name
|
||||
|
|
@ -731,7 +731,7 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
as a String for Telegram to get a file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Document caption
|
||||
(may also be used when resending documents by file_id), 0-200 characters
|
||||
(may also be used when resending documents by file_id), 0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -788,7 +788,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
:param width: Integer (Optional) - Video width
|
||||
:param height: Integer (Optional) - Video height
|
||||
:param caption: String (Optional) - Video caption (may also be used when resending videos by file_id),
|
||||
0-200 characters
|
||||
0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -845,7 +845,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
to send a file that exists on the Telegram servers (recommended), pass an HTTP URL
|
||||
as a String for Telegram to get a file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Voice message caption, 0-200 characters
|
||||
:param caption: String (Optional) - Voice message caption, 0-1024 characters after entities parsing
|
||||
:param duration: Integer (Optional) - Duration of the voice message in seconds
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from .input_file import InputFile
|
|||
from .input_media import InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, \
|
||||
InputMediaVideo, MediaGroup
|
||||
from .input_message_content import InputContactMessageContent, InputLocationMessageContent, InputMessageContent, \
|
||||
InputTextMessageContent, InputVenueMessageContent
|
||||
InputTextMessageContent, InputVenueMessageContent, InputInvoiceMessageContent
|
||||
from .invoice import Invoice
|
||||
from .labeled_price import LabeledPrice
|
||||
from .location import Location
|
||||
|
|
@ -72,6 +72,7 @@ from .video_note import VideoNote
|
|||
from .voice import Voice
|
||||
from .voice_chat_ended import VoiceChatEnded
|
||||
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
|
||||
from .voice_chat_scheduled import VoiceChatScheduled
|
||||
from .voice_chat_started import VoiceChatStarted
|
||||
from .webhook_info import WebhookInfo
|
||||
|
||||
|
|
@ -131,6 +132,7 @@ __all__ = (
|
|||
'InlineQueryResultVideo',
|
||||
'InlineQueryResultVoice',
|
||||
'InputContactMessageContent',
|
||||
'InputInvoiceMessageContent',
|
||||
'InputFile',
|
||||
'InputLocationMessageContent',
|
||||
'InputMedia',
|
||||
|
|
@ -191,6 +193,7 @@ __all__ = (
|
|||
'Voice',
|
||||
'VoiceChatEnded',
|
||||
'VoiceChatParticipantsInvited',
|
||||
'VoiceChatScheduled',
|
||||
'VoiceChatStarted',
|
||||
'WebhookInfo',
|
||||
'base',
|
||||
|
|
|
|||
|
|
@ -732,6 +732,8 @@ class ChatActions(helper.Helper):
|
|||
UPLOAD_VIDEO: str = helper.Item() # upload_video
|
||||
RECORD_AUDIO: str = helper.Item() # record_audio
|
||||
UPLOAD_AUDIO: str = helper.Item() # upload_audio
|
||||
RECORD_VOICE: str = helper.Item() # record_voice
|
||||
UPLOAD_VOICE: str = helper.Item() # upload_voice
|
||||
UPLOAD_DOCUMENT: str = helper.Item() # upload_document
|
||||
FIND_LOCATION: str = helper.Item() # find_location
|
||||
RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note
|
||||
|
|
@ -817,6 +819,26 @@ class ChatActions(helper.Helper):
|
|||
"""
|
||||
await cls._do(cls.UPLOAD_AUDIO, sleep)
|
||||
|
||||
@classmethod
|
||||
async def record_voice(cls, sleep=None):
|
||||
"""
|
||||
Do record voice
|
||||
|
||||
:param sleep: sleep timeout
|
||||
:return:
|
||||
"""
|
||||
await cls._do(cls.RECORD_VOICE, sleep)
|
||||
|
||||
@classmethod
|
||||
async def upload_voice(cls, sleep=None):
|
||||
"""
|
||||
Do upload voice
|
||||
|
||||
:param sleep: sleep timeout
|
||||
:return:
|
||||
"""
|
||||
await cls._do(cls.UPLOAD_VOICE, sleep)
|
||||
|
||||
@classmethod
|
||||
async def upload_document(cls, sleep=None):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ class InlineQuery(base.TelegramObject):
|
|||
"""
|
||||
id: base.String = fields.Field()
|
||||
from_user: User = fields.Field(alias='from', base=User)
|
||||
location: Location = fields.Field(base=Location)
|
||||
query: base.String = fields.Field()
|
||||
offset: base.String = fields.Field()
|
||||
chat_type: base.String = fields.Field()
|
||||
location: Location = fields.Field(base=Location)
|
||||
|
||||
async def answer(self,
|
||||
results: typing.List[InlineQueryResult],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import typing
|
|||
from . import base
|
||||
from . import fields
|
||||
from .message_entity import MessageEntity
|
||||
from .labeled_price import LabeledPrice
|
||||
from ..utils.payload import generate_payload
|
||||
|
||||
|
||||
class InputMessageContent(base.TelegramObject):
|
||||
|
|
@ -44,6 +46,66 @@ class InputContactMessageContent(InputMessageContent):
|
|||
)
|
||||
|
||||
|
||||
class InputInvoiceMessageContent(InputMessageContent):
|
||||
"""
|
||||
Represents the content of an invoice message to be sent as the
|
||||
result of an inline query.
|
||||
|
||||
https://core.telegram.org/bots/api#inputinvoicemessagecontent
|
||||
"""
|
||||
|
||||
title: base.String = fields.Field()
|
||||
description: base.String = fields.Field()
|
||||
payload: base.String = fields.Field()
|
||||
provider_token: base.String = fields.Field()
|
||||
currency: base.String = fields.Field()
|
||||
prices: typing.List[LabeledPrice] = fields.ListField(base=LabeledPrice)
|
||||
max_tip_amount: typing.Optional[base.Integer] = fields.Field()
|
||||
suggested_tip_amounts: typing.Optional[
|
||||
typing.List[base.Integer]
|
||||
] = fields.ListField(base=base.Integer)
|
||||
provider_data: typing.Optional[base.String] = fields.Field()
|
||||
photo_url: typing.Optional[base.String] = fields.Field()
|
||||
photo_size: typing.Optional[base.Integer] = fields.Field()
|
||||
photo_width: typing.Optional[base.Integer] = fields.Field()
|
||||
photo_height: typing.Optional[base.Integer] = fields.Field()
|
||||
need_name: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_phone_number: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_email: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_shipping_address: typing.Optional[base.Boolean] = fields.Field()
|
||||
send_phone_number_to_provider: typing.Optional[base.Boolean] = fields.Field()
|
||||
send_email_to_provider: typing.Optional[base.Boolean] = fields.Field()
|
||||
is_flexible: typing.Optional[base.Boolean] = fields.Field()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: base.String,
|
||||
description: base.String,
|
||||
payload: base.String,
|
||||
provider_token: base.String,
|
||||
currency: base.String,
|
||||
prices: typing.List[LabeledPrice] = None,
|
||||
max_tip_amount: typing.Optional[base.Integer] = None,
|
||||
suggested_tip_amounts: typing.Optional[typing.List[base.Integer]] = None,
|
||||
provider_data: typing.Optional[base.String] = None,
|
||||
photo_url: typing.Optional[base.String] = None,
|
||||
photo_size: typing.Optional[base.Integer] = None,
|
||||
photo_width: typing.Optional[base.Integer] = None,
|
||||
photo_height: typing.Optional[base.Integer] = None,
|
||||
need_name: typing.Optional[base.Boolean] = None,
|
||||
need_phone_number: typing.Optional[base.Boolean] = None,
|
||||
need_email: typing.Optional[base.Boolean] = None,
|
||||
need_shipping_address: typing.Optional[base.Boolean] = None,
|
||||
send_phone_number_to_provider: typing.Optional[base.Boolean] = None,
|
||||
send_email_to_provider: typing.Optional[base.Boolean] = None,
|
||||
is_flexible: typing.Optional[base.Boolean] = None,
|
||||
):
|
||||
if prices is None:
|
||||
prices = []
|
||||
payload = generate_payload(**locals())
|
||||
super().__init__(**payload)
|
||||
|
||||
|
||||
class InputLocationMessageContent(InputMessageContent):
|
||||
"""
|
||||
Represents the content of a location message to be sent as the result of an inline query.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from .video_note import VideoNote
|
|||
from .voice import Voice
|
||||
from .voice_chat_ended import VoiceChatEnded
|
||||
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
|
||||
from .voice_chat_scheduled import VoiceChatScheduled
|
||||
from .voice_chat_started import VoiceChatStarted
|
||||
from ..utils import helper
|
||||
from ..utils import markdown as md
|
||||
|
|
@ -98,6 +99,7 @@ class Message(base.TelegramObject):
|
|||
connected_website: base.String = fields.Field()
|
||||
passport_data: PassportData = fields.Field(base=PassportData)
|
||||
proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered)
|
||||
voice_chat_scheduled: VoiceChatScheduled = fields.Field(base=VoiceChatScheduled)
|
||||
voice_chat_started: VoiceChatStarted = fields.Field(base=VoiceChatStarted)
|
||||
voice_chat_ended: VoiceChatEnded = fields.Field(base=VoiceChatEnded)
|
||||
voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited)
|
||||
|
|
@ -166,6 +168,8 @@ class Message(base.TelegramObject):
|
|||
return ContentType.PASSPORT_DATA
|
||||
if self.proximity_alert_triggered:
|
||||
return ContentType.PROXIMITY_ALERT_TRIGGERED
|
||||
if self.voice_chat_scheduled:
|
||||
return ContentType.VOICE_CHAT_SCHEDULED
|
||||
if self.voice_chat_started:
|
||||
return ContentType.VOICE_CHAT_STARTED
|
||||
if self.voice_chat_ended:
|
||||
|
|
@ -267,7 +271,8 @@ class Message(base.TelegramObject):
|
|||
|
||||
:return: str
|
||||
"""
|
||||
if ChatType.is_private(self.chat):
|
||||
|
||||
if self.chat.type == ChatType.PRIVATE:
|
||||
raise TypeError("Invalid chat type!")
|
||||
url = "https://t.me/"
|
||||
if self.chat.username:
|
||||
|
|
@ -461,7 +466,7 @@ class Message(base.TelegramObject):
|
|||
:param audio: Audio file to send.
|
||||
:type audio: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Audio caption, 0-200 characters
|
||||
:param caption: Audio caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -738,7 +743,7 @@ class Message(base.TelegramObject):
|
|||
A thumbnail‘s width and height should not exceed 320.
|
||||
:type thumb: :obj:`typing.Union[base.InputFile, base.String, None]`
|
||||
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -817,7 +822,7 @@ class Message(base.TelegramObject):
|
|||
:param voice: Audio file to send.
|
||||
:type voice: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Voice message caption, 0-200 characters
|
||||
:param caption: Voice message caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1602,7 +1607,7 @@ class Message(base.TelegramObject):
|
|||
:param audio: Audio file to send.
|
||||
:type audio: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Audio caption, 0-200 characters
|
||||
:param caption: Audio caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1879,7 +1884,7 @@ class Message(base.TelegramObject):
|
|||
A thumbnail‘s width and height should not exceed 320.
|
||||
:type thumb: :obj:`typing.Union[base.InputFile, base.String, None]`
|
||||
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1958,7 +1963,7 @@ class Message(base.TelegramObject):
|
|||
:param voice: Audio file to send.
|
||||
:type voice: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Voice message caption, 0-200 characters
|
||||
:param caption: Voice message caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -3032,6 +3037,7 @@ class ContentType(helper.Helper):
|
|||
GROUP_CHAT_CREATED = helper.Item() # group_chat_created
|
||||
PASSPORT_DATA = helper.Item() # passport_data
|
||||
PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered
|
||||
VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled
|
||||
VOICE_CHAT_STARTED = helper.Item() # voice_chat_started
|
||||
VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended
|
||||
VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .callback_query import CallbackQuery
|
||||
|
|
@ -72,3 +74,9 @@ class AllowedUpdates(helper.Helper):
|
|||
"Use `CHOSEN_INLINE_RESULT`",
|
||||
new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@lru_cache(1)
|
||||
def default(cls):
|
||||
excluded = cls.CHAT_MEMBER + cls.MY_CHAT_MEMBER
|
||||
return list(filter(lambda item: item not in excluded, cls.all()))
|
||||
|
|
|
|||
15
aiogram/types/voice_chat_scheduled.py
Normal file
15
aiogram/types/voice_chat_scheduled.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from datetime import datetime
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .user import User
|
||||
|
||||
|
||||
class VoiceChatScheduled(base.TelegramObject):
|
||||
"""
|
||||
This object represents a service message about a voice chat scheduled in the chat.
|
||||
|
||||
https://core.telegram.org/bots/api#voicechatscheduled
|
||||
"""
|
||||
|
||||
start_date: datetime = fields.DateTimeField()
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
"""
|
||||
Deep linking
|
||||
|
||||
Telegram bots have a deep linking mechanism, that allows for passing additional
|
||||
parameters to the bot on startup. It could be a command that launches the bot — or
|
||||
an auth token to connect the user's Telegram account to their account on some
|
||||
external service.
|
||||
Telegram bots have a deep linking mechanism, that allows for passing
|
||||
additional parameters to the bot on startup. It could be a command that
|
||||
launches the bot — or an auth token to connect the user's Telegram
|
||||
account to their account on some external service.
|
||||
|
||||
You can read detailed description in the source:
|
||||
https://core.telegram.org/bots#deep-linking
|
||||
|
|
@ -16,86 +16,123 @@ Basic link example:
|
|||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link
|
||||
link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo'
|
||||
link = await get_start_link('foo')
|
||||
|
||||
# result: 'https://t.me/MyBot?start=foo'
|
||||
|
||||
Encoded link example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link, decode_payload
|
||||
link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v'
|
||||
# and decode it back:
|
||||
payload = decode_payload('Zm9v') # result: 'foo'
|
||||
from aiogram.utils.deep_linking import get_start_link
|
||||
|
||||
link = await get_start_link('foo', encode=True)
|
||||
# result: 'https://t.me/MyBot?start=Zm9v'
|
||||
|
||||
Decode it back example:
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import decode_payload
|
||||
from aiogram.types import Message
|
||||
|
||||
@dp.message_handler(commands=["start"])
|
||||
async def handler(message: Message):
|
||||
args = message.get_args()
|
||||
payload = decode_payload(args)
|
||||
await message.answer(f"Your payload: {payload}")
|
||||
|
||||
"""
|
||||
import re
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
|
||||
from ..bot import Bot
|
||||
|
||||
BAD_PATTERN = re.compile(r"[^_A-z0-9-]")
|
||||
|
||||
|
||||
async def get_start_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'start' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
Get 'start' deep link with your payload.
|
||||
|
||||
If you need to encode payload or pass special characters -
|
||||
set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('start', payload, encode)
|
||||
return await _create_link(
|
||||
link_type="start",
|
||||
payload=payload,
|
||||
encode=encode,
|
||||
)
|
||||
|
||||
|
||||
async def get_startgroup_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'startgroup' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
Get 'startgroup' deep link with your payload.
|
||||
|
||||
If you need to encode payload or pass special characters -
|
||||
set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('startgroup', payload, encode)
|
||||
return await _create_link(
|
||||
link_type="startgroup",
|
||||
payload=payload,
|
||||
encode=encode,
|
||||
)
|
||||
|
||||
|
||||
async def _create_link(link_type, payload: str, encode=False):
|
||||
"""
|
||||
Create deep link.
|
||||
|
||||
:param link_type: `start` or `startgroup`
|
||||
:param payload: any string-convertible data
|
||||
:param encode: pass True to encode the payload
|
||||
:return: deeplink
|
||||
"""
|
||||
bot = await _get_bot_user()
|
||||
payload = filter_payload(payload)
|
||||
if encode:
|
||||
payload = encode_payload(payload)
|
||||
return f'https://t.me/{bot.username}?{link_type}={payload}'
|
||||
|
||||
|
||||
def encode_payload(payload: str) -> str:
|
||||
""" Encode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64encode
|
||||
result: bytes = urlsafe_b64encode(payload.encode())
|
||||
return result.decode()
|
||||
|
||||
|
||||
def decode_payload(payload: str) -> str:
|
||||
""" Decode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64decode
|
||||
result: bytes = urlsafe_b64decode(payload + '=' * (4 - len(payload) % 4))
|
||||
return result.decode()
|
||||
|
||||
|
||||
def filter_payload(payload: str) -> str:
|
||||
""" Convert payload to text and search for not allowed symbols. """
|
||||
import re
|
||||
|
||||
# convert to string
|
||||
if not isinstance(payload, str):
|
||||
payload = str(payload)
|
||||
|
||||
# search for not allowed characters
|
||||
if re.search(r'[^_A-z0-9-]', payload):
|
||||
message = ('Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. '
|
||||
'We recommend to encode parameters with binary and other '
|
||||
'types of content.')
|
||||
if encode:
|
||||
payload = encode_payload(payload)
|
||||
|
||||
if re.search(BAD_PATTERN, payload):
|
||||
message = (
|
||||
"Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. "
|
||||
"Pass `encode=True` or encode payload manually."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return payload
|
||||
if len(payload) > 64:
|
||||
message = "Payload must be up to 64 characters long."
|
||||
raise ValueError(message)
|
||||
|
||||
return f"https://t.me/{bot.username}?{link_type}={payload}"
|
||||
|
||||
|
||||
def encode_payload(payload: str) -> str:
|
||||
"""Encode payload with URL-safe base64url."""
|
||||
payload = str(payload)
|
||||
bytes_payload: bytes = urlsafe_b64encode(payload.encode())
|
||||
str_payload = bytes_payload.decode()
|
||||
return str_payload.replace("=", "")
|
||||
|
||||
|
||||
def decode_payload(payload: str) -> str:
|
||||
"""Decode payload with URL-safe base64url."""
|
||||
payload += "=" * (4 - len(payload) % 4)
|
||||
result: bytes = urlsafe_b64decode(payload)
|
||||
return result.decode()
|
||||
|
||||
|
||||
async def _get_bot_user():
|
||||
""" Get current user of bot. """
|
||||
from ..bot import Bot
|
||||
"""Get current user of bot."""
|
||||
bot = Bot.get_current()
|
||||
return await bot.me
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import datetime
|
||||
import functools
|
||||
import secrets
|
||||
from typing import Callable, Union, Optional, Any
|
||||
from typing import Callable, Union, Optional, Any, List
|
||||
from warnings import warn
|
||||
|
||||
from aiohttp import web
|
||||
|
|
@ -23,7 +23,8 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
|
|||
|
||||
|
||||
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
|
||||
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
|
||||
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True,
|
||||
allowed_updates: Optional[List[str]] = None):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -34,11 +35,20 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
|
|||
:param on_startup:
|
||||
:param on_shutdown:
|
||||
:param timeout:
|
||||
:param relax:
|
||||
:param fast:
|
||||
:param allowed_updates:
|
||||
"""
|
||||
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
|
||||
_setup_callbacks(executor, on_startup, on_shutdown)
|
||||
|
||||
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
|
||||
executor.start_polling(
|
||||
reset_webhook=reset_webhook,
|
||||
timeout=timeout,
|
||||
relax=relax,
|
||||
fast=fast,
|
||||
allowed_updates=allowed_updates
|
||||
)
|
||||
|
||||
|
||||
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
|
|
@ -295,7 +305,8 @@ class Executor:
|
|||
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
|
||||
self.run_app(**kwargs)
|
||||
|
||||
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
|
||||
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True,
|
||||
allowed_updates: Optional[List[str]] = None):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -308,7 +319,7 @@ class Executor:
|
|||
try:
|
||||
loop.run_until_complete(self._startup_polling())
|
||||
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
|
||||
relax=relax, fast=fast))
|
||||
relax=relax, fast=fast, allowed_updates=allowed_updates))
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
# loop.stop()
|
||||
|
|
|
|||
|
|
@ -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-5.1-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ bot = Bot(token=API_TOKEN)
|
|||
dp = Dispatcher(bot)
|
||||
|
||||
|
||||
# if the text from user in the list
|
||||
# if the text is equal to any string in the list
|
||||
@dp.message_handler(text=['text1', 'text2'])
|
||||
async def text_in_handler(message: types.Message):
|
||||
await message.answer("The message text equals to one of in the list!")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.utils.deep_linking import decode_payload, encode_payload, filter_payload
|
||||
from aiogram.utils.deep_linking import get_start_link, get_startgroup_link
|
||||
from aiogram.utils.deep_linking import (
|
||||
decode_payload,
|
||||
encode_payload,
|
||||
get_start_link,
|
||||
get_startgroup_link,
|
||||
)
|
||||
from tests.types import dataset
|
||||
|
||||
# enable asyncio mode
|
||||
|
|
@ -17,9 +21,11 @@ PAYLOADS = [
|
|||
|
||||
WRONG_PAYLOADS = [
|
||||
'@BotFather',
|
||||
"Some:special$characters#=",
|
||||
'spaces spaces spaces',
|
||||
1234567890123456789.0,
|
||||
]
|
||||
USERNAME = dataset.USER["username"]
|
||||
|
||||
|
||||
@pytest.fixture(params=PAYLOADS, name='payload')
|
||||
|
|
@ -47,7 +53,7 @@ def get_bot_user_fixture(monkeypatch):
|
|||
class TestDeepLinking:
|
||||
async def test_get_start_link(self, payload):
|
||||
link = await get_start_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?start={payload}'
|
||||
|
||||
async def test_wrong_symbols(self, wrong_payload):
|
||||
with pytest.raises(ValueError):
|
||||
|
|
@ -55,20 +61,29 @@ class TestDeepLinking:
|
|||
|
||||
async def test_get_startgroup_link(self, payload):
|
||||
link = await get_startgroup_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?startgroup={payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?startgroup={payload}'
|
||||
|
||||
async def test_filter_encode_and_decode(self, payload):
|
||||
_payload = filter_payload(payload)
|
||||
encoded = encode_payload(_payload)
|
||||
encoded = encode_payload(payload)
|
||||
decoded = decode_payload(encoded)
|
||||
assert decoded == str(payload)
|
||||
|
||||
async def test_get_start_link_with_encoding(self, payload):
|
||||
async def test_get_start_link_with_encoding(self, wrong_payload):
|
||||
# define link
|
||||
link = await get_start_link(payload, encode=True)
|
||||
link = await get_start_link(wrong_payload, encode=True)
|
||||
|
||||
# define reference link
|
||||
payload = filter_payload(payload)
|
||||
encoded_payload = encode_payload(payload)
|
||||
encoded_payload = encode_payload(wrong_payload)
|
||||
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?start={encoded_payload}'
|
||||
|
||||
async def test_64_len_payload(self):
|
||||
payload = "p" * 64
|
||||
link = await get_start_link(payload)
|
||||
assert link
|
||||
|
||||
async def test_too_long_payload(self):
|
||||
payload = "p" * 65
|
||||
print(payload, len(payload))
|
||||
with pytest.raises(ValueError):
|
||||
await get_start_link(payload)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue