mirror of
https://github.com/ada-dmitry/kurator_bot.git
synced 2026-09-24 08:10:15 +00:00
Init
This commit is contained in:
+8920
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2223
File diff suppressed because it is too large
Load Diff
+8058
File diff suppressed because it is too large
Load Diff
+450
@@ -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
|
||||
+2202
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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
@@ -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
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
A folder with asynchronous and synchronous extensions.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
A folder with all the async extensions.
|
||||
"""
|
||||
|
||||
from .webhooks import AsyncWebhookListener
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AsyncWebhookListener"
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+123
@@ -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
@@ -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)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
A folder with all the sync extensions.
|
||||
"""
|
||||
|
||||
from .webhooks import SyncWebhookListener
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SyncWebhookListener"
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+116
@@ -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
@@ -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}">⁠</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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
_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("&", "&").replace("<", "<").replace(">", ">")
|
||||
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("&", "&").replace("<", "<").replace(">", ">")
|
||||
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
@@ -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
@@ -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
@@ -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 :(
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
from .context import StateContext
|
||||
from .middleware import StateMiddleware
|
||||
|
||||
__all__ = [
|
||||
"StateContext",
|
||||
"StateMiddleware",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
from .context import StateContext
|
||||
from .middleware import StateMiddleware
|
||||
|
||||
__all__ = [
|
||||
'StateContext',
|
||||
'StateMiddleware',
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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,
|
||||
)
|
||||
@@ -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}>"
|
||||
@@ -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})"
|
||||
@@ -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})"
|
||||
+10810
File diff suppressed because it is too large
Load Diff
+720
@@ -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 '&', '<' with '<' and '>' with '>').
|
||||
|
||||
:param text: the text to escape
|
||||
:return: the escaped text
|
||||
"""
|
||||
chars = {"&": "&", "<": "<", ">": ">"}
|
||||
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
@@ -0,0 +1,3 @@
|
||||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '4.22.1'
|
||||
Reference in New Issue
Block a user