This commit is contained in:
ada-dmitry
2024-08-24 15:45:54 +03:00
commit ab9d8386fb
1777 changed files with 358417 additions and 0 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+450
View File
@@ -0,0 +1,450 @@
from abc import ABC
from typing import Optional, Union
from telebot.asyncio_handler_backends import State
from telebot import types
from telebot.states import resolve_context
class SimpleCustomFilter(ABC):
"""
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating a simple custom filter.
class ForwardFilter(SimpleCustomFilter):
# Check whether message was forwarded from channel or group.
key = 'is_forwarded'
def check(self, message):
return message.forward_date is not None
"""
key: str = None
async def check(self, message) -> bool:
"""
Perform a check.
"""
pass
class AdvancedCustomFilter(ABC):
"""
Advanced Custom Filter base class.
Create child class with check() method.
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating an advanced custom filter.
class TextStartsFilter(AdvancedCustomFilter):
# Filter to check whether message starts with some text.
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
"""
key: str = None
async def check(self, message, text):
"""
Perform a check.
"""
pass
class TextFilter:
"""
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
example of usage is in examples/asynchronous_telebot/custom_filters/advanced_text_filter.py
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
async def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
"""
:meta private:
"""
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
prepare_func = lambda string: str(string).lower()
else:
prepare_func = str
if self.equals:
result = prepare_func(self.equals) == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([prepare_func(i) in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(prepare_func(i)) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(prepare_func(i)) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(text=['account'])
# your function
"""
key = 'text'
async def check(self, message, text):
"""
:meta private:
"""
if isinstance(text, TextFilter):
return await text.check(message)
elif type(text) is list:
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
key: text
.. code-block:: python3
:caption: Example on using this filter:
# Will respond if any message.text contains word 'account'
@bot.message_handler(text_contains=['account'])
# your function
"""
key = 'text_contains'
async def check(self, message, text):
"""
:meta private:
"""
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
Filter to check whether message starts with some text.
.. code-block:: python3
:caption: Example on using this filter:
# Will work if message.text starts with 'sir'.
@bot.message_handler(text_startswith='sir')
# your function
"""
key = 'text_startswith'
async def check(self, message, text):
"""
:meta private:
"""
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
Check whether chat_id corresponds to given chat_id.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_id=[99999])
# your function
"""
key = 'chat_id'
async def check(self, message, text):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.chat.id in text
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_forwarded=True)
# your function
"""
key = 'is_forwarded'
async def check(self, message):
"""
:meta private:
"""
return message.forward_origin is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_reply=True)
# your function
"""
key = 'is_reply'
async def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.reply_to_message is not None
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(language_code=['ru'])
# your function
"""
key = 'language_code'
async def check(self, message, text):
"""
:meta private:
"""
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
Check whether the user is administrator / owner of the chat.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
# your function
"""
key = 'is_chat_admin'
def __init__(self, bot):
self._bot = bot
async def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
result = await self._bot.get_chat_member(message.message.chat.id, message.from_user.id)
return result.status ('creator', 'administrator')
result = await self._bot.get_chat_member(message.chat.id, message.from_user.id)
return result.status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(state=1)
# your function
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
async def check(self, message, text):
"""
:meta private:
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(message, self.bot._user.id)
if chat_id is None:
chat_id = user_id # May change in future
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
user_state = await self.bot.current_states.get_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
# CHANGED BEHAVIOUR
if text == "*" and user_state is not None:
return True
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
return False
class IsDigitFilter(SimpleCustomFilter):
"""
Filter to check whether the string is made up of only digits.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_digit=True)
# your function
"""
key = 'is_digit'
async def check(self, message):
"""
:meta private:
"""
return message.text.isdigit()
@@ -0,0 +1,98 @@
"""
File with all middleware classes, states.
"""
from telebot.states import State, StatesGroup
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
Set update_sensitive=True if you want to get different updates on
different functions. For example, if you want to handle pre_process for
message update, then you will have to create pre_process_message function, and
so on. Same applies to post_process.
.. code-block:: python
:caption: Example of class-based middlewares
class MyMiddleware(BaseMiddleware):
def __init__(self):
self.update_sensitive = True
self.update_types = ['message', 'edited_message']
async def pre_process_message(self, message, data):
# only message update here
pass
async def post_process_message(self, message, data, exception):
pass # only message update here for post_process
async def pre_process_edited_message(self, message, data):
# only edited_message update here
pass
async def post_process_edited_message(self, message, data, exception):
pass # only edited_message update here for post_process
"""
update_sensitive: bool = False
def __init__(self):
pass
async def pre_process(self, message, data):
raise NotImplementedError
async def post_process(self, message, data, exception):
raise NotImplementedError
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass
class ContinueHandling:
"""
Class for continue updates in handlers.
Just return instance of this class
in handlers to continue process.
.. code-block:: python3
:caption: Example of using ContinueHandling
@bot.message_handler(commands=['start'])
async def start(message):
await bot.send_message(message.chat.id, 'Hello World!')
return ContinueHandling()
@bot.message_handler(commands=['start'])
async def start2(message):
await bot.send_message(message.chat.id, 'Hello World2!')
"""
def __init__(self) -> None:
pass
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
from telebot.asyncio_storage.redis_storage import StateRedisStorage
from telebot.asyncio_storage.pickle_storage import StatePickleStorage
from telebot.asyncio_storage.base_storage import StateDataContext, StateStorageBase
__all__ = [
"StateStorageBase",
"StateDataContext",
"StateMemoryStorage",
"StateRedisStorage",
"StatePickleStorage",
]
@@ -0,0 +1,123 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
async def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
async def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
async def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
async def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
async def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
async def get_state(self, chat_id, user_id):
raise NotImplementedError
def get_interactive_data(self, chat_id, user_id):
"""
Should be sync, but should provide a context manager
with __aenter__ and __aexit__ methods.
"""
raise NotImplementedError
async def save(self, chat_id, user_id, data):
raise NotImplementedError
def _get_key(
self,
chat_id: int,
user_id: int,
prefix: str,
separator: str,
business_connection_id: str = None,
message_thread_id: int = None,
bot_id: int = None,
) -> str:
"""
Convert parameters to a key.
"""
params = [prefix]
if bot_id:
params.append(str(bot_id))
if business_connection_id:
params.append(business_connection_id)
if message_thread_id:
params.append(str(message_thread_id))
params.append(str(chat_id))
params.append(str(user_id))
return separator.join(params)
class StateDataContext:
"""
Class for data.
"""
def __init__(
self,
obj,
chat_id,
user_id,
business_connection_id=None,
message_thread_id=None,
bot_id=None,
):
self.obj = obj
self.data = None
self.chat_id = chat_id
self.user_id = user_id
self.bot_id = bot_id
self.business_connection_id = business_connection_id
self.message_thread_id = message_thread_id
async def __aenter__(self):
data = await self.obj.get_data(
chat_id=self.chat_id,
user_id=self.user_id,
business_connection_id=self.business_connection_id,
message_thread_id=self.message_thread_id,
bot_id=self.bot_id,
)
self.data = copy.deepcopy(data)
return self.data
async def __aexit__(self, exc_type, exc_val, exc_tb):
return await self.obj.save(
self.chat_id,
self.user_id,
self.data,
self.business_connection_id,
self.message_thread_id,
self.bot_id,
)
@@ -0,0 +1,225 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateDataContext
from typing import Optional, Union
class StateMemoryStorage(StateStorageBase):
"""
Memory storage for states.
Stores states in memory as a dictionary.
.. code-block:: python3
storage = StateMemoryStorage()
bot = AsyncTeleBot(token, storage=storage)
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
"""
def __init__(
self, separator: Optional[str] = ":", prefix: Optional[str] = "telebot"
) -> None:
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
self.data = (
{}
) # key: telebot:bot_id:business_connection_id:message_thread_id:chat_id:user_id
async def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
self.data[_key] = {"state": state, "data": {}}
else:
self.data[_key]["state"] = state
return True
async def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return None
return self.data[_key]["state"]
async def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
del self.data[_key]
return True
async def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
raise RuntimeError(f"MemoryStorage: key {_key} does not exist.")
self.data[_key]["data"][key] = value
return True
async def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
return self.data.get(_key, {}).get("data", {})
async def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = {}
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
async def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = data
return True
def __str__(self) -> str:
return f"<StateMemoryStorage: {self.data}>"
@@ -0,0 +1,274 @@
aiofiles_installed = True
try:
import aiofiles
except ImportError:
aiofiles_installed = False
import os
import pickle
import asyncio
from typing import Optional, Union, Callable, Any
from telebot.asyncio_storage.base_storage import StateStorageBase, StateDataContext
def with_lock(func: Callable) -> Callable:
async def wrapper(self, *args, **kwargs):
async with self.lock:
return await func(self, *args, **kwargs)
return wrapper
class StatePickleStorage(StateStorageBase):
"""
State storage based on pickle file.
.. warning::
This storage is not recommended for production use.
Data may be corrupted. If you face a case where states do not work as expected,
try to use another storage.
.. code-block:: python3
storage = StatePickleStorage()
bot = AsyncTeleBot(token, storage=storage)
:param file_path: Path to file where states will be stored.
:type file_path: str
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
file_path: str = "./.state-save/states.pkl",
prefix="telebot",
separator: Optional[str] = ":",
) -> None:
if not aiofiles_installed:
raise ImportError("Please install aiofiles using `pip install aiofiles`")
self.file_path = file_path
self.prefix = prefix
self.separator = separator
self.lock = asyncio.Lock()
self.create_dir()
async def _read_from_file(self) -> dict:
async with aiofiles.open(self.file_path, "rb") as f:
data = await f.read()
return pickle.loads(data)
async def _write_to_file(self, data: dict) -> None:
async with aiofiles.open(self.file_path, "wb") as f:
await f.write(pickle.dumps(data))
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path, "wb") as file:
pickle.dump({}, file)
@with_lock
async def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
if _key not in data:
data[_key] = {"state": state, "data": {}}
else:
data[_key]["state"] = state
await self._write_to_file(data)
return True
@with_lock
async def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
return data.get(_key, {}).get("state")
@with_lock
async def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
if _key in data:
del data[_key]
await self._write_to_file(data)
return True
return False
@with_lock
async def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
state_data = data.get(_key, {})
state_data["data"][key] = value
if _key not in data:
raise RuntimeError(f"StatePickleStorage: key {_key} does not exist.")
else:
data[_key]["data"][key] = value
await self._write_to_file(data)
return True
@with_lock
async def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
return data.get(_key, {}).get("data", {})
@with_lock
async def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
if _key in data:
data[_key]["data"] = {}
await self._write_to_file(data)
return True
return False
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
@with_lock
async def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self._read_from_file()
data[_key]["data"] = data
await self._write_to_file(data)
return True
def __str__(self) -> str:
return f"StatePickleStorage({self.file_path}, {self.prefix})"
@@ -0,0 +1,339 @@
redis_installed = True
try:
import redis
from redis.asyncio import Redis, ConnectionPool
except ImportError:
redis_installed = False
import json
from typing import Optional, Union, Callable, Coroutine
import asyncio
from telebot.asyncio_storage.base_storage import StateStorageBase, StateDataContext
def async_with_lock(func: Callable[..., Coroutine]) -> Callable[..., Coroutine]:
async def wrapper(self, *args, **kwargs):
async with self.lock:
return await func(self, *args, **kwargs)
return wrapper
def async_with_pipeline(func: Callable[..., Coroutine]) -> Callable[..., Coroutine]:
async def wrapper(self, *args, **kwargs):
async with self.redis.pipeline() as pipe:
pipe.multi()
result = await func(self, pipe, *args, **kwargs)
await pipe.execute()
return result
return wrapper
class StateRedisStorage(StateStorageBase):
"""
State storage based on Redis.
.. code-block:: python3
storage = StateRedisStorage(...)
bot = AsyncTeleBot(token, storage=storage)
:param host: Redis host, default is "localhost".
:type host: str
:param port: Redis port, default is 6379.
:type port: int
:param db: Redis database, default is 0.
:type db: int
:param password: Redis password, default is None.
:type password: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param redis_url: Redis URL, default is None.
:type redis_url: Optional[str]
:param connection_pool: Redis connection pool, default is None.
:type connection_pool: Optional[ConnectionPool]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
host="localhost",
port=6379,
db=0,
password=None,
prefix="telebot",
redis_url=None,
connection_pool: "ConnectionPool" = None,
separator: Optional[str] = ":",
) -> None:
if not redis_installed:
raise ImportError("Please install redis using `pip install redis`")
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
if redis_url:
self.redis = redis.asyncio.from_url(redis_url)
elif connection_pool:
self.redis = Redis(connection_pool=connection_pool)
else:
self.redis = Redis(host=host, port=port, db=db, password=password)
self.lock = asyncio.Lock()
@async_with_lock
@async_with_pipeline
async def set_state(
self,
pipe,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
pipe.hget(_key, "data")
result = await pipe.execute()
data = result[0]
if data is None:
pipe.hset(_key, "data", json.dumps({}))
await pipe.hset(_key, "state", state)
return True
async def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
state_bytes = await self.redis.hget(_key, "state")
return state_bytes.decode("utf-8") if state_bytes else None
async def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
result = await self.redis.delete(_key)
return result > 0
@async_with_lock
@async_with_pipeline
async def set_data(
self,
pipe,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await pipe.hget(_key, "data")
data = await pipe.execute()
data = data[0]
if data is None:
raise RuntimeError(f"StateRedisStorage: key {_key} does not exist.")
else:
data = json.loads(data)
data[key] = value
await pipe.hset(_key, "data", json.dumps(data))
return True
async def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = await self.redis.hget(_key, "data")
return json.loads(data) if data else {}
@async_with_lock
@async_with_pipeline
async def reset_data(
self,
pipe,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if await pipe.exists(_key):
await pipe.hset(_key, "data", "{}")
else:
return False
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
@async_with_lock
@async_with_pipeline
async def save(
self,
pipe,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if await pipe.exists(_key):
await pipe.hset(_key, "data", json.dumps(data))
else:
return False
return True
def migrate_format(self, bot_id: int, prefix: Optional[str] = "telebot_"):
"""
Migrate from old to new format of keys.
Run this function once to migrate all redis existing keys to new format.
Starting from version 4.23.0, the format of keys has been changed:
<key>:value
- Old format: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
- New format:
{prefix}{separator}{bot_id}{separator}{business_connection_id}{separator}{message_thread_id}{separator}{chat_id}{separator}{user_id}: {'state': ..., 'data': {}}
This function will help you to migrate from the old format to the new one in order to avoid data loss.
:param bot_id: Bot ID; To get it, call a getMe request and grab the id from the response.
:type bot_id: int
:param prefix: Prefix for keys, default is "telebot_"(old default value)
:type prefix: Optional[str]
"""
keys = self.redis.keys(f"{prefix}*")
for key in keys:
old_key = key.decode("utf-8")
# old: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
value = self.redis.get(old_key)
value = json.loads(value)
chat_id = old_key[len(prefix) :]
user_id = list(value.keys())[0]
state = value[user_id]["state"]
state_data = value[user_id]["data"]
# set new format
new_key = self._get_key(
int(chat_id), int(user_id), self.prefix, self.separator, bot_id=bot_id
)
self.redis.hset(new_key, "state", state)
self.redis.hset(new_key, "data", json.dumps(state_data))
# delete old key
self.redis.delete(old_key)
def __str__(self) -> str:
# include some connection info
return f"StateRedisStorage({self.redis})"
+155
View File
@@ -0,0 +1,155 @@
"""
Callback data factory's file.
"""
"""
Copyright (c) 2017-2018 Alex Root Junior
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
This file was added during the pull request. The maintainers overlooked that it was copied
"as is" from another project and they do not consider it as a right way to develop a project.
However, due to backward compatibility we had to leave this file in the project with the above
copyright added, as it is required by the original project license.
"""
import typing
class CallbackDataFilter:
"""
Filter for CallbackData.
"""
def __init__(self, factory, config: typing.Dict[str, str]):
self.config = config
self.factory = factory
def check(self, query) -> bool:
"""
Checks if query.data appropriates to specified config
:param query: telebot.types.CallbackQuery
:type query: telebot.types.CallbackQuery
:return: True if query.data appropriates to specified config
:rtype: bool
"""
try:
data = self.factory.parse(query.data)
except ValueError:
return False
for key, value in self.config.items():
if isinstance(value, (list, tuple, set, frozenset)):
if data.get(key) not in value:
return False
elif data.get(key) != value:
return False
return True
class CallbackData:
"""
Callback data factory
This class will help you to work with CallbackQuery
"""
def __init__(self, *parts, prefix: str, sep=':'):
if not isinstance(prefix, str):
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
if not prefix:
raise ValueError("Prefix can't be empty")
if sep in prefix:
raise ValueError(f"Separator {sep!r} can't be used in prefix")
self.prefix = prefix
self.sep = sep
self._part_names = parts
def new(self, *args, **kwargs) -> str:
"""
Generate callback data
:param args: positional parameters of CallbackData instance parts
:param kwargs: named parameters
:return: str
"""
args = list(args)
data = [self.prefix]
for part in self._part_names:
value = kwargs.pop(part, None)
if value is None:
if args:
value = args.pop(0)
else:
raise ValueError(f'Value for {part!r} was not passed!')
if value is not None and not isinstance(value, str):
value = str(value)
if self.sep in value:
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
data.append(value)
if args or kwargs:
raise TypeError('Too many arguments were passed!')
callback_data = self.sep.join(data)
if len(callback_data.encode()) > 64:
raise ValueError('Resulted callback data is too long!')
return callback_data
def parse(self, callback_data: str) -> typing.Dict[str, str]:
"""
Parse data from the callback data
:param callback_data: string, use to telebot.types.CallbackQuery to parse it from string to a dict
:return: dict parsed from callback data
"""
prefix, *parts = callback_data.split(self.sep)
if prefix != self.prefix:
raise ValueError("Passed callback data can't be parsed with that prefix.")
elif len(parts) != len(self._part_names):
raise ValueError('Invalid parts count!')
result = {'@': prefix}
result.update(zip(self._part_names, parts))
return result
def filter(self, **config) -> CallbackDataFilter:
"""
Generate filter
:param config: specified named parameters will be checked with CallbackQuery.data
:return: CallbackDataFilter class
"""
for key in config.keys():
if key not in self._part_names:
raise ValueError(f'Invalid field name {key!r}')
return CallbackDataFilter(self, config)
+455
View File
@@ -0,0 +1,455 @@
from abc import ABC
from typing import Optional, Union
from telebot.handler_backends import State
from telebot import types
from telebot.states import resolve_context
class SimpleCustomFilter(ABC):
"""
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating a simple custom filter.
class ForwardFilter(SimpleCustomFilter):
# Check whether message was forwarded from channel or group.
key = 'is_forwarded'
def check(self, message):
return message.forward_date is not None
"""
key: str = None
def check(self, message):
"""
Perform a check.
"""
pass
class AdvancedCustomFilter(ABC):
"""
Advanced Custom Filter base class.
Create child class with check() method.
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating an advanced custom filter.
class TextStartsFilter(AdvancedCustomFilter):
# Filter to check whether message starts with some text.
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
"""
key: str = None
def check(self, message, text):
"""
Perform a check.
"""
pass
class TextFilter:
"""
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
example of usage is in examples/custom_filters/advanced_text_filter.py
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name: str):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
"""
:meta private:
"""
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
if text is None:
return False
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
if self.equals:
self.equals = self.equals.lower()
elif self.contains:
self.contains = tuple(map(str.lower, self.contains))
elif self.starts_with:
self.starts_with = tuple(map(str.lower, self.starts_with))
elif self.ends_with:
self.ends_with = tuple(map(str.lower, self.ends_with))
if self.equals:
result = self.equals == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([i in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(i) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(i) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(text=['account'])
# your function
"""
key = 'text'
def check(self, message, text):
"""
:meta private:
"""
if isinstance(text, TextFilter):
return text.check(message)
elif isinstance(text, list):
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
key: text
.. code-block:: python3
:caption: Example on using this filter:
# Will respond if any message.text contains word 'account'
@bot.message_handler(text_contains=['account'])
# your function
"""
key = 'text_contains'
def check(self, message, text):
"""
:meta private:
"""
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
Filter to check whether message starts with some text.
.. code-block:: python3
:caption: Example on using this filter:
# Will work if message.text starts with 'sir'.
@bot.message_handler(text_startswith='sir')
# your function
"""
key = 'text_startswith'
def check(self, message, text):
"""
:meta private:
"""
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
Check whether chat_id corresponds to given chat_id.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_id=[99999])
# your function
"""
key = 'chat_id'
def check(self, message, text):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.chat.id in text
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_forwarded=True)
# your function
"""
key = 'is_forwarded'
def check(self, message):
"""
:meta private:
"""
return message.forward_origin is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_reply=True)
# your function
"""
key = 'is_reply'
def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.reply_to_message is not None
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(language_code=['ru'])
# your function
"""
key = 'language_code'
def check(self, message, text):
"""
:meta private:
"""
if isinstance(text, list):
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
Check whether the user is administrator / owner of the chat.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
# your function
"""
key = 'is_chat_admin'
def __init__(self, bot):
self._bot = bot
def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return self._bot.get_chat_member(message.message.chat.id, message.from_user.id).status in ['creator', 'administrator']
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(state=1)
# your function
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
def check(self, message, text):
"""
:meta private:
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(message, self.bot._user.id)
if chat_id is None:
chat_id = user_id # May change in future
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
user_state = self.bot.current_states.get_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
# CHANGED BEHAVIOUR
if text == "*" and user_state is not None:
return True
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
return False
class IsDigitFilter(SimpleCustomFilter):
"""
Filter to check whether the string is made up of only digits.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_digit=True)
# your function
"""
key = 'is_digit'
def check(self, message):
"""
:meta private:
"""
return message.text.isdigit()
+3
View File
@@ -0,0 +1,3 @@
"""
A folder with asynchronous and synchronous extensions.
"""
+10
View File
@@ -0,0 +1,10 @@
"""
A folder with all the async extensions.
"""
from .webhooks import AsyncWebhookListener
__all__ = [
"AsyncWebhookListener"
]
+123
View File
@@ -0,0 +1,123 @@
"""
This file is used by AsyncTeleBot.run_webhooks() function.
Fastapi and starlette(0.20.2+) libraries are required to run this script.
"""
# modules required for running this script
fastapi_installed = True
try:
import fastapi
from fastapi.responses import JSONResponse
from fastapi.requests import Request
from uvicorn import Server, Config
except ImportError:
fastapi_installed = False
import asyncio
from telebot.types import Update
from typing import Optional
class AsyncWebhookListener:
def __init__(self, bot,
secret_token: str,
host: Optional[str]="127.0.0.1",
port: Optional[int]=443,
ssl_context: Optional[tuple]=None,
url_path: Optional[str]=None,
) -> None:
"""
Aynchronous implementation of webhook listener
for asynchronous version of telebot.
Not supposed to be used manually by user.
Use AsyncTeleBot.run_webhooks() instead.
:param bot: AsyncTeleBot instance.
:type bot: telebot.async_telebot.AsyncTeleBot
:param secret_token: Telegram secret token
:type secret_token: str
:param host: Webhook host
:type host: str
:param port: Webhook port
:type port: int
:param ssl_context: SSL context
:type ssl_context: tuple
:param url_path: Webhook url path
:type url_path: str
:raises ImportError: If FastAPI or uvicorn is not installed.
:raises ImportError: If Starlette version is too old.
:return: None
"""
self._check_dependencies()
self.app = fastapi.FastAPI()
self._secret_token = secret_token
self._bot = bot
self._port = port
self._host = host
self._ssl_context = ssl_context
self._url_path = url_path
self._prepare_endpoint_urls()
def _check_dependencies(self):
if not fastapi_installed:
raise ImportError('Fastapi or uvicorn is not installed. Please install it via pip.')
import starlette
if starlette.__version__ < '0.20.2':
raise ImportError('Starlette version is too old. Please upgrade it: `pip3 install starlette -U`')
return
def _prepare_endpoint_urls(self):
self.app.add_api_route(endpoint=self.process_update,path= self._url_path, methods=["POST"])
async def process_update(self, request: Request, update: dict):
"""
Processes updates.
:meta private:
"""
# header containsX-Telegram-Bot-Api-Secret-Token
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != self._secret_token:
# secret token didn't match
return JSONResponse(status_code=403, content={"error": "Forbidden"})
if request.headers.get('content-type') == 'application/json':
json_string = update
asyncio.create_task(self._bot.process_new_updates([Update.de_json(json_string)]))
return JSONResponse('', status_code=200)
return JSONResponse(status_code=403, content={"error": "Forbidden"})
async def run_app(self):
"""
Run app with the given parameters to init.
Not supposed to be used manually by user.
:return: None
"""
config = Config(app=self.app,
host=self._host,
port=self._port,
ssl_certfile=self._ssl_context[0],
ssl_keyfile=self._ssl_context[1]
)
server = Server(config)
await server.serve()
await self._bot.close_session()
+31
View File
@@ -0,0 +1,31 @@
from watchdog.events import FileSystemEventHandler
from watchdog.events import FileSystemEvent
import psutil
import os
import sys
import logging
logger = logging.getLogger('TeleBot')
class EventHandler(FileSystemEventHandler):
def on_any_event(self, event: FileSystemEvent):
logger.info('* Detected changes in: %s, reloading', (event.src_path))
restart_file()
def restart_file():
try:
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
os.close(handler.fd)
except OSError:
pass
except Exception as e:
logger.error(e)
python = sys.executable
if os.name == 'nt':
os.execv(sys.executable, ['python'] + sys.argv)
else:
os.execl(python, python, *sys.argv)
+10
View File
@@ -0,0 +1,10 @@
"""
A folder with all the sync extensions.
"""
from .webhooks import SyncWebhookListener
__all__ = [
"SyncWebhookListener"
]
+116
View File
@@ -0,0 +1,116 @@
"""
This file is used by TeleBot.run_webhooks() function.
Fastapi is required to run this script.
"""
# modules required for running this script
fastapi_installed = True
try:
import fastapi
from fastapi.responses import JSONResponse
from fastapi.requests import Request
import uvicorn
except ImportError:
fastapi_installed = False
from telebot.types import Update
from typing import Optional
class SyncWebhookListener:
def __init__(self, bot,
secret_token: str,
host: Optional[str]="127.0.0.1",
port: Optional[int]=443,
ssl_context: Optional[tuple]=None,
url_path: Optional[str]=None,
) -> None:
"""
Synchronous implementation of webhook listener
for synchronous version of telebot.
Not supposed to be used manually by user.
Use TeleBot.run_webhooks() instead.
:param bot: TeleBot instance.
:type bot: telebot.TeleBot
:param secret_token: Telegram secret token
:type secret_token: str
:param host: Webhook host
:type host: str
:param port: Webhook port
:type port: int
:param ssl_context: SSL context
:type ssl_context: tuple
:param url_path: Webhook url path
:type url_path: str
:raises ImportError: If FastAPI or uvicorn is not installed.
:raises ImportError: If Starlette version is too old.
:return: None
"""
self._check_dependencies()
self.app = fastapi.FastAPI()
self._secret_token = secret_token
self._bot = bot
self._port = port
self._host = host
self._ssl_context = ssl_context
self._url_path = url_path
self._prepare_endpoint_urls()
@staticmethod
def _check_dependencies():
if not fastapi_installed:
raise ImportError('Fastapi or uvicorn is not installed. Please install it via pip.')
import starlette
if starlette.__version__ < '0.20.2':
raise ImportError('Starlette version is too old. Please upgrade it: `pip3 install starlette -U`')
return
def _prepare_endpoint_urls(self):
self.app.add_api_route(endpoint=self.process_update,path= self._url_path, methods=["POST"])
def process_update(self, request: Request, update: dict):
"""
Processes updates.
:meta private:
"""
# header containsX-Telegram-Bot-Api-Secret-Token
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != self._secret_token:
# secret token didn't match
return JSONResponse(status_code=403, content={"error": "Forbidden"})
if request.headers.get('content-type') == 'application/json':
self._bot.process_new_updates([Update.de_json(update)])
return JSONResponse('', status_code=200)
return JSONResponse(status_code=403, content={"error": "Forbidden"})
def run_app(self):
"""
Run app with the given parameters to init.
Not supposed to be used manually by user.
:return: None
"""
uvicorn.run(app=self.app,
host=self._host,
port=self._port,
ssl_certfile=self._ssl_context[0],
ssl_keyfile=self._ssl_context[1]
)
+474
View File
@@ -0,0 +1,474 @@
"""
Markdown & HTML formatting functions.
.. versionadded:: 4.5.1
"""
import re
import html
from typing import Optional, List, Dict
def format_text(*args, separator="\n"):
"""
Formats a list of strings into a single string.
.. code:: python3
format_text( # just an example
mbold('Hello'),
mitalic('World')
)
:param args: Strings to format.
:type args: :obj:`str`
:param separator: The separator to use between each string.
:type separator: :obj:`str`
:return: The formatted string.
:rtype: :obj:`str`
"""
return separator.join(args)
def escape_html(content: str) -> str:
"""
Escapes HTML characters in a string of HTML.
:param content: The string of HTML to escape.
:type content: :obj:`str`
:return: The escaped string.
:rtype: :obj:`str`
"""
return html.escape(content)
def escape_markdown(content: str) -> str:
"""
Escapes Markdown characters in a string of Markdown.
Credits to: simonsmh
:param content: The string of Markdown to escape.
:type content: :obj:`str`
:return: The escaped string.
:rtype: :obj:`str`
"""
parse = re.sub(r"([_*\[\]()~`>\#\+\-=|\.!\{\}\\])", r"\\\1", content)
reparse = re.sub(r"\\\\([_*\[\]()~`>\#\+\-=|\.!\{\}\\])", r"\1", parse)
return reparse
def mbold(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted bold string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '*{}*'.format(escape_markdown(content) if escape else content)
def hbold(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted bold string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<b>{}</b>'.format(escape_html(content) if escape else content)
def mitalic(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted italic string.
:param content: The string to italicize.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '_{}_\r'.format(escape_markdown(content) if escape else content)
def hitalic(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted italic string.
:param content: The string to italicize.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<i>{}</i>'.format(escape_html(content) if escape else content)
def munderline(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted underline string.
:param content: The string to underline.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '__{}__'.format(escape_markdown(content) if escape else content)
def hunderline(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted underline string.
:param content: The string to underline.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<u>{}</u>'.format(escape_html(content) if escape else content)
def mstrikethrough(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted strikethrough string.
:param content: The string to strikethrough.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '~{}~'.format(escape_markdown(content) if escape else content)
def hstrikethrough(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted strikethrough string.
:param content: The string to strikethrough.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<s>{}</s>'.format(escape_html(content) if escape else content)
def mspoiler(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted spoiler string.
:param content: The string to spoiler.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '||{}||'.format(escape_markdown(content) if escape else content)
def hspoiler(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted spoiler string.
:param content: The string to spoiler.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<tg-spoiler>{}</tg-spoiler>'.format(escape_html(content) if escape else content)
def mlink(content: str, url: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted link string.
:param content: The string to link.
:type content: :obj:`str`
:param url: The URL to link to.
:type url: str
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '[{}]({})'.format(escape_markdown(content), escape_markdown(url) if escape else content)
def hlink(content: str, url: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted link string.
:param content: The string to link.
:type content: :obj:`str`
:param url: The URL to link to.
:type url: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<a href="{}">{}</a>'.format(escape_html(url), escape_html(content) if escape else content)
def mcode(content: str, language: str="", escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted code string.
:param content: The string to code.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '```{}\n{}```'.format(language, escape_markdown(content) if escape else content)
def hcode(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted code string.
:param content: The string to code.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<code>{}</code>'.format(escape_html(content) if escape else content)
def hpre(content: str, escape: Optional[bool]=True, language: str="") -> str:
"""
Returns an HTML-formatted preformatted string.
:param content: The string to preformatted.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<pre><code class="{}">{}</code></pre>'.format(language, escape_html(content) if escape else content)
def hide_link(url: str) -> str:
"""
Hide url of an image.
:param url: The url of the image.
:type url: :obj:`str`
:return: The hidden url.
:rtype: :obj:`str`
"""
return f'<a href="{url}">&#8288;</a>'
def mcite(content: str, escape: Optional[bool] = True, expandable: Optional[bool] = False) -> str:
"""
Returns a Markdown-formatted block-quotation string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:param expandable: True if you need the quote to be expandable. Defaults to False.
:type expandable: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
content = escape_markdown(content) if escape else content
content = "\n".join([">" + line for line in content.split("\n")])
if expandable:
return f"**{content}||"
return content
def hcite(content: str, escape: Optional[bool] = True, expandable: Optional[bool] = False) -> str:
"""
Returns a html-formatted block-quotation string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:param expandable: True if you need the quote to be expandable. Defaults to False.
:type expandable: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return "<blockquote{}>{}</blockquote>".format(
" expandable" if expandable else "",
escape_html(content) if escape else content,
)
def apply_html_entities(text: str, entities: Optional[List], custom_subs: Optional[Dict[str, str]]) -> str:
"""
Author: @sviat9440
Updaters: @badiboy, @EgorKhabarov
Message: "*Test* parse _formatting_, [url](https://example.com), [text_mention](tg://user?id=123456) and mention @username"
.. code-block:: python3
:caption: Example:
apply_html_entities(text, entities)
>> "<b>Test</b> parse <i>formatting</i>, <a href=\"https://example.com\">url</a>, <a href=\"tg://user?id=123456\">text_mention</a> and mention @username"
Custom subs:
You can customize the substitutes. By default, there is no substitute for the entities: hashtag, bot_command, email. You can add or modify substitute an existing entity.
.. code-block:: python3
:caption: Example:
apply_html_entities(
text,
entities,
{"bold": "<strong class=\"example\">{text}</strong>", "italic": "<i class=\"example\">{text}</i>", "mention": "<a href={url}>{text}</a>"},
)
>> "<strong class=\"example\">Test</strong> parse <i class=\"example\">formatting</i>, <a href=\"https://example.com\">url</a> and <a href=\"tg://user?id=123456\">text_mention</a> and mention <a href=\"https://t.me/username\">@username</a>"
"""
if not entities:
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
_subs = {
"bold": "<b>{text}</b>",
"italic": "<i>{text}</i>",
"pre": "<pre>{text}</pre>",
"code": "<code>{text}</code>",
# "url": "<a href=\"{url}\">{text}</a>", # @badiboy plain URLs have no text and do not need tags
"text_link": "<a href=\"{url}\">{text}</a>",
"strikethrough": "<s>{text}</s>",
"underline": "<u>{text}</u>",
"spoiler": "<span class=\"tg-spoiler\">{text}</span>",
"custom_emoji": "<tg-emoji emoji-id=\"{custom_emoji_id}\">{text}</tg-emoji>",
"blockquote": "<blockquote>{text}</blockquote>",
"expandable_blockquote": "<blockquote expandable>{text}</blockquote>",
}
if custom_subs:
for key, value in custom_subs.items():
_subs[key] = value
utf16_text = text.encode("utf-16-le")
html_text = ""
def func(upd_text, subst_type=None, url=None, user=None, custom_emoji_id=None):
upd_text = upd_text.decode("utf-16-le")
if subst_type == "text_mention":
subst_type = "text_link"
url = "tg://user?id={0}".format(user.id)
elif subst_type == "mention":
url = "https://t.me/{0}".format(upd_text[1:])
upd_text = upd_text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
if not subst_type or not _subs.get(subst_type):
return upd_text
subs = _subs.get(subst_type)
if subst_type == "custom_emoji":
return subs.format(text=upd_text, custom_emoji_id=custom_emoji_id)
return subs.format(text=upd_text, url=url)
offset = 0
start_index = 0
end_index = 0
for entity in entities:
if entity.offset > offset:
# when the offset is not 0: for example, a __b__
# we need to add the text before the entity to the html_text
html_text += func(utf16_text[offset * 2: entity.offset * 2])
offset = entity.offset
new_string = func(utf16_text[offset * 2: (offset + entity.length) * 2], subst_type=entity.type,
url=entity.url, user=entity.user, custom_emoji_id=entity.custom_emoji_id)
start_index = len(html_text)
html_text += new_string
offset += entity.length
end_index = len(html_text)
elif entity.offset == offset:
new_string = func(utf16_text[offset * 2: (offset + entity.length) * 2], subst_type=entity.type,
url=entity.url, user=entity.user, custom_emoji_id=entity.custom_emoji_id)
start_index = len(html_text)
html_text += new_string
end_index = len(html_text)
offset += entity.length
else:
# Here we are processing nested entities.
# We shouldn't update offset, because they are the same as entity before.
# And, here we are replacing previous string with a new html-rendered text(previous string is already html-rendered,
# And we don't change it).
entity_string = html_text[start_index: end_index].encode("utf-16-le")
formatted_string = func(entity_string, subst_type=entity.type, url=entity.url, user=entity.user,
custom_emoji_id=entity.custom_emoji_id). \
replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
html_text = html_text[:start_index] + formatted_string + html_text[end_index:]
end_index = len(html_text)
if offset * 2 < len(utf16_text):
html_text += func(utf16_text[offset * 2:])
return html_text
+258
View File
@@ -0,0 +1,258 @@
import os
import pickle
import threading
from telebot import apihelper
try:
from redis import Redis
redis_installed = True
except:
redis_installed = False
# backward compatibility
from telebot.states import State, StatesGroup
class HandlerBackend(object):
"""
Class for saving (next step|reply) handlers.
:meta private:
"""
def __init__(self, handlers=None):
if handlers is None:
handlers = {}
self.handlers = handlers
def register_handler(self, handler_group_id, handler):
raise NotImplementedError()
def clear_handlers(self, handler_group_id):
raise NotImplementedError()
def get_handlers(self, handler_group_id):
raise NotImplementedError()
class MemoryHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def register_handler(self, handler_group_id, handler):
if handler_group_id in self.handlers:
self.handlers[handler_group_id].append(handler)
else:
self.handlers[handler_group_id] = [handler]
def clear_handlers(self, handler_group_id):
self.handlers.pop(handler_group_id, None)
def get_handlers(self, handler_group_id):
return self.handlers.pop(handler_group_id, None)
def load_handlers(self, filename, del_file_after_loading):
raise NotImplementedError()
class FileHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def __init__(self, handlers=None, filename='./.handler-saves/handlers.save', delay=120):
super(FileHandlerBackend, self).__init__(handlers)
self.filename = filename
self.delay = delay
self.timer = threading.Timer(delay, self.save_handlers)
def register_handler(self, handler_group_id, handler):
if handler_group_id in self.handlers:
self.handlers[handler_group_id].append(handler)
else:
self.handlers[handler_group_id] = [handler]
self.start_save_timer()
def clear_handlers(self, handler_group_id):
self.handlers.pop(handler_group_id, None)
self.start_save_timer()
def get_handlers(self, handler_group_id):
handlers = self.handlers.pop(handler_group_id, None)
self.start_save_timer()
return handlers
def start_save_timer(self):
if not self.timer.is_alive():
if self.delay <= 0:
self.save_handlers()
else:
self.timer = threading.Timer(self.delay, self.save_handlers)
self.timer.start()
def save_handlers(self):
self.dump_handlers(self.handlers, self.filename)
def load_handlers(self, filename=None, del_file_after_loading=True):
if not filename:
filename = self.filename
tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading)
if tmp is not None:
self.handlers.update(tmp)
@staticmethod
def dump_handlers(handlers, filename, file_mode="wb"):
dirs = filename.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
with open(filename + ".tmp", file_mode) as file:
if (apihelper.CUSTOM_SERIALIZER is None):
pickle.dump(handlers, file)
else:
apihelper.CUSTOM_SERIALIZER.dump(handlers, file)
if os.path.isfile(filename):
os.remove(filename)
os.rename(filename + ".tmp", filename)
@staticmethod
def return_load_handlers(filename, del_file_after_loading=True):
if os.path.isfile(filename) and os.path.getsize(filename) > 0:
with open(filename, "rb") as file:
if (apihelper.CUSTOM_SERIALIZER is None):
handlers = pickle.load(file)
else:
handlers = apihelper.CUSTOM_SERIALIZER.load(file)
if del_file_after_loading:
os.remove(filename)
return handlers
class RedisHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def __init__(self, handlers=None, host='localhost', port=6379, db=0, prefix='telebot', password=None):
super(RedisHandlerBackend, self).__init__(handlers)
if not redis_installed:
raise Exception("Redis is not installed. Install it via 'pip install redis'")
self.prefix = prefix
self.redis = Redis(host, port, db, password)
def _key(self, handle_group_id):
return ':'.join((self.prefix, str(handle_group_id)))
def register_handler(self, handler_group_id, handler):
handlers = []
value = self.redis.get(self._key(handler_group_id))
if value:
handlers = pickle.loads(value)
handlers.append(handler)
self.redis.set(self._key(handler_group_id), pickle.dumps(handlers))
def clear_handlers(self, handler_group_id):
self.redis.delete(self._key(handler_group_id))
def get_handlers(self, handler_group_id):
handlers = None
value = self.redis.get(self._key(handler_group_id))
if value:
handlers = pickle.loads(value)
self.clear_handlers(handler_group_id)
return handlers
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
Set update_sensitive=True if you want to get different updates on
different functions. For example, if you want to handle pre_process for
message update, then you will have to create pre_process_message function, and
so on. Same applies to post_process.
.. note::
If you want to use middleware, you have to set use_class_middlewares=True in your
TeleBot instance.
.. code-block:: python3
:caption: Example of class-based middlewares.
class MyMiddleware(BaseMiddleware):
def __init__(self):
self.update_sensitive = True
self.update_types = ['message', 'edited_message']
def pre_process_message(self, message, data):
# only message update here
pass
def post_process_message(self, message, data, exception):
pass # only message update here for post_process
def pre_process_edited_message(self, message, data):
# only edited_message update here
pass
def post_process_edited_message(self, message, data, exception):
pass # only edited_message update here for post_process
"""
update_sensitive: bool = False
def __init__(self):
pass
def pre_process(self, message, data):
raise NotImplementedError
def post_process(self, message, data, exception):
raise NotImplementedError
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass
class ContinueHandling:
"""
Class for continue updates in handlers.
Just return instance of this class
in handlers to continue process.
.. code-block:: python3
:caption: Example of using ContinueHandling
@bot.message_handler(commands=['start'])
def start(message):
bot.send_message(message.chat.id, 'Hello World!')
return ContinueHandling()
@bot.message_handler(commands=['start'])
def start2(message):
bot.send_message(message.chat.id, 'Hello World2!')
"""
def __init__(self) -> None:
pass
+84
View File
@@ -0,0 +1,84 @@
import random
import string
from io import BytesIO
try:
# noinspection PyPackageRequirements
from PIL import Image
pil_imported = True
except ImportError:
pil_imported = False
def is_string(var) -> bool:
"""
Returns True if the given object is a string.
"""
return isinstance(var, str)
def is_dict(var) -> bool:
"""
Returns True if the given object is a dictionary.
:param var: object to be checked
:type var: :obj:`object`
:return: True if the given object is a dictionary.
:rtype: :obj:`bool`
"""
return isinstance(var, dict)
def is_bytes(var) -> bool:
"""
Returns True if the given object is a bytes object.
:param var: object to be checked
:type var: :obj:`object`
:return: True if the given object is a bytes object.
:rtype: :obj:`bool`
"""
return isinstance(var, bytes)
def is_pil_image(var) -> bool:
"""
Returns True if the given object is a PIL.Image.Image object.
:param var: object to be checked
:type var: :obj:`object`
:return: True if the given object is a PIL.Image.Image object.
:rtype: :obj:`bool`
"""
return pil_imported and isinstance(var, Image.Image)
def pil_image_to_file(image, extension='JPEG', quality='web_low'):
if pil_imported:
photoBuffer = BytesIO()
image.convert('RGB').save(photoBuffer, extension, quality=quality)
photoBuffer.seek(0)
return photoBuffer
else:
raise RuntimeError('PIL module is not imported')
def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
# https://stackoverflow.com/a/312464/9935473
for i in range(0, len(lst), n):
yield lst[i:i + n]
def generate_random_token() -> str:
"""
Generates a random token consisting of letters and digits, 16 characters long.
:return: a random token
:rtype: :obj:`str`
"""
return ''.join(random.sample(string.ascii_letters, 16))
+128
View File
@@ -0,0 +1,128 @@
"""
Contains classes for states and state groups.
"""
from telebot import types
class State:
"""
Class representing a state.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init__(self) -> None:
self.name: str = None
self.group: StatesGroup = None
def __str__(self) -> str:
return f"<{self.name}>"
class StatesGroup:
"""
Class representing common states.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init_subclass__(cls) -> None:
state_list = []
for name, value in cls.__dict__.items():
if (
not name.startswith("__")
and not callable(value)
and isinstance(value, State)
):
# change value of that variable
value.name = ":".join((cls.__name__, name))
value.group = cls
state_list.append(value)
cls._state_list = state_list
@classmethod
def state_list(self):
return self._state_list
def resolve_context(message, bot_id: int) -> tuple:
# chat_id, user_id, business_connection_id, bot_id, message_thread_id
# message, edited_message, channel_post, edited_channel_post, business_message, edited_business_message
if isinstance(message, types.Message):
return (
message.chat.id,
message.from_user.id,
message.business_connection_id,
bot_id,
message.message_thread_id if message.is_topic_message else None,
)
elif isinstance(message, types.CallbackQuery): # callback_query
return (
message.message.chat.id,
message.from_user.id,
message.message.business_connection_id,
bot_id,
(
message.message.message_thread_id
if message.message.is_topic_message
else None
),
)
elif isinstance(message, types.BusinessConnection): # business_connection
return (message.user_chat_id, message.user.id, message.id, bot_id, None)
elif isinstance(
message, types.BusinessMessagesDeleted
): # deleted_business_messages
return (
message.chat.id,
message.chat.id,
message.business_connection_id,
bot_id,
None,
)
elif isinstance(message, types.MessageReactionUpdated): # message_reaction
return (message.chat.id, message.user.id, None, bot_id, None)
elif isinstance(
message, types.MessageReactionCountUpdated
): # message_reaction_count
return (message.chat.id, None, None, bot_id, None)
elif isinstance(message, types.InlineQuery): # inline_query
return (None, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.ChosenInlineResult): # chosen_inline_result
return (None, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.ShippingQuery): # shipping_query
return (None, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.PreCheckoutQuery): # pre_checkout_query
return (None, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.PollAnswer): # poll_answer
return (None, message.user.id, None, bot_id, None)
elif isinstance(message, types.ChatMemberUpdated): # chat_member # my_chat_member
return (message.chat.id, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.ChatJoinRequest): # chat_join_request
return (message.chat.id, message.from_user.id, None, bot_id, None)
elif isinstance(message, types.ChatBoostRemoved): # removed_chat_boost
return (
message.chat.id,
message.source.user.id if message.source else None,
None,
bot_id,
None,
)
elif isinstance(message, types.ChatBoostUpdated): # chat_boost
return (
message.chat.id,
message.boost.source.user.id if message.boost.source else None,
None,
bot_id,
None,
)
else:
pass # not yet supported :(
@@ -0,0 +1,7 @@
from .context import StateContext
from .middleware import StateMiddleware
__all__ = [
"StateContext",
"StateMiddleware",
]
+153
View File
@@ -0,0 +1,153 @@
from telebot.states import State, StatesGroup
from telebot.types import CallbackQuery, Message
from telebot.async_telebot import AsyncTeleBot
from telebot.states import resolve_context
from typing import Union
class StateContext:
"""
Class representing a state context.
Passed through a middleware to provide easy way to set states.
.. code-block:: python3
@bot.message_handler(commands=['start'])
async def start_ex(message: types.Message, state_context: StateContext):
await state_context.set(MyStates.name)
await bot.send_message(message.chat.id, 'Hi, write me a name', reply_to_message_id=message.message_id)
# also, state_context.data(), .add_data(), .reset_data(), .delete() methods available.
"""
def __init__(self, message: Union[Message, CallbackQuery], bot: str) -> None:
self.message: Union[Message, CallbackQuery] = message
self.bot: AsyncTeleBot = bot
self.bot_id = self.bot.bot_id
async def set(self, state: Union[State, str]) -> bool:
"""
Set state for current user.
:param state: State object or state name.
:type state: Union[State, str]
.. code-block:: python3
@bot.message_handler(commands=['start'])
async def start_ex(message: types.Message, state_context: StateContext):
await state_context.set(MyStates.name)
await bot.send_message(message.chat.id, 'Hi, write me a name', reply_to_message_id=message.message_id)
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
if isinstance(state, State):
state = state.name
return await self.bot.set_state(
chat_id=chat_id,
user_id=user_id,
state=state,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
)
async def get(self) -> str:
"""
Get current state for current user.
:return: Current state name.
:rtype: str
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
return await self.bot.get_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
)
async def delete(self) -> bool:
"""
Deletes state and data for current user.
.. warning::
This method deletes state and associated data for current user.
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
return await self.bot.delete_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
)
async def reset_data(self) -> bool:
"""
Reset data for current user.
State will not be changed.
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
return await self.bot.reset_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
)
def data(self) -> dict:
"""
Get data for current user.
.. code-block:: python3
with state_context.data() as data:
print(data)
data['name'] = 'John'
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
return self.bot.retrieve_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
)
async def add_data(self, **kwargs) -> None:
"""
Add data for current user.
:param kwargs: Data to add.
:type kwargs: dict
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = (
resolve_context(self.message, self.bot.bot_id)
)
return await self.bot.add_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
**kwargs
)
@@ -0,0 +1,21 @@
from telebot.asyncio_handler_backends import BaseMiddleware
from telebot.async_telebot import AsyncTeleBot
from telebot.states.sync.context import StateContext
from telebot.util import update_types
from telebot import types
class StateMiddleware(BaseMiddleware):
def __init__(self, bot: AsyncTeleBot) -> None:
self.update_sensitive = False
self.update_types = update_types
self.bot: AsyncTeleBot = bot
async def pre_process(self, message, data):
state_context = StateContext(message, self.bot)
data["state_context"] = state_context
data["state"] = state_context # 2 ways to access state context
async def post_process(self, message, data, exception):
pass
+7
View File
@@ -0,0 +1,7 @@
from .context import StateContext
from .middleware import StateMiddleware
__all__ = [
'StateContext',
'StateMiddleware',
]
+143
View File
@@ -0,0 +1,143 @@
from telebot.states import State, StatesGroup
from telebot.types import CallbackQuery, Message
from telebot import TeleBot, types
from telebot.states import resolve_context
from typing import Union
class StateContext():
"""
Class representing a state context.
Passed through a middleware to provide easy way to set states.
.. code-block:: python3
@bot.message_handler(commands=['start'])
def start_ex(message: types.Message, state_context: StateContext):
state_context.set(MyStates.name)
bot.send_message(message.chat.id, 'Hi, write me a name', reply_to_message_id=message.message_id)
# also, state_context.data(), .add_data(), .reset_data(), .delete() methods available.
"""
def __init__(self, message: Union[Message, CallbackQuery], bot: str) -> None:
self.message: Union[Message, CallbackQuery] = message
self.bot: TeleBot = bot
self.bot_id = self.bot.bot_id
def set(self, state: Union[State, str]) -> bool:
"""
Set state for current user.
:param state: State object or state name.
:type state: Union[State, str]
.. code-block:: python3
@bot.message_handler(commands=['start'])
def start_ex(message: types.Message, state_context: StateContext):
state_context.set(MyStates.name)
bot.send_message(message.chat.id, 'Hi, write me a name', reply_to_message_id=message.message_id)
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
if isinstance(state, State):
state = state.name
return self.bot.set_state(
chat_id=chat_id,
user_id=user_id,
state=state,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
def get(self) -> str:
"""
Get current state for current user.
:return: Current state name.
:rtype: str
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
return self.bot.get_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
def delete(self) -> bool:
"""
Deletes state and data for current user.
.. warning::
This method deletes state and associated data for current user.
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
return self.bot.delete_state(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
def reset_data(self) -> bool:
"""
Reset data for current user.
State will not be changed.
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
return self.bot.reset_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
def data(self) -> dict:
"""
Get data for current user.
.. code-block:: python3
with state_context.data() as data:
print(data)
data['name'] = 'John'
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
return self.bot.retrieve_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id
)
def add_data(self, **kwargs) -> None:
"""
Add data for current user.
:param kwargs: Data to add.
:type kwargs: dict
"""
chat_id, user_id, business_connection_id, bot_id, message_thread_id = resolve_context(self.message, self.bot.bot_id)
return self.bot.add_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
bot_id=bot_id,
message_thread_id=message_thread_id,
**kwargs
)
+21
View File
@@ -0,0 +1,21 @@
from telebot.handler_backends import BaseMiddleware
from telebot import TeleBot
from telebot.states.sync.context import StateContext
from telebot.util import update_types
from telebot import types
class StateMiddleware(BaseMiddleware):
def __init__(self, bot: TeleBot) -> None:
self.update_sensitive = False
self.update_types = update_types
self.bot: TeleBot = bot
def pre_process(self, message, data):
state_context = StateContext(message, self.bot)
data['state_context'] = state_context
data['state'] = state_context # 2 ways to access state context
def post_process(self, message, data, exception):
pass
+13
View File
@@ -0,0 +1,13 @@
from telebot.storage.memory_storage import StateMemoryStorage
from telebot.storage.redis_storage import StateRedisStorage
from telebot.storage.pickle_storage import StatePickleStorage
from telebot.storage.base_storage import StateDataContext, StateStorageBase
__all__ = [
"StateStorageBase",
"StateDataContext",
"StateMemoryStorage",
"StateRedisStorage",
"StatePickleStorage",
]
+118
View File
@@ -0,0 +1,118 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
def get_state(self, chat_id, user_id):
raise NotImplementedError
def get_interactive_data(self, chat_id, user_id):
raise NotImplementedError
def save(self, chat_id, user_id, data):
raise NotImplementedError
def _get_key(
self,
chat_id: int,
user_id: int,
prefix: str,
separator: str,
business_connection_id: str = None,
message_thread_id: int = None,
bot_id: int = None,
) -> str:
"""
Convert parameters to a key.
"""
params = [prefix]
if bot_id:
params.append(str(bot_id))
if business_connection_id:
params.append(business_connection_id)
if message_thread_id:
params.append(str(message_thread_id))
params.append(str(chat_id))
params.append(str(user_id))
return separator.join(params)
class StateDataContext:
"""
Class for data.
"""
def __init__(
self,
obj,
chat_id,
user_id,
business_connection_id=None,
message_thread_id=None,
bot_id=None,
):
self.obj = obj
res = obj.get_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
self.data = copy.deepcopy(res)
self.chat_id = chat_id
self.user_id = user_id
self.bot_id = bot_id
self.business_connection_id = business_connection_id
self.message_thread_id = message_thread_id
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
return self.obj.save(
self.chat_id,
self.user_id,
self.data,
self.business_connection_id,
self.message_thread_id,
self.bot_id,
)
+225
View File
@@ -0,0 +1,225 @@
from telebot.storage.base_storage import StateStorageBase, StateDataContext
from typing import Optional, Union
class StateMemoryStorage(StateStorageBase):
"""
Memory storage for states.
Stores states in memory as a dictionary.
.. code-block:: python3
storage = StateMemoryStorage()
bot = TeleBot(token, storage=storage)
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
"""
def __init__(
self, separator: Optional[str] = ":", prefix: Optional[str] = "telebot"
) -> None:
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
self.data = (
{}
) # key: telebot:bot_id:business_connection_id:message_thread_id:chat_id:user_id
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
self.data[_key] = {"state": state, "data": {}}
else:
self.data[_key]["state"] = state
return True
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return None
return self.data[_key]["state"]
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
del self.data[_key]
return True
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
raise RuntimeError(f"StateMemoryStorage: key {_key} does not exist.")
self.data[_key]["data"][key] = value
return True
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
return self.data.get(_key, {}).get("data", {})
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = {}
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = data
return True
def __str__(self) -> str:
return f"<StateMemoryStorage: {self.data}>"
+263
View File
@@ -0,0 +1,263 @@
import os
import pickle
import threading
from typing import Optional, Union, Callable
from telebot.storage.base_storage import StateStorageBase, StateDataContext
def with_lock(func: Callable) -> Callable:
def wrapper(self, *args, **kwargs):
with self.lock:
return func(self, *args, **kwargs)
return wrapper
class StatePickleStorage(StateStorageBase):
"""
State storage based on pickle file.
.. warning::
This storage is not recommended for production use.
Data may be corrupted. If you face a case where states do not work as expected,
try to use another storage.
.. code-block:: python3
storage = StatePickleStorage()
bot = TeleBot(token, storage=storage)
:param file_path: Path to file where states will be stored.
:type file_path: str
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
file_path: str = "./.state-save/states.pkl",
prefix="telebot",
separator: Optional[str] = ":",
) -> None:
self.file_path = file_path
self.prefix = prefix
self.separator = separator
self.lock = threading.Lock()
self.create_dir()
def _read_from_file(self) -> dict:
with open(self.file_path, "rb") as f:
return pickle.load(f)
def _write_to_file(self, data: dict) -> None:
with open(self.file_path, "wb") as f:
pickle.dump(data, f)
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path, "wb") as file:
pickle.dump({}, file)
@with_lock
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key not in data:
data[_key] = {"state": state, "data": {}}
else:
data[_key]["state"] = state
self._write_to_file(data)
return True
@with_lock
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
return data.get(_key, {}).get("state")
@with_lock
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key in data:
del data[_key]
self._write_to_file(data)
return True
return False
@with_lock
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
state_data = data.get(_key, {})
state_data["data"][key] = value
if _key not in data:
raise RuntimeError(f"PickleStorage: key {_key} does not exist.")
self._write_to_file(data)
return True
@with_lock
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
return data.get(_key, {}).get("data", {})
@with_lock
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key in data:
data[_key]["data"] = {}
self._write_to_file(data)
return True
return False
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
@with_lock
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
data[_key]["data"] = data
self._write_to_file(data)
return True
def __str__(self) -> str:
return f"StatePickleStorage({self.file_path}, {self.prefix})"
+324
View File
@@ -0,0 +1,324 @@
import json
from telebot.storage.base_storage import StateStorageBase, StateDataContext
from typing import Optional, Union
redis_installed = True
try:
import redis
except ImportError:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
State storage based on Redis.
.. code-block:: python3
storage = StateRedisStorage(...)
bot = TeleBot(token, storage=storage)
:param host: Redis host, default is "localhost".
:type host: str
:param port: Redis port, default is 6379.
:type port: int
:param db: Redis database, default is 0.
:type db: int
:param password: Redis password, default is None.
:type password: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param redis_url: Redis URL, default is None.
:type redis_url: Optional[str]
:param connection_pool: Redis connection pool, default is None.
:type connection_pool: Optional[ConnectionPool]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
host="localhost",
port=6379,
db=0,
password=None,
prefix="telebot",
redis_url=None,
connection_pool: "redis.ConnectionPool" = None,
separator: Optional[str] = ":",
) -> None:
if not redis_installed:
raise ImportError(
"Redis is not installed. Please install it via pip install redis"
)
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
if redis_url:
self.redis = redis.Redis.from_url(redis_url)
elif connection_pool:
self.redis = redis.Redis(connection_pool=connection_pool)
else:
self.redis = redis.Redis(host=host, port=port, db=db, password=password)
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def set_state_action(pipe):
pipe.multi()
data = pipe.hget(_key, "data")
result = pipe.execute()
data = result[0]
if data is None:
# If data is None, set it to an empty dictionary
data = {}
pipe.hset(_key, "data", json.dumps(data))
pipe.hset(_key, "state", state)
self.redis.transaction(set_state_action, _key)
return True
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
state_bytes = self.redis.hget(_key, "state")
return state_bytes.decode("utf-8") if state_bytes else None
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
return self.redis.delete(_key) > 0
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def set_data_action(pipe):
pipe.multi()
data = pipe.hget(_key, "data")
data = data.execute()[0]
if data is None:
raise RuntimeError(f"RedisStorage: key {_key} does not exist.")
else:
data = json.loads(data)
data[key] = value
pipe.hset(_key, "data", json.dumps(data))
self.redis.transaction(set_data_action, _key)
return True
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self.redis.hget(_key, "data")
return json.loads(data) if data else {}
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def reset_data_action(pipe):
pipe.multi()
if pipe.exists(_key):
pipe.hset(_key, "data", "{}")
else:
return False
self.redis.transaction(reset_data_action, _key)
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def save_action(pipe):
pipe.multi()
if pipe.exists(_key):
pipe.hset(_key, "data", json.dumps(data))
else:
return False
self.redis.transaction(save_action, _key)
return True
def migrate_format(self, bot_id: int, prefix: Optional[str] = "telebot_"):
"""
Migrate from old to new format of keys.
Run this function once to migrate all redis existing keys to new format.
Starting from version 4.23.0, the format of keys has been changed:
<key>:value
- Old format: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
- New format:
{prefix}{separator}{bot_id}{separator}{business_connection_id}{separator}{message_thread_id}{separator}{chat_id}{separator}{user_id}: {'state': ..., 'data': {}}
This function will help you to migrate from the old format to the new one in order to avoid data loss.
:param bot_id: Bot ID; To get it, call a getMe request and grab the id from the response.
:type bot_id: int
:param prefix: Prefix for keys, default is "telebot_"(old default value)
:type prefix: Optional[str]
"""
keys = self.redis.keys(f"{prefix}*")
for key in keys:
old_key = key.decode("utf-8")
# old: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
value = self.redis.get(old_key)
value = json.loads(value)
chat_id = old_key[len(prefix) :]
user_id = list(value.keys())[0]
state = value[user_id]["state"]
state_data = value[user_id]["data"]
# set new format
new_key = self._get_key(
int(chat_id), int(user_id), self.prefix, self.separator, bot_id=bot_id
)
self.redis.hset(new_key, "state", state)
self.redis.hset(new_key, "data", json.dumps(state_data))
# delete old key
self.redis.delete(old_key)
def __str__(self) -> str:
return f"StateRedisStorage({self.redis})"
File diff suppressed because it is too large Load Diff
+720
View File
@@ -0,0 +1,720 @@
# -*- coding: utf-8 -*-
import re
import threading
import traceback
from typing import Any, Callable, List, Dict, Optional, Union
import hmac
from hashlib import sha256
from urllib.parse import parse_qsl
# noinspection PyPep8Naming
import queue as Queue
import logging
from telebot import types
from telebot.service_utils import is_pil_image, is_dict, is_string, is_bytes, chunks, generate_random_token, pil_image_to_file
try:
import ujson as json
except ImportError:
import json
MAX_MESSAGE_LENGTH = 4096
logger = logging.getLogger('TeleBot')
thread_local = threading.local()
#: Contains all media content types.
content_type_media = [
'text', 'animation', 'audio', 'document', 'photo', 'sticker', 'story', 'video', 'video_note', 'voice', 'contact',
'dice', 'game', 'poll', 'venue', 'location', 'invoice', 'successful_payment', 'connected_website',
'passport_data', 'web_app_data',
]
#: Contains all service content types such as `User joined the group`.
content_type_service = [
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo',
'group_chat_created', 'supergroup_chat_created', 'channel_chat_created', 'message_auto_delete_timer_changed',
'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', 'users_shared', 'chat_shared',
'write_access_allowed', 'proximity_alert_triggered', 'forum_topic_created', 'forum_topic_edited',
'forum_topic_closed', 'forum_topic_reopened', 'general_forum_topic_hidden', 'general_forum_topic_unhidden',
'giveaway_created', 'giveaway', 'giveaway_winners', 'giveaway_completed', 'video_chat_scheduled',
'video_chat_started', 'video_chat_ended', 'video_chat_participants_invited',
]
#: All update types, should be used for allowed_updates parameter in polling.
update_types = [
"message", "edited_message", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result",
"callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer", "my_chat_member", "chat_member",
"chat_join_request", "message_reaction", "message_reaction_count", "chat_boost", "removed_chat_boost",
"business_connection", "business_message", "edited_business_message", "deleted_business_messages"
]
class WorkerThread(threading.Thread):
"""
:meta private:
"""
count = 0
def __init__(self, exception_callback=None, queue=None, name=None):
if not name:
name = "WorkerThread{0}".format(self.__class__.count + 1)
self.__class__.count += 1
if not queue:
queue = Queue.Queue()
threading.Thread.__init__(self, name=name)
self.queue = queue
self.daemon = True
self.received_task_event = threading.Event()
self.done_event = threading.Event()
self.exception_event = threading.Event()
self.continue_event = threading.Event()
self.exception_callback = exception_callback
self.exception_info = None
self._running = True
self.start()
def run(self):
while self._running:
try:
task, args, kwargs = self.queue.get(block=True, timeout=.5)
self.continue_event.clear()
self.received_task_event.clear()
self.done_event.clear()
self.exception_event.clear()
logger.debug("Received task")
self.received_task_event.set()
task(*args, **kwargs)
logger.debug("Task complete")
self.done_event.set()
except Queue.Empty:
pass
except Exception as e:
logger.debug(type(e).__name__ + " occurred, args=" + str(e.args) + "\n" + traceback.format_exc())
self.exception_info = e
self.exception_event.set()
if self.exception_callback:
self.exception_callback(self, self.exception_info)
self.continue_event.wait()
def put(self, task, *args, **kwargs):
self.queue.put((task, args, kwargs))
def raise_exceptions(self):
if self.exception_event.is_set():
raise self.exception_info
def clear_exceptions(self):
self.exception_event.clear()
self.continue_event.set()
def stop(self):
self._running = False
class ThreadPool:
"""
:meta private:
"""
def __init__(self, telebot, num_threads=2):
self.telebot = telebot
self.tasks = Queue.Queue()
self.workers = [WorkerThread(self.on_exception, self.tasks) for _ in range(num_threads)]
self.num_threads = num_threads
self.exception_event = threading.Event()
self.exception_info = None
def put(self, func, *args, **kwargs):
self.tasks.put((func, args, kwargs))
def on_exception(self, worker_thread, exc_info):
if self.telebot.exception_handler is not None:
handled = self.telebot.exception_handler.handle(exc_info)
else:
handled = False
if not handled:
self.exception_info = exc_info
self.exception_event.set()
worker_thread.continue_event.set()
def raise_exceptions(self):
if self.exception_event.is_set():
raise self.exception_info
def clear_exceptions(self):
self.exception_event.clear()
def close(self):
for worker in self.workers:
worker.stop()
for worker in self.workers:
if worker != threading.current_thread():
worker.join()
class AsyncTask:
"""
:meta private:
"""
def __init__(self, target, *args, **kwargs):
self.target = target
self.args = args
self.kwargs = kwargs
self.done = False
self.thread = threading.Thread(target=self._run)
self.thread.start()
def _run(self):
try:
self.result = self.target(*self.args, **self.kwargs)
except Exception as e:
self.result = e
self.done = True
def wait(self):
if not self.done:
self.thread.join()
if isinstance(self.result, BaseException):
raise self.result
else:
return self.result
class CustomRequestResponse:
"""
:meta private:
"""
def __init__(self, json_text, status_code=200, reason=""):
self.status_code = status_code
self.text = json_text
self.reason = reason
def json(self):
return json.loads(self.text)
def async_dec():
"""
:meta private:
"""
def decorator(fn):
def wrapper(*args, **kwargs):
return AsyncTask(fn, *args, **kwargs)
return wrapper
return decorator
def is_command(text: str) -> bool:
r"""
Checks if `text` is a command. Telegram chat commands start with the '/' character.
:param text: Text to check.
:type text: :obj:`str`
:return: True if `text` is a command, else False.
:rtype: :obj:`bool`
"""
if text is None: return False
return text.startswith('/')
def extract_command(text: str) -> Union[str, None]:
"""
Extracts the command from `text` (minus the '/') if `text` is a command (see is_command).
If `text` is not a command, this function returns None.
.. code-block:: python3
:caption: Examples:
extract_command('/help'): 'help'
extract_command('/help@BotName'): 'help'
extract_command('/search black eyed peas'): 'search'
extract_command('Good day to you'): None
:param text: String to extract the command from
:type text: :obj:`str`
:return: the command if `text` is a command (according to is_command), else None.
:rtype: :obj:`str` or :obj:`None`
"""
if text is None: return None
return text.split()[0].split('@')[0][1:] if is_command(text) else None
def extract_arguments(text: str) -> str or None:
"""
Returns the argument after the command.
.. code-block:: python3
:caption: Examples:
extract_arguments("/get name"): 'name'
extract_arguments("/get"): ''
extract_arguments("/get@botName name"): 'name'
:param text: String to extract the arguments from a command
:type text: :obj:`str`
:return: the arguments if `text` is a command (according to is_command), else None.
:rtype: :obj:`str` or :obj:`None`
"""
regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE)
result = regexp.match(text)
return result.group(2) if is_command(text) else None
def extract_entity(text: str, e: types.MessageEntity) -> str:
"""
Returns the content of the entity.
:param text: The text of the message the entity belongs to
:type text: :obj:`str`
:param e: The entity to extract
:type e: :obj:`MessageEntity`
:return: The content of the entity
:rtype: :obj:`str`
"""
offset = 0
start = 0
encoded_text = text.encode()
end = len(encoded_text)
i = 0
for byte in encoded_text:
if (byte & 0xc0) != 0x80:
if offset == e.offset:
start = i
elif offset - e.offset == e.length:
end = i
break
if byte >= 0xf0:
offset += 2
else:
offset += 1
i += 1
return encoded_text[start:end].decode()
def split_string(text: str, chars_per_string: int) -> List[str]:
"""
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
This is very useful for splitting one giant message into multiples.
:param text: The text to split
:type text: :obj:`str`
:param chars_per_string: The number of characters per line the text is split into.
:type chars_per_string: :obj:`int`
:return: The splitted text as a list of strings.
:rtype: :obj:`list` of :obj:`str`
"""
return [text[i:i + chars_per_string] for i in range(0, len(text), chars_per_string)]
def smart_split(text: str, chars_per_string: int = MAX_MESSAGE_LENGTH) -> List[str]:
r"""
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
This is very useful for splitting one giant message into multiples.
If `chars_per_string` > 4096: `chars_per_string` = 4096.
Splits by '\n', '. ' or ' ' in exactly this priority.
:param text: The text to split
:type text: :obj:`str`
:param chars_per_string: The number of maximum characters per part the text is split to.
:type chars_per_string: :obj:`int`
:return: The splitted text as a list of strings.
:rtype: :obj:`list` of :obj:`str`
"""
def _text_before_last(substr: str) -> str:
return substr.join(part.split(substr)[:-1]) + substr
if chars_per_string > MAX_MESSAGE_LENGTH: chars_per_string = MAX_MESSAGE_LENGTH
parts = []
while True:
if len(text) < chars_per_string:
parts.append(text)
return parts
part = text[:chars_per_string]
if "\n" in part:
part = _text_before_last("\n")
elif ". " in part:
part = _text_before_last(". ")
elif " " in part:
part = _text_before_last(" ")
parts.append(part)
text = text[len(part):]
def escape(text: str) -> Optional[str]:
"""
Replaces the following chars in `text` ('&' with '&amp;', '<' with '&lt;' and '>' with '&gt;').
:param text: the text to escape
:return: the escaped text
"""
chars = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
if text is None:
return None
for old, new in chars.items():
text = text.replace(old, new)
return text
def user_link(user: types.User, include_id: bool = False) -> str:
"""
Returns an HTML user link. This is useful for reports.
Attention: Don't forget to set parse_mode to 'HTML'!
.. code-block:: python3
:caption: Example:
bot.send_message(your_user_id, user_link(message.from_user) + ' started the bot!', parse_mode='HTML')
.. note::
You can use formatting.* for all other formatting options(bold, italic, links, and etc.)
This method is kept for backward compatibility, and it is recommended to use formatting.* for
more options.
:param user: the user (not the user_id)
:type user: :obj:`telebot.types.User`
:param include_id: include the user_id
:type include_id: :obj:`bool`
:return: HTML user link
:rtype: :obj:`str`
"""
name = escape(user.first_name)
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int = 2) -> types.InlineKeyboardMarkup:
"""
Returns a reply markup from a dict in this format: {'text': kwargs}
This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)'
Example:
.. code-block:: python3
:caption: Using quick_markup:
from telebot.util import quick_markup
markup = quick_markup({
'Twitter': {'url': 'https://twitter.com'},
'Facebook': {'url': 'https://facebook.com'},
'Back': {'callback_data': 'whatever'}
}, row_width=2)
# returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
# and a back button below
# kwargs can be:
{
'url': None,
'callback_data': None,
'switch_inline_query': None,
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None,
'web_app': None
}
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
:type values: :obj:`dict`
:param row_width: number of :class:`telebot.types.InlineKeyboardButton` objects on each row
:type row_width: :obj:`int`
:return: InlineKeyboardMarkup
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
markup = types.InlineKeyboardMarkup(row_width=row_width)
buttons = [
types.InlineKeyboardButton(text=text, **kwargs)
for text, kwargs in values.items()
]
markup.add(*buttons)
return markup
# CREDITS TO http://stackoverflow.com/questions/12317940#answer-12320352
def or_set(self):
"""
:meta private:
"""
self._set()
self.changed()
def or_clear(self):
"""
:meta private:
"""
self._clear()
self.changed()
def orify(e, changed_callback):
"""
:meta private:
"""
if not hasattr(e, "_set"):
e._set = e.set
if not hasattr(e, "_clear"):
e._clear = e.clear
e.changed = changed_callback
e.set = lambda: or_set(e)
e.clear = lambda: or_clear(e)
def OrEvent(*events):
"""
:meta private:
"""
or_event = threading.Event()
def changed():
bools = [ev.is_set() for ev in events]
if any(bools):
or_event.set()
else:
or_event.clear()
def busy_wait():
while not or_event.is_set():
# noinspection PyProtectedMember
or_event._wait(3)
for e in events:
orify(e, changed)
or_event._wait = or_event.wait
or_event.wait = busy_wait
changed()
return or_event
def per_thread(key, construct_value, reset=False):
"""
:meta private:
"""
if reset or not hasattr(thread_local, key):
value = construct_value()
setattr(thread_local, key, value)
return getattr(thread_local, key)
def deprecated(warn: bool = True, alternative: Optional[Callable] = None, deprecation_text=None):
"""
Use this decorator to mark functions as deprecated.
When the function is used, an info (or warning if `warn` is True) is logged.
:meta private:
:param warn: If True a warning is logged else an info
:type warn: :obj:`bool`
:param alternative: The new function to use instead
:type alternative: :obj:`Callable`
:param deprecation_text: Custom deprecation text
:type deprecation_text: :obj:`str`
:return: The decorated function
"""
def decorator(function):
def wrapper(*args, **kwargs):
info = f"`{function.__name__}` is deprecated."
if alternative:
info += f" Use `{alternative.__name__}` instead"
if deprecation_text:
info += " " + deprecation_text
if not warn:
logger.info(info)
else:
logger.warning(info)
return function(*args, **kwargs)
return wrapper
return decorator
# Cloud helpers
def webhook_google_functions(bot, request):
"""
A webhook endpoint for Google Cloud Functions FaaS.
:param bot: The bot instance
:type bot: :obj:`telebot.TeleBot` or :obj:`telebot.async_telebot.AsyncTeleBot`
:param request: The request object
:type request: :obj:`flask.Request`
:return: The response object
"""
if request.is_json:
try:
request_json = request.get_json()
update = types.Update.de_json(request_json)
bot.process_new_updates([update])
return ''
except Exception as e:
print(e)
return 'Bot FAIL', 400
else:
return 'Bot ON'
def antiflood(function: Callable, *args, number_retries=5, **kwargs):
"""
Use this function inside loops in order to avoid getting TooManyRequests error.
Example:
.. code-block:: python3
from telebot.util import antiflood
for chat_id in chat_id_list:
msg = antiflood(bot.send_message, chat_id, text)
:param function: The function to call
:type function: :obj:`Callable`
:param number_retries: Number of retries to send
:type function: :obj:int
:param args: The arguments to pass to the function
:type args: :obj:`tuple`
:param kwargs: The keyword arguments to pass to the function
:type kwargs: :obj:`dict`
:return: None
"""
from telebot.apihelper import ApiTelegramException
from time import sleep
for _ in range(number_retries - 1):
try:
return function(*args, **kwargs)
except ApiTelegramException as ex:
if ex.error_code == 429:
sleep(ex.result_json['parameters']['retry_after'])
else:
raise
else:
return function(*args, **kwargs)
def parse_web_app_data(token: str, raw_init_data: str):
"""
Parses web app data.
:param token: The bot token
:type token: :obj:`str`
:param raw_init_data: The raw init data
:type raw_init_data: :obj:`str`
:return: The parsed init data
"""
is_valid = validate_web_app_data(token, raw_init_data)
if not is_valid:
return False
result = {}
for key, value in parse_qsl(raw_init_data):
try:
value = json.loads(value)
except json.JSONDecodeError:
result[key] = value
else:
result[key] = value
return result
def validate_web_app_data(token: str, raw_init_data: str):
"""
Validates web app data.
:param token: The bot token
:type token: :obj:`str`
:param raw_init_data: The raw init data
:type raw_init_data: :obj:`str`
:return: The parsed init data
"""
try:
parsed_data = dict(parse_qsl(raw_init_data))
except ValueError:
return False
if "hash" not in parsed_data:
return False
init_data_hash = parsed_data.pop('hash')
data_check_string = "\n".join(f"{key}={value}" for key, value in sorted(parsed_data.items()))
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=sha256)
return hmac.new(secret_key.digest(), data_check_string.encode(), sha256).hexdigest() == init_data_hash
def validate_token(token) -> bool:
if any(char.isspace() for char in token):
raise ValueError('Token must not contain spaces')
if ':' not in token:
raise ValueError('Token must contain a colon')
if len(token.split(':')) != 2:
raise ValueError('Token must contain exactly 2 parts separated by a colon')
return True
def extract_bot_id(token) -> Union[int, None]:
try:
validate_token(token)
except ValueError:
return None
return int(token.split(':')[0])
__all__ = (
"content_type_media", "content_type_service", "update_types",
"WorkerThread", "AsyncTask", "CustomRequestResponse",
"async_dec", "deprecated",
"is_bytes", "is_string", "is_dict", "is_pil_image",
"chunks", "generate_random_token", "pil_image_to_file",
"is_command", "extract_command", "extract_arguments",
"split_string", "smart_split", "escape", "user_link", "quick_markup",
"antiflood", "parse_web_app_data", "validate_web_app_data",
"or_set", "or_clear", "orify", "OrEvent", "per_thread",
"webhook_google_functions", "validate_token", "extract_bot_id"
)
+3
View File
@@ -0,0 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '4.22.1'