-
-
Save KurimuzonAkuma/683eec4d62e111578a42608d4485fc27 to your computer and use it in GitHub Desktop.
import pickle | |
from asyncio import Lock | |
from typing import Any, Dict, Optional | |
import aiomysql | |
import aiosqlite | |
from aiogram import Bot | |
from aiogram.fsm.state import State | |
from aiogram.fsm.storage.base import BaseStorage, StateType, StorageKey | |
class MySQLStorage(BaseStorage): | |
""" | |
MySQL storage backend for FSM.\n | |
If database and table does not exist, it will be created automatically. | |
""" | |
def __init__( | |
self, | |
host: str, | |
user: str, | |
password: str, | |
database: str, | |
) -> None: | |
self.host = host | |
self.user = user | |
self.password = password | |
self.database = database | |
self.connection = None | |
self.cursor = None | |
self._lock = Lock() | |
async def __execute(self, query: str, values: tuple = None, commit: bool = False): | |
async with self._lock: | |
await self.connect() | |
await self.cursor.execute(query, values) | |
if commit: | |
await self.connection.commit() | |
async def __create_tables(self) -> None: | |
await self.cursor.execute( | |
f"CREATE DATABASE IF NOT EXISTS `{self.database}`;\n" | |
f"USE `{self.database}`;\n" | |
"CREATE TABLE IF NOT EXISTS `aiogram_fsm_states` (" | |
"`chat_id` BIGINT NOT NULL," | |
"`user_id` BIGINT NOT NULL," | |
"`state` TEXT," | |
"PRIMARY KEY (`chat_id`)" | |
");\n" | |
"CREATE TABLE IF NOT EXISTS `aiogram_fsm_data` (" | |
"`chat_id` BIGINT NOT NULL," | |
"`user_id` BIGINT NOT NULL," | |
"`data` BLOB," | |
"PRIMARY KEY (`chat_id`)" | |
");" | |
) | |
async def connect(self) -> aiomysql.Connection: | |
if self.connection is None: | |
self.connection = await aiomysql.connect( | |
host=self.host, | |
user=self.user, | |
password=self.password, | |
db=self.database, | |
) | |
self.cursor = await self.connection.cursor(aiomysql.DictCursor) | |
await self.__create_tables() | |
return self.connection | |
async def close(self) -> None: | |
if isinstance(self.connection, aiomysql.Connection): | |
await self.cursor.close() | |
self.connection.close() | |
self.connection = None | |
self.cursor = None | |
async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None: | |
_state = state.state if isinstance(state, State) else state | |
if _state is None: | |
await self.__execute( | |
"DELETE FROM `aiogram_fsm_states` WHERE `chat_id` = %s AND `user_id` = %s;", | |
(key.chat_id, key.user_id), | |
commit=True, | |
) | |
return | |
await self.__execute( | |
"INSERT INTO `aiogram_fsm_states` (`chat_id`, `user_id`, `state`) VALUES (%s, %s, %s) " | |
"ON DUPLICATE KEY UPDATE `state` = %s;", | |
(key.chat_id, key.user_id, _state, _state), | |
commit=True, | |
) | |
async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]: | |
await self.__execute( | |
"SELECT `state` FROM `aiogram_fsm_states` WHERE `chat_id` = %s AND `user_id` = %s;", | |
(key.chat_id, key.user_id), | |
) | |
result = await self.cursor.fetchone() | |
return result["state"] if result else None | |
async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> None: | |
if not data: | |
await self.__execute( | |
"DELETE FROM `aiogram_fsm_data` WHERE `chat_id` = %s AND `user_id` = %s;", | |
(key.chat_id, key.user_id), | |
commit=True, | |
) | |
return | |
serialized_data = pickle.dumps(data) | |
await self.__execute( | |
"INSERT INTO `aiogram_fsm_data` (`chat_id`, `user_id`, `data`) VALUES (%s, %s, %s) " | |
"ON DUPLICATE KEY UPDATE `data` = %s;", | |
(key.chat_id, key.user_id, serialized_data, serialized_data), | |
commit=True, | |
) | |
async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]: | |
await self.__execute( | |
"SELECT `data` FROM `aiogram_fsm_data` WHERE `chat_id` = %s AND `user_id` = %s;", | |
(key.chat_id, key.user_id), | |
) | |
result = await self.cursor.fetchone() | |
return pickle.loads(result["data"]) if result else {} | |
async def update_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]: | |
current_data = await self.get_data(bot=bot, key=key) | |
current_data.update(data) | |
await self.set_data(bot=bot, key=key, data=current_data) | |
return current_data | |
class SQLiteStorage(BaseStorage): | |
""" | |
SQLite storage backend for FSM.\n | |
Tables will be created automatically. | |
""" | |
def __init__(self, path: str) -> None: | |
self.path = path | |
self.connection = None | |
self.cursor = None | |
self._lock = Lock() | |
async def __execute(self, query: str, values: tuple = None, commit: bool = False): | |
async with self._lock: | |
await self.connect() | |
self.cursor.execute(query, values) | |
if commit: | |
await self.connection.commit() | |
async def __create_tables(self) -> None: | |
await self.cursor.execute( | |
"CREATE TABLE IF NOT EXISTS `aiogram_fsm_states` (" | |
"`chat_id` BIGINT NOT NULL," | |
"`user_id` BIGINT NOT NULL," | |
"`state` TEXT," | |
"PRIMARY KEY (`chat_id`, `user_id`)" | |
");\n" | |
"CREATE TABLE IF NOT EXISTS `aiogram_fsm_data` (" | |
"`chat_id` BIGINT NOT NULL," | |
"`user_id` BIGINT NOT NULL," | |
"`data` BLOB," | |
"PRIMARY KEY (`chat_id`, `user_id`)" | |
");" | |
) | |
async def connect(self) -> aiosqlite.Connection: | |
if self.connection is None: | |
self.connection = await aiosqlite.connect(self.path) | |
self.connection.row_factory = aiosqlite.Row | |
self.cursor = await self.connection.cursor() | |
await self.__create_tables() | |
return self.connection | |
async def close(self) -> None: | |
if isinstance(self.connection, aiosqlite.Connection): | |
await self.connection.close() | |
await self.cursor.close() | |
self.connection = None | |
self.cursor = None | |
async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None: | |
_state = state.state if isinstance(state, State) else state | |
if _state is None: | |
await self.__execute( | |
"DELETE FROM `aiogram_fsm_states` WHERE `chat_id` = ? AND `user_id` = ?;", | |
(key.chat_id, key.user_id), | |
commit=True, | |
) | |
return | |
await self.__execute( | |
"INSERT INTO `aiogram_fsm_states` (`chat_id`, `user_id`, `state`) VALUES (?, ?, ?) " | |
"ON CONFLICT(`chat_id`, `user_id`) DO UPDATE SET `state` = ?;", | |
(key.chat_id, key.user_id, _state, _state), | |
commit=True, | |
) | |
async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]: | |
await self.__execute( | |
"SELECT `state` FROM `aiogram_fsm_states` WHERE `chat_id` = ? AND `user_id` = ?;", | |
(key.chat_id, key.user_id), | |
) | |
result = await self.cursor.fetchone() | |
return result["state"] if result else None | |
async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> None: | |
if not data: | |
await self.__execute( | |
"DELETE FROM `aiogram_fsm_data` WHERE `chat_id` = ? AND `user_id` = ?;", | |
(key.chat_id, key.user_id), | |
commit=True, | |
) | |
return | |
serialized_data = pickle.dumps(data) | |
await self.__execute( | |
"INSERT INTO `aiogram_fsm_data` (`chat_id`, `user_id`, `data`) VALUES (?, ?, ?) " | |
"ON CONFLICT(`chat_id`, `user_id`) DO UPDATE SET `data` = ?;", | |
(key.chat_id, key.user_id, serialized_data, serialized_data), | |
commit=True, | |
) | |
async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]: | |
await self.__execute( | |
"SELECT `data` FROM `aiogram_fsm_data` WHERE `chat_id` = ? AND `user_id` = ?;", | |
(key.chat_id, key.user_id), | |
) | |
result = await self.cursor.fetchone() | |
return pickle.loads(result["data"]) if result else {} | |
async def update_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]: | |
current_data = await self.get_data(bot=bot, key=key) | |
current_data.update(data) | |
await self.set_data(bot=bot, key=key, data=current_data) | |
return current_data |
Добрый день! Не совсем ясно: как в теле Midlewares оказывается bot: Bot? Ведь мы не передаём в неё это. Вызов идёт через storage = MysqlStorage(host, user, password, database). Если удалить из кода bot: Bot, то всё работает отлично. Ещё было бы здорово проверять наличие этих таблиц, которые создаются в начале. Благодарю.
Этому гисту уже более года. Я изменил этот код под себя, но и с этим у меня проблемы: не записываются даты в бд...
Добрый день! Не совсем ясно: как в теле Midlewares оказывается bot: Bot? Ведь мы не передаём в неё это. Вызов идёт через storage = MysqlStorage(host, user, password, database). Если удалить из кода bot: Bot, то всё работает отлично. Ещё было бы здорово проверять наличие этих таблиц, которые создаются в начале. Благодарю.
Этому гисту уже более года. Я изменил этот код под себя, но и с этим у меня проблемы: не записываются даты в бд...
На самом деле, всё работает и записывает. Нужно смотреть в чём у Вас ошибка (по логам). Но как мидлварь подключается нормально, запись state идёт нормально. Вот только что проверил.
Возможно, проект нуждается в некоторой доработке, но он в рабочем состоянии.
На самом деле, всё работает и записывает.
Можете показать мне ваш код?
Но как мидлварь подключается нормально,
Нужно ли регистрировать сторедж в мидлвари? Как?
Если удалить из кода bot: Bot, то всё работает отлично.
Это вроде из второй версии такое пошло, для тройки надо убрать
Нужно ли регистрировать сторедж в мидлвари? Как?
Вы имеете в виду router.message.middleware() ? Или какую ещё регистрацию?
Регистрировать не нужно. Создаём файл с именем storages.py в папке middleware (если у Вас другая папка или имя файла - исправьте по тексту)
`
---импорт класса мидлвари, обратите внимание на имя файла и папку, если нужно исправьте на свои
from middlewares.storages import MySQLStorage
---другие импорты, в том числе, и для этой мидлвари
...
---подключаем хранилище
try:
storage = MySQLStorage(host="", user="", password="", database="") # в значениях указываем свои данные, соответственно
except:
storage = MemoryStorage() # указываем хранилище в памяти, если не удалось инициализировать MySQL, можно заменить на Redis или что-то ещё по вкусу
bot = Bot(token=config('TOKEN'), default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=storage)
`
Вот и всё.
- В тексте кода уберите по тексту bot: Bot
(больше, по-моему ничего не менял)
Если удалить из кода bot: Bot, то всё работает отлично.
Это вроде из второй версии такое пошло, для тройки надо убрать
Благодарю за код! Удачно подошёл )
Единственное, не по этому коду, можно спросить? У меня мидлварь наподобие AntiThrottle переделанная из Redis в работу с MySQL. Ловит флуд по @rate_limit
Мне непонятно как зарегистрировать мидлварь в аиограм 3 для дальнейшей работы в системе? Либо в начале каждого файла хендлера прописать router.message.middleware(названиекласса) и router.callbask.middleware(названиекласса) или как-то (как?) зарегить её сразу для всех файлов хендлеров?
Раньше было что-то типа Dispatcher.middleware.setup, но сейчас его вроде нет.
Спасибо!
@messireL, Я кстати использую SQLite версию... Стейты у меня записываются на ура, а вот данные (state.set_data
, state.update_data
) – почему-то нет
UPD: они записываются, но как только хочу получить их через state.get_data
, то данные сбрасываются
@messireL, Я кстати использую SQLite версию... Стейты у меня записываются на ура, а вот данные (
state.set_data
,state.update_data
) – почему-то нетUPD: они записываются, но как только хочу получить их через
state.get_data
, то данные сбрасываются
Смотрите по логам что происходит, есть ли реально данные в таблицах (в процессе заполнения), что по программному коду - нет ли state.clear() в ненужном месте и точно ли Вы берёт именно эти данные.
Добрый день! Не совсем ясно: как в теле Midlewares оказывается bot: Bot? Ведь мы не передаём в неё это.
Вызов идёт через storage = MysqlStorage(host, user, password, database).
Если удалить из кода bot: Bot, то всё работает отлично.
Ещё было бы здорово проверять наличие этих таблиц, которые создаются в начале.
Благодарю.