This commit is contained in:
ada-dmitry
2024-08-24 15:45:54 +03:00
commit ab9d8386fb
1777 changed files with 358417 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
from telebot.storage.memory_storage import StateMemoryStorage
from telebot.storage.redis_storage import StateRedisStorage
from telebot.storage.pickle_storage import StatePickleStorage
from telebot.storage.base_storage import StateDataContext, StateStorageBase
__all__ = [
"StateStorageBase",
"StateDataContext",
"StateMemoryStorage",
"StateRedisStorage",
"StatePickleStorage",
]
+118
View File
@@ -0,0 +1,118 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
def get_state(self, chat_id, user_id):
raise NotImplementedError
def get_interactive_data(self, chat_id, user_id):
raise NotImplementedError
def save(self, chat_id, user_id, data):
raise NotImplementedError
def _get_key(
self,
chat_id: int,
user_id: int,
prefix: str,
separator: str,
business_connection_id: str = None,
message_thread_id: int = None,
bot_id: int = None,
) -> str:
"""
Convert parameters to a key.
"""
params = [prefix]
if bot_id:
params.append(str(bot_id))
if business_connection_id:
params.append(business_connection_id)
if message_thread_id:
params.append(str(message_thread_id))
params.append(str(chat_id))
params.append(str(user_id))
return separator.join(params)
class StateDataContext:
"""
Class for data.
"""
def __init__(
self,
obj,
chat_id,
user_id,
business_connection_id=None,
message_thread_id=None,
bot_id=None,
):
self.obj = obj
res = obj.get_data(
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
self.data = copy.deepcopy(res)
self.chat_id = chat_id
self.user_id = user_id
self.bot_id = bot_id
self.business_connection_id = business_connection_id
self.message_thread_id = message_thread_id
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
return self.obj.save(
self.chat_id,
self.user_id,
self.data,
self.business_connection_id,
self.message_thread_id,
self.bot_id,
)
+225
View File
@@ -0,0 +1,225 @@
from telebot.storage.base_storage import StateStorageBase, StateDataContext
from typing import Optional, Union
class StateMemoryStorage(StateStorageBase):
"""
Memory storage for states.
Stores states in memory as a dictionary.
.. code-block:: python3
storage = StateMemoryStorage()
bot = TeleBot(token, storage=storage)
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
"""
def __init__(
self, separator: Optional[str] = ":", prefix: Optional[str] = "telebot"
) -> None:
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
self.data = (
{}
) # key: telebot:bot_id:business_connection_id:message_thread_id:chat_id:user_id
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
self.data[_key] = {"state": state, "data": {}}
else:
self.data[_key]["state"] = state
return True
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return None
return self.data[_key]["state"]
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
del self.data[_key]
return True
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
raise RuntimeError(f"StateMemoryStorage: key {_key} does not exist.")
self.data[_key]["data"][key] = value
return True
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
return self.data.get(_key, {}).get("data", {})
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = {}
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
if self.data.get(_key) is None:
return False
self.data[_key]["data"] = data
return True
def __str__(self) -> str:
return f"<StateMemoryStorage: {self.data}>"
+263
View File
@@ -0,0 +1,263 @@
import os
import pickle
import threading
from typing import Optional, Union, Callable
from telebot.storage.base_storage import StateStorageBase, StateDataContext
def with_lock(func: Callable) -> Callable:
def wrapper(self, *args, **kwargs):
with self.lock:
return func(self, *args, **kwargs)
return wrapper
class StatePickleStorage(StateStorageBase):
"""
State storage based on pickle file.
.. warning::
This storage is not recommended for production use.
Data may be corrupted. If you face a case where states do not work as expected,
try to use another storage.
.. code-block:: python3
storage = StatePickleStorage()
bot = TeleBot(token, storage=storage)
:param file_path: Path to file where states will be stored.
:type file_path: str
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
file_path: str = "./.state-save/states.pkl",
prefix="telebot",
separator: Optional[str] = ":",
) -> None:
self.file_path = file_path
self.prefix = prefix
self.separator = separator
self.lock = threading.Lock()
self.create_dir()
def _read_from_file(self) -> dict:
with open(self.file_path, "rb") as f:
return pickle.load(f)
def _write_to_file(self, data: dict) -> None:
with open(self.file_path, "wb") as f:
pickle.dump(data, f)
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path, "wb") as file:
pickle.dump({}, file)
@with_lock
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key not in data:
data[_key] = {"state": state, "data": {}}
else:
data[_key]["state"] = state
self._write_to_file(data)
return True
@with_lock
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
return data.get(_key, {}).get("state")
@with_lock
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key in data:
del data[_key]
self._write_to_file(data)
return True
return False
@with_lock
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
state_data = data.get(_key, {})
state_data["data"][key] = value
if _key not in data:
raise RuntimeError(f"PickleStorage: key {_key} does not exist.")
self._write_to_file(data)
return True
@with_lock
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
return data.get(_key, {}).get("data", {})
@with_lock
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
if _key in data:
data[_key]["data"] = {}
self._write_to_file(data)
return True
return False
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
@with_lock
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self._read_from_file()
data[_key]["data"] = data
self._write_to_file(data)
return True
def __str__(self) -> str:
return f"StatePickleStorage({self.file_path}, {self.prefix})"
+324
View File
@@ -0,0 +1,324 @@
import json
from telebot.storage.base_storage import StateStorageBase, StateDataContext
from typing import Optional, Union
redis_installed = True
try:
import redis
except ImportError:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
State storage based on Redis.
.. code-block:: python3
storage = StateRedisStorage(...)
bot = TeleBot(token, storage=storage)
:param host: Redis host, default is "localhost".
:type host: str
:param port: Redis port, default is 6379.
:type port: int
:param db: Redis database, default is 0.
:type db: int
:param password: Redis password, default is None.
:type password: Optional[str]
:param prefix: Prefix for keys, default is "telebot".
:type prefix: Optional[str]
:param redis_url: Redis URL, default is None.
:type redis_url: Optional[str]
:param connection_pool: Redis connection pool, default is None.
:type connection_pool: Optional[ConnectionPool]
:param separator: Separator for keys, default is ":".
:type separator: Optional[str]
"""
def __init__(
self,
host="localhost",
port=6379,
db=0,
password=None,
prefix="telebot",
redis_url=None,
connection_pool: "redis.ConnectionPool" = None,
separator: Optional[str] = ":",
) -> None:
if not redis_installed:
raise ImportError(
"Redis is not installed. Please install it via pip install redis"
)
self.separator = separator
self.prefix = prefix
if not self.prefix:
raise ValueError("Prefix cannot be empty")
if redis_url:
self.redis = redis.Redis.from_url(redis_url)
elif connection_pool:
self.redis = redis.Redis(connection_pool=connection_pool)
else:
self.redis = redis.Redis(host=host, port=port, db=db, password=password)
def set_state(
self,
chat_id: int,
user_id: int,
state: str,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
if hasattr(state, "name"):
state = state.name
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def set_state_action(pipe):
pipe.multi()
data = pipe.hget(_key, "data")
result = pipe.execute()
data = result[0]
if data is None:
# If data is None, set it to an empty dictionary
data = {}
pipe.hset(_key, "data", json.dumps(data))
pipe.hset(_key, "state", state)
self.redis.transaction(set_state_action, _key)
return True
def get_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Union[str, None]:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
state_bytes = self.redis.hget(_key, "state")
return state_bytes.decode("utf-8") if state_bytes else None
def delete_state(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
return self.redis.delete(_key) > 0
def set_data(
self,
chat_id: int,
user_id: int,
key: str,
value: Union[str, int, float, dict],
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def set_data_action(pipe):
pipe.multi()
data = pipe.hget(_key, "data")
data = data.execute()[0]
if data is None:
raise RuntimeError(f"RedisStorage: key {_key} does not exist.")
else:
data = json.loads(data)
data[key] = value
pipe.hset(_key, "data", json.dumps(data))
self.redis.transaction(set_data_action, _key)
return True
def get_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> dict:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
data = self.redis.hget(_key, "data")
return json.loads(data) if data else {}
def reset_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def reset_data_action(pipe):
pipe.multi()
if pipe.exists(_key):
pipe.hset(_key, "data", "{}")
else:
return False
self.redis.transaction(reset_data_action, _key)
return True
def get_interactive_data(
self,
chat_id: int,
user_id: int,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> Optional[dict]:
return StateDataContext(
self,
chat_id=chat_id,
user_id=user_id,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
bot_id=bot_id,
)
def save(
self,
chat_id: int,
user_id: int,
data: dict,
business_connection_id: Optional[str] = None,
message_thread_id: Optional[int] = None,
bot_id: Optional[int] = None,
) -> bool:
_key = self._get_key(
chat_id,
user_id,
self.prefix,
self.separator,
business_connection_id,
message_thread_id,
bot_id,
)
def save_action(pipe):
pipe.multi()
if pipe.exists(_key):
pipe.hset(_key, "data", json.dumps(data))
else:
return False
self.redis.transaction(save_action, _key)
return True
def migrate_format(self, bot_id: int, prefix: Optional[str] = "telebot_"):
"""
Migrate from old to new format of keys.
Run this function once to migrate all redis existing keys to new format.
Starting from version 4.23.0, the format of keys has been changed:
<key>:value
- Old format: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
- New format:
{prefix}{separator}{bot_id}{separator}{business_connection_id}{separator}{message_thread_id}{separator}{chat_id}{separator}{user_id}: {'state': ..., 'data': {}}
This function will help you to migrate from the old format to the new one in order to avoid data loss.
:param bot_id: Bot ID; To get it, call a getMe request and grab the id from the response.
:type bot_id: int
:param prefix: Prefix for keys, default is "telebot_"(old default value)
:type prefix: Optional[str]
"""
keys = self.redis.keys(f"{prefix}*")
for key in keys:
old_key = key.decode("utf-8")
# old: {prefix}chat_id: {user_id: {'state': None, 'data': {}}, ...}
value = self.redis.get(old_key)
value = json.loads(value)
chat_id = old_key[len(prefix) :]
user_id = list(value.keys())[0]
state = value[user_id]["state"]
state_data = value[user_id]["data"]
# set new format
new_key = self._get_key(
int(chat_id), int(user_id), self.prefix, self.separator, bot_id=bot_id
)
self.redis.hset(new_key, "state", state)
self.redis.hset(new_key, "data", json.dumps(state_data))
# delete old key
self.redis.delete(old_key)
def __str__(self) -> str:
return f"StateRedisStorage({self.redis})"