mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x' into is_chat_member
This commit is contained in:
commit
54d5406967
45 changed files with 947 additions and 266 deletions
|
|
@ -38,5 +38,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.2.1.dev1'
|
||||
__api_version__ = '4.3'
|
||||
__version__ = '2.3.dev1'
|
||||
__api_version__ = '4.4'
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 4.3
|
||||
List is updated to Bot API 4.4
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -182,6 +182,7 @@ class Methods(Helper):
|
|||
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
|
||||
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
|
||||
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
|
||||
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
|
||||
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
|
||||
SET_CHAT_PHOTO = Item() # setChatPhoto
|
||||
DELETE_CHAT_PHOTO = Item() # deleteChatPhoto
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from .base import BaseBot, api
|
||||
from .. import types
|
||||
|
|
@ -337,12 +338,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
reply_markup = prepare_arg(reply_markup)
|
||||
payload = generate_payload(**locals(), exclude=['audio'])
|
||||
payload = generate_payload(**locals(), exclude=['audio', 'thumb'])
|
||||
if self.parse_mode:
|
||||
payload.setdefault('parse_mode', self.parse_mode)
|
||||
|
||||
files = {}
|
||||
prepare_file(payload, files, 'audio', audio)
|
||||
prepare_attachment(payload, files, 'thumb', thumb)
|
||||
|
||||
result = await self.request(api.Methods.SEND_AUDIO, payload, files)
|
||||
return types.Message(**result)
|
||||
|
|
@ -1014,6 +1016,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
|
||||
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, None] = None,
|
||||
can_send_messages: typing.Union[base.Boolean, None] = None,
|
||||
can_send_media_messages: typing.Union[base.Boolean, None] = None,
|
||||
|
|
@ -1030,6 +1034,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
:param permissions: New user permissions
|
||||
:type permissions: :obj:`ChatPermissions`
|
||||
:param until_date: Date when restrictions will be lifted for the user, unix time
|
||||
:type until_date: :obj:`typing.Union[base.Integer, None]`
|
||||
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
|
||||
|
|
@ -1047,8 +1053,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
until_date = prepare_arg(until_date)
|
||||
permissions = prepare_arg(permissions)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
for permission in ['can_send_messages',
|
||||
'can_send_media_messages',
|
||||
'can_send_other_messages',
|
||||
'can_add_web_page_previews']:
|
||||
if permission in payload:
|
||||
warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions "
|
||||
f"in a single argument of the type ChatPermissions instead of "
|
||||
f"passing regular argument {payload[permission]}",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload)
|
||||
return result
|
||||
|
||||
|
|
@ -1099,6 +1116,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
|
||||
return result
|
||||
|
||||
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
permissions: types.ChatPermissions) -> 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
|
||||
can_restrict_members admin rights.
|
||||
|
||||
Returns True on success.
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target supergroup
|
||||
:param permissions: New default chat permissions
|
||||
:return: True on success.
|
||||
"""
|
||||
permissions = prepare_arg(permissions)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SET_CHAT_PERMISSIONS)
|
||||
return result
|
||||
|
||||
async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String:
|
||||
"""
|
||||
Use this method to generate a new invite link for a chat; any previously generated link is revoked.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ class _FileStorage(MemoryStorage):
|
|||
pass
|
||||
|
||||
async def close(self):
|
||||
self.write(self.path)
|
||||
if self.data:
|
||||
self.write(self.path)
|
||||
await super(_FileStorage, self).close()
|
||||
|
||||
def read(self, path: pathlib.Path):
|
||||
|
|
|
|||
200
aiogram/contrib/fsm_storage/mongo.py
Normal file
200
aiogram/contrib/fsm_storage/mongo.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
"""
|
||||
This module has mongo storage for finite-state machine
|
||||
based on `aiomongo <https://github.com/ZeoAlliance/aiomongo`_ driver
|
||||
"""
|
||||
|
||||
from typing import Union, Dict, Optional, List, Tuple, AnyStr
|
||||
|
||||
import aiomongo
|
||||
from aiomongo import AioMongoClient, Database
|
||||
|
||||
from ...dispatcher.storage import BaseStorage
|
||||
|
||||
STATE = 'aiogram_state'
|
||||
DATA = 'aiogram_data'
|
||||
BUCKET = 'aiogram_bucket'
|
||||
COLLECTIONS = (STATE, DATA, BUCKET)
|
||||
|
||||
|
||||
class MongoStorage(BaseStorage):
|
||||
"""
|
||||
Mongo-based storage for FSM.
|
||||
Usage:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
storage = MongoStorage(host='localhost', port=27017, db_name='aiogram_fsm')
|
||||
dp = Dispatcher(bot, storage=storage)
|
||||
|
||||
And need to close Mongo client connections when shutdown
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
await dp.storage.close()
|
||||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm',
|
||||
username=None, password=None, index=True, **kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db_name: str = db_name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._kwargs = kwargs
|
||||
|
||||
self._mongo: Union[AioMongoClient, None] = None
|
||||
self._db: Union[Database, None] = None
|
||||
|
||||
self._index = index
|
||||
|
||||
async def get_client(self) -> AioMongoClient:
|
||||
if isinstance(self._mongo, AioMongoClient):
|
||||
return self._mongo
|
||||
|
||||
uri = 'mongodb://'
|
||||
|
||||
# set username + password
|
||||
if self._username and self._password:
|
||||
uri += f'{self._username}:{self._password}@'
|
||||
|
||||
# set host and port (optional)
|
||||
uri += f'{self._host}:{self._port}' if self._host else f'localhost:{self._port}'
|
||||
|
||||
# define and return client
|
||||
self._mongo = await aiomongo.create_client(uri)
|
||||
return self._mongo
|
||||
|
||||
async def get_db(self) -> Database:
|
||||
"""
|
||||
Get Mongo db
|
||||
|
||||
This property is awaitable.
|
||||
"""
|
||||
if isinstance(self._db, Database):
|
||||
return self._db
|
||||
|
||||
mongo = await self.get_client()
|
||||
self._db = mongo.get_database(self._db_name)
|
||||
|
||||
if self._index:
|
||||
await self.apply_index(self._db)
|
||||
return self._db
|
||||
|
||||
@staticmethod
|
||||
async def apply_index(db):
|
||||
for collection in COLLECTIONS:
|
||||
await db[collection].create_index(keys=[('chat', 1), ('user', 1)],
|
||||
name="chat_user_idx", unique=True, background=True)
|
||||
|
||||
async def close(self):
|
||||
if self._mongo:
|
||||
self._mongo.close()
|
||||
|
||||
async def wait_closed(self):
|
||||
if self._mongo:
|
||||
return await self._mongo.wait_closed()
|
||||
return True
|
||||
|
||||
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
state: Optional[AnyStr] = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
||||
if state is None:
|
||||
await db[STATE].delete_one(filter={'chat': chat, 'user': user})
|
||||
else:
|
||||
await db[STATE].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'state': state}}, upsert=True)
|
||||
|
||||
async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[str] = None) -> Optional[str]:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
|
||||
|
||||
return result.get('state') if result else default
|
||||
|
||||
async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
data: Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
||||
await db[DATA].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'data': data}}, upsert=True)
|
||||
|
||||
async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[dict] = None) -> Dict:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
result = await db[DATA].find_one(filter={'chat': chat, 'user': user})
|
||||
|
||||
return result.get('data') if result else default or {}
|
||||
|
||||
async def update_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
data: Dict = None, **kwargs):
|
||||
if data is None:
|
||||
data = {}
|
||||
temp_data = await self.get_data(chat=chat, user=user, default={})
|
||||
temp_data.update(data, **kwargs)
|
||||
await self.set_data(chat=chat, user=user, data=temp_data)
|
||||
|
||||
def has_bucket(self):
|
||||
return True
|
||||
|
||||
async def get_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[dict] = None) -> Dict:
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
result = await db[BUCKET].find_one(filter={'chat': chat, 'user': user})
|
||||
return result.get('bucket') if result else default or {}
|
||||
|
||||
async def set_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
bucket: Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
||||
await db[BUCKET].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'bucket': bucket}}, upsert=True)
|
||||
|
||||
async def update_bucket(self, *, chat: Union[str, int, None] = None,
|
||||
user: Union[str, int, None] = None,
|
||||
bucket: Dict = None, **kwargs):
|
||||
if bucket is None:
|
||||
bucket = {}
|
||||
temp_bucket = await self.get_bucket(chat=chat, user=user)
|
||||
temp_bucket.update(bucket, **kwargs)
|
||||
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
|
||||
|
||||
async def reset_all(self, full=True):
|
||||
"""
|
||||
Reset states in DB
|
||||
|
||||
:param full: clean DB or clean only states
|
||||
:return:
|
||||
"""
|
||||
db = await self.get_db()
|
||||
|
||||
await db[STATE].drop()
|
||||
|
||||
if full:
|
||||
await db[DATA].drop()
|
||||
await db[BUCKET].drop()
|
||||
|
||||
async def get_states_list(self) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Get list of all stored chat's and user's
|
||||
|
||||
:return: list of tuples where first element is chat id and second is user id
|
||||
"""
|
||||
db = await self.get_db()
|
||||
result = []
|
||||
|
||||
items = await db[STATE].find().to_list()
|
||||
for item in items:
|
||||
result.append(
|
||||
(int(item['chat']), int(item['user']))
|
||||
)
|
||||
|
||||
return result
|
||||
|
|
@ -35,7 +35,6 @@ class RedisStorage(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
|
@ -62,8 +61,6 @@ class RedisStorage(BaseStorage):
|
|||
async def redis(self) -> aioredis.RedisConnection:
|
||||
"""
|
||||
Get Redis connection
|
||||
|
||||
This property is awaitable.
|
||||
"""
|
||||
# Use thread-safe asyncio Lock because this method without that is not safe
|
||||
async with self._connection_lock:
|
||||
|
|
@ -222,9 +219,12 @@ class RedisStorage2(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None,
|
||||
pool_size=10, loop=None, prefix='fsm', **kwargs):
|
||||
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
|
||||
ssl=None, pool_size=10, loop=None, prefix='fsm',
|
||||
state_ttl: int = 0,
|
||||
data_ttl: int = 0,
|
||||
bucket_ttl: int = 0,
|
||||
**kwargs):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._db = db
|
||||
|
|
@ -235,14 +235,16 @@ class RedisStorage2(BaseStorage):
|
|||
self._kwargs = kwargs
|
||||
self._prefix = (prefix,)
|
||||
|
||||
self._state_ttl = state_ttl
|
||||
self._data_ttl = data_ttl
|
||||
self._bucket_ttl = bucket_ttl
|
||||
|
||||
self._redis: aioredis.RedisConnection = None
|
||||
self._connection_lock = asyncio.Lock(loop=self._loop)
|
||||
|
||||
async def redis(self) -> aioredis.Redis:
|
||||
"""
|
||||
Get Redis connection
|
||||
|
||||
This property is awaitable.
|
||||
"""
|
||||
# Use thread-safe asyncio Lock because this method without that is not safe
|
||||
async with self._connection_lock:
|
||||
|
|
@ -294,14 +296,14 @@ class RedisStorage2(BaseStorage):
|
|||
if state is None:
|
||||
await redis.delete(key)
|
||||
else:
|
||||
await redis.set(key, state)
|
||||
await redis.set(key, state, expire=self._state_ttl)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_DATA_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(data))
|
||||
await redis.set(key, json.dumps(data), expire=self._data_ttl)
|
||||
|
||||
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None, **kwargs):
|
||||
|
|
@ -329,7 +331,7 @@ class RedisStorage2(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(bucket))
|
||||
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
|
||||
|
||||
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
|
|
@ -338,7 +340,7 @@ class RedisStorage2(BaseStorage):
|
|||
bucket = {}
|
||||
temp_bucket = await self.get_bucket(chat=chat, user=user)
|
||||
temp_bucket.update(bucket, **kwargs)
|
||||
await self.set_bucket(chat=chat, user=user, data=temp_bucket)
|
||||
await self.set_bucket(chat=chat, user=user, bucket=temp_bucket)
|
||||
|
||||
async def reset_all(self, full=True):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class I18nMiddleware(BaseMiddleware):
|
|||
else:
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy:
|
||||
def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy:
|
||||
"""
|
||||
Lazy get text
|
||||
|
||||
|
|
@ -115,9 +115,10 @@ class I18nMiddleware(BaseMiddleware):
|
|||
:param plural:
|
||||
:param n:
|
||||
:param locale:
|
||||
:param enable_cache:
|
||||
:return:
|
||||
"""
|
||||
return LazyProxy(self.gettext, singular, plural, n, locale)
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -89,34 +89,39 @@ class LoggingMiddleware(BaseMiddleware):
|
|||
|
||||
async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict):
|
||||
if callback_query.message:
|
||||
text = (f"Received callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
|
||||
f"from user [ID:{callback_query.message.from_user.id}]")
|
||||
else:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
else:
|
||||
self.logger.info(f"Received callback query [ID:{callback_query.id}] "
|
||||
f"from inline message [ID:{callback_query.inline_message_id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}]")
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for inline message [ID:{callback_query.inline_message_id}] ")
|
||||
|
||||
async def on_post_process_callback_query(self, callback_query, results, data: dict):
|
||||
if callback_query.message:
|
||||
text = (f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}] "
|
||||
f"for message [ID:{callback_query.message.message_id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
|
||||
if callback_query.message.from_user:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] "
|
||||
f"from user [ID:{callback_query.message.from_user.id}]")
|
||||
else:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]")
|
||||
text += f" originally posted by user [ID:{callback_query.message.from_user.id}]"
|
||||
|
||||
self.logger.info(text)
|
||||
|
||||
else:
|
||||
self.logger.debug(f"{HANDLED_STR[bool(len(results))]} "
|
||||
f"callback query [ID:{callback_query.id}] "
|
||||
f"from inline message [ID:{callback_query.inline_message_id}] "
|
||||
f"from user [ID:{callback_query.from_user.id}]")
|
||||
f"from user [ID:{callback_query.from_user.id}]"
|
||||
f"from inline message [ID:{callback_query.inline_message_id}]")
|
||||
|
||||
async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict):
|
||||
self.logger.info(f"Received shipping query [ID:{shipping_query.id}] "
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import aiohttp
|
|||
from aiohttp.helpers import sentinel
|
||||
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
|
||||
RegexpCommandsFilter, StateFilter, Text
|
||||
RegexpCommandsFilter, StateFilter, Text, IdFilter
|
||||
from .handler import Handler
|
||||
from .middlewares import MiddlewareManager
|
||||
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
|
||||
|
|
@ -97,7 +97,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(Text, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.poll_handlers
|
||||
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers
|
||||
])
|
||||
filters_factory.bind(HashTag, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
|
|
@ -106,7 +106,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(Regexp, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.poll_handlers
|
||||
self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers
|
||||
])
|
||||
filters_factory.bind(RegexpCommandsFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers
|
||||
|
|
@ -114,6 +114,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
filters_factory.bind(ExceptionsFilter, event_handlers=[
|
||||
self.errors_handlers
|
||||
])
|
||||
filters_factory.bind(IdFilter, event_handlers=[
|
||||
self.message_handlers, self.edited_message_handlers,
|
||||
self.channel_post_handlers, self.edited_channel_post_handlers,
|
||||
self.callback_query_handlers, self.inline_query_handlers
|
||||
])
|
||||
|
||||
def __del__(self):
|
||||
self.stop_polling()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text
|
||||
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter
|
||||
from .factory import FiltersFactory
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
|
||||
check_filters, get_filter_spec, get_filters_spec
|
||||
|
|
@ -23,6 +23,7 @@ __all__ = [
|
|||
'Regexp',
|
||||
'StateFilter',
|
||||
'Text',
|
||||
'IdFilter',
|
||||
'get_filter_spec',
|
||||
'get_filters_spec',
|
||||
'execute_filter',
|
||||
|
|
|
|||
|
|
@ -221,13 +221,13 @@ class Text(Filter):
|
|||
:param ignore_case: case insensitive
|
||||
"""
|
||||
# Only one mode can be used. check it.
|
||||
check = sum(map(bool, (equals, contains, startswith, endswith)))
|
||||
check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith)))
|
||||
if check > 1:
|
||||
args = "' and '".join([arg[0] for arg in [('equals', equals),
|
||||
('contains', contains),
|
||||
('startswith', startswith),
|
||||
('endswith', endswith)
|
||||
] if arg[1]])
|
||||
] if arg[1] is not None])
|
||||
raise ValueError(f"Arguments '{args}' cannot be used together.")
|
||||
elif check == 0:
|
||||
raise ValueError(f"No one mode is specified!")
|
||||
|
|
@ -249,7 +249,7 @@ class Text(Filter):
|
|||
elif 'text_endswith' in full_config:
|
||||
return {'endswith': full_config.pop('text_endswith')}
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
|
||||
if isinstance(obj, Message):
|
||||
text = obj.text or obj.caption or ''
|
||||
if not text and obj.poll:
|
||||
|
|
@ -266,14 +266,26 @@ class Text(Filter):
|
|||
if self.ignore_case:
|
||||
text = text.lower()
|
||||
|
||||
if self.equals:
|
||||
return text == str(self.equals)
|
||||
elif self.contains:
|
||||
return str(self.contains) in text
|
||||
elif self.startswith:
|
||||
return text.startswith(str(self.startswith))
|
||||
elif self.endswith:
|
||||
return text.endswith(str(self.endswith))
|
||||
if self.equals is not None:
|
||||
self.equals = str(self.equals)
|
||||
if self.ignore_case:
|
||||
self.equals = self.equals.lower()
|
||||
return text == self.equals
|
||||
elif self.contains is not None:
|
||||
self.contains = str(self.contains)
|
||||
if self.ignore_case:
|
||||
self.contains = self.contains.lower()
|
||||
return self.contains in text
|
||||
elif self.startswith is not None:
|
||||
self.startswith = str(self.startswith)
|
||||
if self.ignore_case:
|
||||
self.startswith = self.startswith.lower()
|
||||
return text.startswith(self.startswith)
|
||||
elif self.endswith is not None:
|
||||
self.endswith = str(self.endswith)
|
||||
if self.ignore_case:
|
||||
self.endswith = self.endswith.lower()
|
||||
return text.endswith(self.endswith)
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -359,13 +371,17 @@ class Regexp(Filter):
|
|||
if 'regexp' in full_config:
|
||||
return {'regexp': full_config.pop('regexp')}
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery]):
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]):
|
||||
if isinstance(obj, Message):
|
||||
content = obj.text or obj.caption or ''
|
||||
if not content and obj.poll:
|
||||
content = obj.poll.question
|
||||
elif isinstance(obj, CallbackQuery) and obj.data:
|
||||
content = obj.data
|
||||
elif isinstance(obj, InlineQuery):
|
||||
content = obj.query
|
||||
elif isinstance(obj, Poll):
|
||||
content = obj.question
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -487,3 +503,66 @@ class ExceptionsFilter(BoundFilter):
|
|||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
class IdFilter(Filter):
|
||||
|
||||
def __init__(self,
|
||||
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
):
|
||||
"""
|
||||
:param user_id:
|
||||
:param chat_id:
|
||||
"""
|
||||
if user_id is None and chat_id is None:
|
||||
raise ValueError("Both user_id and chat_id can't be None")
|
||||
|
||||
self.user_id = None
|
||||
self.chat_id = None
|
||||
if user_id:
|
||||
if isinstance(user_id, Iterable):
|
||||
self.user_id = list(map(int, user_id))
|
||||
else:
|
||||
self.user_id = [int(user_id), ]
|
||||
if chat_id:
|
||||
if isinstance(chat_id, Iterable):
|
||||
self.chat_id = list(map(int, chat_id))
|
||||
else:
|
||||
self.chat_id = [int(chat_id), ]
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
result = {}
|
||||
if 'user_id' in full_config:
|
||||
result['user_id'] = full_config.pop('user_id')
|
||||
|
||||
if 'chat_id' in full_config:
|
||||
result['chat_id'] = full_config.pop('chat_id')
|
||||
|
||||
return result
|
||||
|
||||
async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]):
|
||||
if isinstance(obj, Message):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = obj.chat.id
|
||||
elif isinstance(obj, CallbackQuery):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = None
|
||||
if obj.message is not None:
|
||||
# if the button was sent with message
|
||||
chat_id = obj.message.chat.id
|
||||
elif isinstance(obj, InlineQuery):
|
||||
user_id = obj.from_user.id
|
||||
chat_id = None
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.user_id and self.chat_id:
|
||||
return user_id in self.user_id and chat_id in self.chat_id
|
||||
elif self.user_id:
|
||||
return user_id in self.user_id
|
||||
elif self.chat_id:
|
||||
return chat_id in self.chat_id
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -202,14 +202,14 @@ class BoundFilter(Filter):
|
|||
You need to implement ``__init__`` method with single argument related with key attribute
|
||||
and ``check`` method where you need to implement filter logic.
|
||||
"""
|
||||
|
||||
"""Unique name of the filter argument. You need to override this attribute."""
|
||||
|
||||
key = None
|
||||
"""If :obj:`True` this filter will be added to the all of the registered handlers"""
|
||||
"""Unique name of the filter argument. You need to override this attribute."""
|
||||
required = False
|
||||
"""Default value for configure required filters"""
|
||||
"""If :obj:`True` this filter will be added to the all of the registered handlers"""
|
||||
default = None
|
||||
|
||||
"""Default value for configure required filters"""
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ class CancelHandler(Exception):
|
|||
def _get_spec(func: callable):
|
||||
while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks
|
||||
func = func.__wrapped__
|
||||
|
||||
spec = inspect.getfullargspec(func)
|
||||
return spec, func
|
||||
return spec
|
||||
|
||||
|
||||
def _check_spec(spec: inspect.FullArgSpec, kwargs: dict):
|
||||
|
|
@ -56,7 +55,7 @@ class Handler:
|
|||
:param filters: list of filters
|
||||
:param index: you can reorder handlers
|
||||
"""
|
||||
spec, handler = _get_spec(handler)
|
||||
spec = _get_spec(handler)
|
||||
|
||||
if filters and not isinstance(filters, (list, tuple, set)):
|
||||
filters = [filters]
|
||||
|
|
@ -105,7 +104,7 @@ class Handler:
|
|||
try:
|
||||
for handler_obj in self.handlers:
|
||||
try:
|
||||
data.update(await check_filters(handler_obj.filters, args + (data,)))
|
||||
data.update(await check_filters(handler_obj.filters, args))
|
||||
except FilterNotPassed:
|
||||
continue
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import functools
|
|||
import ipaddress
|
||||
import itertools
|
||||
import typing
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from aiohttp import web
|
||||
|
|
@ -35,6 +36,8 @@ TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22')
|
|||
|
||||
allowed_ips = set()
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_ip(ip: str) -> bool:
|
||||
"""
|
||||
|
|
@ -77,7 +80,7 @@ class WebhookRequestHandler(web.View):
|
|||
|
||||
.. code-block:: python3
|
||||
|
||||
app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler')
|
||||
app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler')
|
||||
|
||||
But first you need to configure application for getting Dispatcher instance from request handler!
|
||||
It must always be with key 'BOT_DISPATCHER'
|
||||
|
|
@ -258,7 +261,9 @@ class WebhookRequestHandler(web.View):
|
|||
if self.request.app.get('_check_ip', False):
|
||||
ip_address, accept = self.check_ip()
|
||||
if not accept:
|
||||
log.warning(f"Blocking request from an unauthorized IP: {ip_address}")
|
||||
raise web.HTTPUnauthorized()
|
||||
|
||||
# context.set_value('TELEGRAM_IP', ip_address)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .callback_game import CallbackGame
|
|||
from .callback_query import CallbackQuery
|
||||
from .chat import Chat, ChatActions, ChatType
|
||||
from .chat_member import ChatMember, ChatMemberStatus
|
||||
from .chat_permissions import ChatPermissions
|
||||
from .chat_photo import ChatPhoto
|
||||
from .chosen_inline_result import ChosenInlineResult
|
||||
from .contact import Contact
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import typing
|
|||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .chat_permissions import ChatPermissions
|
||||
from .chat_photo import ChatPhoto
|
||||
from ..utils import helper
|
||||
from ..utils import markdown
|
||||
|
|
@ -27,6 +28,7 @@ class Chat(base.TelegramObject):
|
|||
description: base.String = fields.Field()
|
||||
invite_link: base.String = fields.Field()
|
||||
pinned_message: 'Message' = fields.Field(base='Message')
|
||||
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
|
||||
sticker_set_name: base.String = fields.Field()
|
||||
can_set_sticker_set: base.Boolean = fields.Field()
|
||||
|
||||
|
|
@ -202,6 +204,7 @@ class Chat(base.TelegramObject):
|
|||
return await self.bot.unban_chat_member(self.id, user_id=user_id)
|
||||
|
||||
async def restrict(self, user_id: base.Integer,
|
||||
permissions: typing.Optional[ChatPermissions] = None,
|
||||
until_date: typing.Union[base.Integer, None] = None,
|
||||
can_send_messages: typing.Union[base.Boolean, None] = None,
|
||||
can_send_media_messages: typing.Union[base.Boolean, None] = None,
|
||||
|
|
@ -216,6 +219,8 @@ class Chat(base.TelegramObject):
|
|||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
:param permissions: New user permissions
|
||||
:type permissions: :obj:`ChatPermissions`
|
||||
:param until_date: Date when restrictions will be lifted for the user, unix time.
|
||||
:type until_date: :obj:`typing.Union[base.Integer, None]`
|
||||
:param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues
|
||||
|
|
@ -232,7 +237,9 @@ class Chat(base.TelegramObject):
|
|||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date,
|
||||
return await self.bot.restrict_chat_member(self.id, user_id=user_id,
|
||||
permissions=permissions,
|
||||
until_date=until_date,
|
||||
can_send_messages=can_send_messages,
|
||||
can_send_media_messages=can_send_media_messages,
|
||||
can_send_other_messages=can_send_other_messages,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class ChatMember(base.TelegramObject):
|
|||
is_member: base.Boolean = fields.Field()
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: base.Boolean = fields.Field()
|
||||
can_send_polls: base.Boolean = fields.Field()
|
||||
can_send_other_messages: base.Boolean = fields.Field()
|
||||
can_add_web_page_previews: base.Boolean = fields.Field()
|
||||
|
||||
|
|
|
|||
39
aiogram/types/chat_permissions.py
Normal file
39
aiogram/types/chat_permissions.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from . import base
|
||||
from . import fields
|
||||
|
||||
|
||||
class ChatPermissions(base.TelegramObject):
|
||||
"""
|
||||
Describes actions that a non-administrator user is allowed to take in a chat.
|
||||
|
||||
https://core.telegram.org/bots/api#chatpermissions
|
||||
"""
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: base.Boolean = fields.Field()
|
||||
can_send_polls: base.Boolean = fields.Field()
|
||||
can_send_other_messages: base.Boolean = fields.Field()
|
||||
can_add_web_page_previews: base.Boolean = fields.Field()
|
||||
can_change_info: base.Boolean = fields.Field()
|
||||
can_invite_users: base.Boolean = fields.Field()
|
||||
can_pin_messages: base.Boolean = fields.Field()
|
||||
|
||||
def __init__(self,
|
||||
can_send_messages: base.Boolean = None,
|
||||
can_send_media_messages: base.Boolean = None,
|
||||
can_send_polls: base.Boolean = None,
|
||||
can_send_other_messages: base.Boolean = None,
|
||||
can_add_web_page_previews: base.Boolean = None,
|
||||
can_change_info: base.Boolean = None,
|
||||
can_invite_users: base.Boolean = None,
|
||||
can_pin_messages: base.Boolean = None,
|
||||
**kwargs):
|
||||
super(ChatPermissions, self).__init__(
|
||||
can_send_messages=can_send_messages,
|
||||
can_send_media_messages=can_send_media_messages,
|
||||
can_send_polls=can_send_polls,
|
||||
can_send_other_messages=can_send_other_messages,
|
||||
can_add_web_page_previews=can_add_web_page_previews,
|
||||
can_change_info=can_change_info,
|
||||
can_invite_users=can_invite_users,
|
||||
can_pin_messages=can_pin_messages,
|
||||
)
|
||||
|
|
@ -26,7 +26,7 @@ class InputMedia(base.TelegramObject):
|
|||
media: base.String = fields.Field(alias='media', on_change='_media_changed')
|
||||
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
|
||||
caption: base.String = fields.Field()
|
||||
parse_mode: base.Boolean = fields.Field()
|
||||
parse_mode: base.String = fields.Field()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._thumb_file = None
|
||||
|
|
@ -110,7 +110,7 @@ class InputMediaAnimation(InputMedia):
|
|||
thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None,
|
||||
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
|
||||
parse_mode: base.Boolean = None, **kwargs):
|
||||
parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
parse_mode=parse_mode, conf=kwargs)
|
||||
|
|
@ -124,7 +124,7 @@ class InputMediaDocument(InputMedia):
|
|||
"""
|
||||
|
||||
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
|
||||
caption: base.String = None, parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
|
||||
caption=caption, parse_mode=parse_mode,
|
||||
conf=kwargs)
|
||||
|
|
@ -150,7 +150,7 @@ class InputMediaAudio(InputMedia):
|
|||
duration: base.Integer = None,
|
||||
performer: base.String = None,
|
||||
title: base.String = None,
|
||||
parse_mode: base.Boolean = None, **kwargs):
|
||||
parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
performer=performer, title=title,
|
||||
|
|
@ -165,7 +165,7 @@ class InputMediaPhoto(InputMedia):
|
|||
"""
|
||||
|
||||
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
|
||||
caption: base.String = None, parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
|
||||
caption=caption, parse_mode=parse_mode,
|
||||
conf=kwargs)
|
||||
|
|
@ -186,7 +186,7 @@ class InputMediaVideo(InputMedia):
|
|||
thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None,
|
||||
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
|
||||
parse_mode: base.Boolean = None,
|
||||
parse_mode: base.String = None,
|
||||
supports_streaming: base.Boolean = None, **kwargs):
|
||||
super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
|
|
@ -277,7 +277,7 @@ class MediaGroup(base.TelegramObject):
|
|||
duration: base.Integer = None,
|
||||
performer: base.String = None,
|
||||
title: base.String = None,
|
||||
parse_mode: base.Boolean = None):
|
||||
parse_mode: base.String = None):
|
||||
"""
|
||||
Attach animation
|
||||
|
||||
|
|
@ -299,7 +299,7 @@ class MediaGroup(base.TelegramObject):
|
|||
self.attach(audio)
|
||||
|
||||
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None, parse_mode: base.Boolean = None):
|
||||
caption: base.String = None, parse_mode: base.String = None):
|
||||
"""
|
||||
Attach document
|
||||
|
||||
|
|
|
|||
|
|
@ -959,70 +959,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_animation(self, animation: typing.Union[base.InputFile, base.String],
|
||||
duration: typing.Union[base.Integer, None] = None,
|
||||
width: typing.Union[base.Integer, None] = None,
|
||||
height: typing.Union[base.Integer, None] = None,
|
||||
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
|
||||
caption: typing.Union[base.String, None] = None,
|
||||
parse_mode: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
|
||||
|
||||
On success, the sent Message is returned.
|
||||
Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
|
||||
|
||||
Source https://core.telegram.org/bots/api#sendanimation
|
||||
|
||||
:param animation: Animation to send. Pass a file_id as String to send an animation that exists
|
||||
on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation
|
||||
from the Internet, or upload a new animation using multipart/form-data
|
||||
:type animation: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
:param duration: Duration of sent animation in seconds
|
||||
:type duration: :obj:`typing.Union[base.Integer, None]`
|
||||
:param width: Animation width
|
||||
:type width: :obj:`typing.Union[base.Integer, None]`
|
||||
:param height: Animation height
|
||||
:type height: :obj:`typing.Union[base.Integer, None]`
|
||||
:param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
|
||||
A thumbnail‘s width and height should not exceed 90.
|
||||
:type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
|
||||
:param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters
|
||||
:type caption: :obj:`typing.Union[base.String, None]`
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
fixed-width text or inline URLs in the media caption
|
||||
:type parse_mode: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
|
||||
types.ReplyKeyboardRemove, types.ForceReply], None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_animation" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_animation(self.chat.id,
|
||||
animation=animation,
|
||||
duration=duration,
|
||||
width=width,
|
||||
height=height,
|
||||
thumb=thumb,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
|
||||
duration: typing.Union[base.Integer, None] = None,
|
||||
|
|
@ -1323,55 +1259,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_venue(self,
|
||||
latitude: base.Float, longitude: base.Float,
|
||||
title: base.String, address: base.String,
|
||||
foursquare_id: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send information about a venue.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendvenue
|
||||
|
||||
:param latitude: Latitude of the venue
|
||||
:type latitude: :obj:`base.Float`
|
||||
:param longitude: Longitude of the venue
|
||||
:type longitude: :obj:`base.Float`
|
||||
:param title: Name of the venue
|
||||
:type title: :obj:`base.String`
|
||||
:param address: Address of the venue
|
||||
:type address: :obj:`base.String`
|
||||
:param foursquare_id: Foursquare identifier of the venue
|
||||
:type foursquare_id: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned.
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_venue" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_venue(chat_id=self.chat.id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
title=title,
|
||||
address=address,
|
||||
foursquare_id=foursquare_id,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_venue(self,
|
||||
latitude: base.Float, longitude: base.Float,
|
||||
title: base.String, address: base.String,
|
||||
|
|
@ -1417,46 +1304,6 @@ class Message(base.TelegramObject):
|
|||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def send_contact(self, phone_number: base.String,
|
||||
first_name: base.String, last_name: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
reply_markup: typing.Union[InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
ForceReply, None] = None,
|
||||
reply: base.Boolean = True) -> Message:
|
||||
"""
|
||||
Use this method to send phone contacts.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendcontact
|
||||
|
||||
:param phone_number: Contact's phone number
|
||||
:type phone_number: :obj:`base.String`
|
||||
:param first_name: Contact's first name
|
||||
:type first_name: :obj:`base.String`
|
||||
:param last_name: Contact's last name
|
||||
:type last_name: :obj:`typing.Union[base.String, None]`
|
||||
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
|
||||
:type disable_notification: :obj:`typing.Union[base.Boolean, None]`
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
|
||||
custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
|
||||
types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
|
||||
:param reply: fill 'reply_to_message_id'
|
||||
:return: On success, the sent Message is returned.
|
||||
:rtype: :obj:`types.Message`
|
||||
"""
|
||||
warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n'
|
||||
'Use "Message.reply_contact" instead.',
|
||||
stacklevel=8)
|
||||
|
||||
return await self.bot.send_contact(chat_id=self.chat.id,
|
||||
phone_number=phone_number,
|
||||
first_name=first_name, last_name=last_name,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=self.message_id if reply else None,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
async def reply_contact(self, phone_number: base.String,
|
||||
first_name: base.String, last_name: typing.Union[base.String, None] = None,
|
||||
disable_notification: typing.Union[base.Boolean, None] = None,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
|
|||
file_id: base.String = fields.Field()
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
is_animated: base.Boolean = fields.Field()
|
||||
thumb: PhotoSize = fields.Field(base=PhotoSize)
|
||||
emoji: base.String = fields.Field()
|
||||
set_name: base.String = fields.Field()
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ class StickerSet(base.TelegramObject):
|
|||
"""
|
||||
name: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
is_animated: base.Boolean = fields.Field()
|
||||
contains_masks: base.Boolean = fields.Field()
|
||||
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
|
|||
|
||||
|
||||
class BotKicked(Unauthorized):
|
||||
match = 'Bot was kicked from a chat'
|
||||
match = 'bot was kicked from a chat'
|
||||
|
||||
|
||||
class BotBlocked(Unauthorized):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue