Migrating to Postgres from sqlite3

This commit is contained in:
machka
2025-05-20 18:41:32 +00:00
parent ac2f83c061
commit a56d804b58
38 changed files with 5017 additions and 4389 deletions
+50 -39
View File
@@ -1,11 +1,12 @@
import logging
import json
from typing import Callable, Dict, Any, Awaitable, Union, Optional
from typing import Callable, Dict, Any, Awaitable, Optional
from datetime import datetime, timezone
from aiogram import BaseMiddleware
from aiogram.types import Update, Message, CallbackQuery, User
from aiogram.types import Update, User, Message, CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from db.database import log_user_action
from db.dal import message_log_dal, user_dal
from config.settings import Settings
@@ -19,72 +20,82 @@ class ActionLoggerMiddleware(BaseMiddleware):
Awaitable[Any]], event: Update,
data: Dict[str, Any]) -> Any:
result = await handler(event, data)
session: AsyncSession = data["session"]
event_user: Optional[User] = data.get("event_from_user")
bot: Optional[Bot] = data.get("bot")
user_id: Optional[int] = None
telegram_username: Optional[str] = None
telegram_first_name: Optional[str] = None
event_type: str = event.event_type
content: Optional[str] = None
is_admin_event_flag: bool = False
target_user_id_for_log: Optional[int] = None
if event_user:
user_id = event_user.id
telegram_username = event_user.username
telegram_first_name = event_user.first_name
if user_id == self.settings.ADMIN_IDS:
if user_id in self.settings.ADMIN_IDS:
is_admin_event_flag = True
raw_update_snippet = None
try:
raw_update_snippet = event.model_dump_json(exclude_none=True,
indent=None)[:1000]
except AttributeError:
raw_update_snippet = str(event)[:1000]
except Exception:
raw_update_snippet = str(event)[:1000]
current_event_type = event.event_type
if event.message:
msg = event.message
msg: Message = event.message
if msg.text:
content = msg.text
if msg.text.startswith('/'):
event_type = "command"
elif msg.caption:
content = f"[{msg.content_type}] {msg.caption}"
current_event_type = f"command:{msg.text.split()[0]}"
else:
content = f"[{msg.content_type}]"
content = f"[{msg.content_type or 'unknown_content_type'}]"
current_event_type = f"message:{msg.content_type or 'unknown'}"
elif event.callback_query:
cb = event.callback_query
event_type = "callback_query"
cb: CallbackQuery = event.callback_query
content = cb.data
action_part = cb.data.split(
":")[0] if cb.data and ":" in cb.data else cb.data
current_event_type = f"callback:{action_part}"
if user_id and event_type and content:
if user_id or current_event_type not in ["update"]:
log_user_id_for_db = user_id
if user_id:
user_exists = await user_dal.get_user_by_id(session, user_id)
if not user_exists:
logging.warning(
f"ActionLoggerMiddleware: User {user_id} not found in DB. Logging action with user_id=NULL."
)
log_user_id_for_db = None
log_payload = {
"user_id": log_user_id_for_db,
"telegram_username": telegram_username,
"telegram_first_name": telegram_first_name,
"event_type": current_event_type,
"content": content[:1000] if content else "N/A",
"raw_update_preview": raw_update_snippet,
"is_admin_event": is_admin_event_flag,
"target_user_id": target_user_id_for_log,
"timestamp": datetime.now(timezone.utc)
}
try:
await log_user_action(user_id=user_id,
telegram_username=telegram_username,
telegram_first_name=telegram_first_name,
event_type=event_type,
content=content[:1000],
raw_update_preview=raw_update_snippet,
is_admin_event=is_admin_event_flag)
await message_log_dal.create_message_log_no_commit(
session, log_payload)
except Exception as e_log:
logging.error(
f"ActionLoggerMiddleware: Failed to log event for user {user_id}: {e_log}",
exc_info=True)
elif user_id and event_type:
try:
await log_user_action(user_id=user_id,
telegram_username=telegram_username,
telegram_first_name=telegram_first_name,
event_type=event_type,
content="N/A",
raw_update_preview=raw_update_snippet,
is_admin_event=is_admin_event_flag)
except Exception as e_log:
logging.error(
f"ActionLoggerMiddleware: Failed to log event (no content) for user {user_id}: {e_log}",
f"ActionLoggerMiddleware: Failed to add log to session for user {user_id}, type {current_event_type}: {e_log}",
exc_info=True)
return await handler(event, data)
return result
+51 -153
View File
@@ -1,15 +1,16 @@
import logging
from typing import Callable, Dict, Any, Awaitable, Union, Optional
from typing import Callable, Dict, Any, Awaitable, Optional, Union
from aiogram import BaseMiddleware, Bot
from aiogram.types import Message, CallbackQuery, User, InlineKeyboardMarkup, Update
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import Message, CallbackQuery, User, Update, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram.exceptions import TelegramAPIError, TelegramForbiddenError, TelegramBadRequest, AiogramError
from config.settings import Settings
from db.database import get_user
from bot.middlewares.i18n import JsonI18n
from bot.keyboards.inline.user_keyboards import get_user_banned_keyboard
from db.dal import user_dal
from .i18n import JsonI18n
from ..keyboards.inline.user_keyboards import get_user_banned_keyboard
class BanCheckMiddleware(BaseMiddleware):
@@ -22,191 +23,88 @@ class BanCheckMiddleware(BaseMiddleware):
async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
Awaitable[Any]], event: Update,
data: Dict[str, Any]) -> Any:
session: AsyncSession = data["session"]
event_user: Optional[User] = data.get("event_from_user")
if not event_user: return await handler(event, data)
bot_instance: Bot = data["bot"]
if not event_user:
return await handler(event, data)
if event_user.id in self.settings.ADMIN_IDS:
return await handler(event, data)
try:
db_user_data = await get_user(user_id=event_user.id)
db_user_model = await user_dal.get_user_by_id(
session, event_user.id)
except Exception as e_db:
logging.error(
f"BanCheckMiddleware: DB error fetching user {event_user.id}: {e_db}",
exc_info=True)
return await handler(event, data)
if db_user_data and db_user_data['is_banned'] == 1:
if db_user_model and db_user_model.is_banned:
logging.info(
f"User {event_user.id} ({event_user.username or 'NoUsername'}) is banned. Blocking access and preparing notification."
f"User {event_user.id} ({event_user.username or 'NoUsername'}) is banned. Blocking access."
)
ban_message_text = "You are blocked. Please contact support."
keyboard: Optional[InlineKeyboardMarkup] = None
current_lang = self.settings.DEFAULT_LANGUAGE
i18n_to_use: Optional[JsonI18n] = None
actual_event_object: Optional[Union[Message, CallbackQuery]] = None
i18n_data_from_event = data.get("i18n_data", {})
current_lang = i18n_data_from_event.get(
"current_language", self.settings.DEFAULT_LANGUAGE)
i18n_to_use: Optional[JsonI18n] = i18n_data_from_event.get(
"i18n_instance", self.i18n_main_instance)
if event.message:
actual_event_object = event.message
ban_message_text = "You are banned. Please contact support."
keyboard: Optional[InlineKeyboardMarkup] = None
if i18n_to_use:
_ = lambda k, **kw: i18n_to_use.gettext(current_lang, k, **kw)
ban_message_text = _("user_is_banned")
keyboard = get_user_banned_keyboard(self.settings.SUPPORT_LINK,
current_lang, i18n_to_use)
elif self.settings.SUPPORT_LINK:
from aiogram.utils.keyboard import InlineKeyboardBuilder
builder = InlineKeyboardBuilder()
builder.button(text="Support", url=self.settings.SUPPORT_LINK)
keyboard = builder.as_markup()
actual_event_object: Optional[Union[Message, CallbackQuery]] = None
if event.message: actual_event_object = event.message
elif event.callback_query:
actual_event_object = event.callback_query
if not actual_event_object:
logging.warning(
f"BanCheck: Could not determine specific event type (Message/CallbackQuery) for banned user {event_user.id} from Update object. Update type: {event.type}"
)
try:
bot_instance: Bot = data["bot"]
await bot_instance.send_message(event_user.id,
ban_message_text)
except Exception as e_direct_send:
logging.error(
f"BanCheck: Failed to send direct ban message to {event_user.id}: {e_direct_send}"
)
return
try:
logging.debug(
"BanCheck: [A] Inside main try block for banned user notification."
)
i18n_data_from_event = data.get("i18n_data", {})
current_lang = i18n_data_from_event.get(
"current_language", self.settings.DEFAULT_LANGUAGE)
i18n_to_use = i18n_data_from_event.get("i18n_instance")
logging.debug(
f"BanCheck: [B] i18n_instance from event_data: {type(i18n_to_use)}. Current lang: {current_lang}"
)
if not i18n_to_use:
i18n_to_use = self.i18n_main_instance
logging.warning(
f"BanCheck: [B_fallback] Using fallback i18n instance for banned user {event_user.id}. Type: {type(i18n_to_use)}"
)
if i18n_to_use:
_ = lambda k, **kw: i18n_to_use.gettext(
current_lang, k, **kw)
logging.debug(
"BanCheck: [D] Attempting to get 'user_is_banned' text."
)
ban_message_text = _("user_is_banned")
logging.debug(
f"BanCheck: [E] Ban message text: '{ban_message_text}'"
)
if self.settings.SUPPORT_LINK:
logging.debug(
"BanCheck: [F] Support link found. Attempting to get user_banned_keyboard."
)
keyboard = get_user_banned_keyboard(
self.settings.SUPPORT_LINK, current_lang,
i18n_to_use)
logging.debug(
f"BanCheck: [G] Keyboard created: {keyboard is not None}"
)
else:
logging.debug(
"BanCheck: [F_alt] No support link configured.")
else:
logging.error(
f"BanCheck: [CRITICAL] No i18n instance for user {event_user.id}. Using hardcoded text."
)
if self.settings.SUPPORT_LINK:
kb_temp = InlineKeyboardBuilder()
kb_temp.button(text="Support",
url=self.settings.SUPPORT_LINK)
keyboard = kb_temp.as_markup()
logging.debug(
"BanCheck: [G_alt] Fallback keyboard created due to no i18n."
)
logging.debug(
f"BanCheck: [H] Final pre-send check. Message: '{ban_message_text}', Keyboard: {keyboard is not None}"
)
if isinstance(actual_event_object, Message):
logging.debug(
f"BanCheck: [I_Msg] Attempting actual_event_object.answer for Message to user {event_user.id}"
)
await actual_event_object.answer(ban_message_text,
reply_markup=keyboard)
logging.info(
f"BanCheck: [J_Msg] Ban notification 'actual_event_object.answer' attempted for user {event_user.id} (Message)."
)
elif isinstance(actual_event_object, CallbackQuery):
logging.debug(
f"BanCheck: [I_CB] Attempting actual_event_object.answer (alert) for CallbackQuery to user {event_user.id}"
)
await actual_event_object.answer(ban_message_text,
show_alert=True)
logging.info(
f"BanCheck: [J_CB] Ban alert 'actual_event_object.answer' attempted for user {event_user.id} (CallbackQuery)."
)
target_message_obj = actual_event_object.message
if target_message_obj:
target_chat_id = target_message_obj.chat.id
if actual_event_object.message:
try:
logging.debug(
f"BanCheck: [K_CB_Edit] Attempting target_message_obj.edit_text for user {event_user.id}"
)
await target_message_obj.edit_text(
await actual_event_object.message.edit_text(
ban_message_text, reply_markup=keyboard)
logging.info(
f"BanCheck: [L_CB_Edit] Ban msg 'target_message_obj.edit_text' attempted for user {event_user.id} (Callback)."
)
except Exception as e_edit:
logging.warning(
f"BanCheck: [M_CB_EditFail] Failed to edit message for banned user {event_user.id}: {type(e_edit).__name__} - {e_edit}. Sending new message."
)
await actual_event_object.bot.send_message(
target_chat_id,
except (TelegramAPIError, AiogramError):
await bot_instance.send_message(
actual_event_object.from_user.id,
ban_message_text,
reply_markup=keyboard)
logging.info(
f"BanCheck: [N_CB_NewMsg] Ban msg 'actual_event_object.bot.send_message' (after edit fail) attempted for user {event_user.id} (Callback)."
)
else:
logging.warning(
f"BanCheck: [K_CB_NoMsg] CallbackQuery from {event_user.id} has no .message attribute. Sending new message directly."
)
await actual_event_object.bot.send_message(
await bot_instance.send_message(
actual_event_object.from_user.id,
ban_message_text,
reply_markup=keyboard)
logging.info(
f"BanCheck: [L_CB_NoMsg_NewMsg] Ban msg 'actual_event_object.bot.send_message' (no .message) attempted for user {event_user.id} (Callback)."
)
else:
logging.error(
f"BanCheck: [UNHANDLED_EVENT_TYPE_INTERNAL] actual_event_object type {type(actual_event_object)} was not Message or CallbackQuery."
)
except TelegramForbiddenError as e_forbidden:
await bot_instance.send_message(event_user.id,
ban_message_text,
reply_markup=keyboard)
logging.info(f"Ban notification sent to user {event_user.id}.")
except TelegramForbiddenError:
logging.warning(
f"BanCheck: TelegramForbiddenError sending ban msg to {event_user.id}: {e_forbidden}"
)
except TelegramBadRequest as e_bad_req:
f"BanCheck: Bot is blocked by user {event_user.id}.")
except Exception as e_send:
logging.error(
f"BanCheck: TelegramBadRequest sending ban msg to {event_user.id}: {e_bad_req}",
exc_info=True)
except TelegramAPIError as e_api:
logging.error(
f"BanCheck: TelegramAPIError sending ban msg to {event_user.id}: {e_api}",
exc_info=True)
except AiogramError as e_aio:
logging.error(
f"BanCheck: AiogramError sending ban msg to {event_user.id}: {e_aio}",
exc_info=True)
except Exception as e_general:
logging.error(
f"BanCheck: Generic failure preparing or sending ban notification to user {event_user.id}: {e_general}",
f"BanCheck: Failed to notify banned user {event_user.id}: {type(e_send).__name__} - {e_send}",
exc_info=True)
logging.debug(
f"BanCheck: [Z] End of ban processing for user {event_user.id}. Returning to stop further handlers."
)
return
return await handler(event, data)
+66 -69
View File
@@ -1,13 +1,13 @@
import logging
import json
import os
import aiosqlite
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, User, Update
from aiogram.types import User, Update
from sqlalchemy.ext.asyncio import AsyncSession
from db.database import get_user, get_db_connection_manager, _setup_db_connection
from db.dal import user_dal
from config.settings import Settings
@@ -25,7 +25,8 @@ class JsonI18n:
def _load_locales(self):
if not os.path.isdir(self.path):
logging.error(f"Locales path not found: {self.path}")
logging.error(
f"Locales path not found or not a directory: {self.path}")
return
for item in os.listdir(self.path):
if item.endswith(".json"):
@@ -34,52 +35,73 @@ class JsonI18n:
try:
with open(file_path, "r", encoding="utf-8") as f:
self.locales_data[lang_code] = json.load(f)
except Exception as e:
except json.JSONDecodeError as e_json_load:
logging.error(
f"Error loading locale {lang_code} from {file_path}: {e}"
f"Error loading locale {lang_code} from {file_path} (JSON Decode Error): {e_json_load}"
)
except Exception as e_load:
logging.error(
f"Error loading locale {lang_code} from {file_path}: {e_load}",
exc_info=True)
def gettext(self, lang_code: Optional[str], key: str, **kwargs) -> str:
effective_lang_code = lang_code
if not effective_lang_code or effective_lang_code not in self.locales_data:
effective_lang_code = self.default_lang
effective_lang_code = lang_code if lang_code and lang_code in self.locales_data else self.default_lang
lang_data = self.locales_data.get(effective_lang_code)
if lang_data is None:
logging.warning(
f"No language data for '{effective_lang_code}' (default: '{self.default_lang}'). Key '{key}'."
f"No language data for '{effective_lang_code}' (default '{self.default_lang}' also missing). Key '{key}' will be returned as is."
)
return key.format(**kwargs) if kwargs else key
text = lang_data.get(key)
if text is None:
if effective_lang_code != self.default_lang:
default_lang_data = self.locales_data.get(
self.default_lang, {})
text = default_lang_data.get(key)
if text is None: return key.format(**kwargs) if kwargs else key
if text is None:
logging.warning(
f"Translation key '{key}' not found for lang '{effective_lang_code}' or default '{self.default_lang}'. Returning key."
)
return key.format(**kwargs) if kwargs else key
try:
return text.format(**kwargs) if kwargs else text
except KeyError as e:
except KeyError as e_format:
logging.warning(
f"Missing format key {e} for key '{key}' (lang: {effective_lang_code}). Text: '{text}'"
f"Missing format key '{e_format}' for i18n key '{key}' (lang: {effective_lang_code}). Original text: '{text}'"
)
return text
except Exception as e:
except Exception as e_general_format:
logging.error(
f"Error formatting key '{key}' (lang: {effective_lang_code}): {e}. Text: '{text}'"
)
f"General error formatting i18n key '{key}' (lang: {effective_lang_code}): {e_general_format}. Original text: '{text}'",
exc_info=True)
return text
_i18n_instance: Optional[JsonI18n] = None
_i18n_instance_singleton: Optional[JsonI18n] = None
def get_i18n_instance(path: str = "locales",
default: str = "en",
domain: str = "bot") -> JsonI18n:
global _i18n_instance
if _i18n_instance is None:
_i18n_instance = JsonI18n(path=path, default=default, domain=domain)
return _i18n_instance
global _i18n_instance_singleton
if _i18n_instance_singleton is None:
if not os.path.exists(path) or not os.path.isdir(path):
logging.error(
f"CRITICAL: Locales directory '{path}' not found. i18n will not work correctly."
)
_i18n_instance_singleton = JsonI18n(path=path,
default=default,
domain=domain)
else:
_i18n_instance_singleton = JsonI18n(path=path,
default=default,
domain=domain)
return _i18n_instance_singleton
class I18nMiddleware(BaseMiddleware):
@@ -92,65 +114,40 @@ class I18nMiddleware(BaseMiddleware):
async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
Awaitable[Any]], event: Update,
data: Dict[str, Any]) -> Any:
session: AsyncSession = data["session"]
event_user: Optional[User] = data.get("event_from_user")
current_language = self.i18n.default_lang
if event_user:
logging.debug(
f"I18nMiddleware: Processing for user {event_user.id}")
try:
user_db_data = await get_user(event_user.id)
if user_db_data and user_db_data[
'language_code'] and user_db_data[
'language_code'] in self.i18n.locales_data:
current_language = user_db_data['language_code']
logging.debug(
f"I18nMiddleware: User {event_user.id} language loaded from DB: {current_language}"
)
else:
if event_user.language_code:
lang_prefix = event_user.language_code.split('-')[0]
if lang_prefix in self.i18n.locales_data:
current_language = lang_prefix
logging.debug(
f"I18nMiddleware: User {event_user.id} language set from Telegram client (prefix): {current_language}"
)
elif event_user.language_code in self.i18n.locales_data:
current_language = event_user.language_code
logging.debug(
f"I18nMiddleware: User {event_user.id} language set from Telegram client (full): {current_language}"
)
else:
logging.debug(
f"I18nMiddleware: User {event_user.id} Telegram client language '{event_user.language_code}' not supported. Using default: {current_language}"
)
else:
logging.debug(
f"I18nMiddleware: User {event_user.id} has no language_code from Telegram. Using default: {current_language}"
)
except Exception as e_db_lang:
logging.error(
f"I18nMiddleware: Error fetching user language from DB for user {event_user.id}: {e_db_lang}. Falling back."
)
if event_user.language_code:
lang_prefix = event_user.language_code.split('-')[0]
user_db_model = await user_dal.get_user_by_id(
session, event_user.id)
if user_db_model and user_db_model.language_code and user_db_model.language_code in self.i18n.locales_data:
current_language = user_db_model.language_code
elif event_user.language_code:
lang_prefix = event_user.language_code.split(
'-')[0].lower()
if lang_prefix in self.i18n.locales_data:
current_language = lang_prefix
elif event_user.language_code in self.i18n.locales_data:
current_language = event_user.language_code
elif event_user.language_code.lower(
) in self.i18n.locales_data:
current_language = event_user.language_code.lower()
except Exception as e_db_lang:
logging.error(
f"I18nMiddleware: Error fetching user lang from DB for {event_user.id}: {e_db_lang}. Falling back.",
exc_info=True)
if event_user.language_code:
lang_prefix = event_user.language_code.split(
'-')[0].lower()
if lang_prefix in self.i18n.locales_data:
current_language = lang_prefix
elif event_user.language_code.lower(
) in self.i18n.locales_data:
current_language = event_user.language_code.lower()
data["i18n_data"] = {
"i18n_instance": self.i18n,
"current_language": current_language
}
logging.debug(
f"I18nMiddleware: Final current_language for event: {current_language}"
)
return await handler(event, data)