refactor: project architecture refactor, container splitting
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import CallbackQuery, Message, Update, User
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.dal import message_log_dal, user_dal
|
||||
|
||||
|
||||
class ActionLoggerMiddleware(BaseMiddleware):
|
||||
def __init__(self, settings: Settings):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Update, Dict[str, Any]], 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")
|
||||
|
||||
user_id: Optional[int] = None
|
||||
telegram_username: Optional[str] = None
|
||||
telegram_first_name: Optional[str] = None
|
||||
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 in self.settings.ADMIN_IDS:
|
||||
is_admin_event_flag = True
|
||||
|
||||
if is_admin_event_flag and not self.settings.LOG_ADMIN_ACTIONS:
|
||||
return result
|
||||
|
||||
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: Message = event.message
|
||||
if msg.text:
|
||||
content = msg.text
|
||||
if msg.text.startswith("/"):
|
||||
current_event_type = f"command:{msg.text.split()[0]}"
|
||||
|
||||
else:
|
||||
content = f"[{msg.content_type or 'unknown_content_type'}]"
|
||||
current_event_type = f"message:{msg.content_type or 'unknown'}"
|
||||
elif event.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 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." # noqa: E501
|
||||
)
|
||||
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 message_log_dal.create_message_log_no_commit(session, log_payload)
|
||||
except Exception as e_log:
|
||||
logging.error(
|
||||
f"ActionLoggerMiddleware: Failed to add log to session for user {user_id}, type {current_event_type}: {e_log}", # noqa: E501
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Union
|
||||
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.exceptions import (
|
||||
AiogramError,
|
||||
TelegramAPIError,
|
||||
TelegramForbiddenError,
|
||||
)
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message, Update, User
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
|
||||
from ..keyboards.inline.user_keyboards import get_user_banned_keyboard
|
||||
from .i18n import JsonI18n
|
||||
|
||||
|
||||
class BanCheckMiddleware(BaseMiddleware):
|
||||
def __init__(self, settings: Settings, i18n_instance: JsonI18n):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
self.i18n_main_instance = i18n_instance
|
||||
|
||||
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")
|
||||
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_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_model and db_user_model.is_banned:
|
||||
logging.info(
|
||||
f"User {event_user.id} ({event_user.username or 'NoUsername'}) is banned. Blocking access." # noqa: E501
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
if isinstance(actual_event_object, Message):
|
||||
await actual_event_object.answer(ban_message_text, reply_markup=keyboard)
|
||||
elif isinstance(actual_event_object, CallbackQuery):
|
||||
await actual_event_object.answer(ban_message_text, show_alert=True)
|
||||
if actual_event_object.message:
|
||||
try:
|
||||
await actual_event_object.message.edit_text(
|
||||
ban_message_text, reply_markup=keyboard
|
||||
)
|
||||
except (TelegramAPIError, AiogramError):
|
||||
await bot_instance.send_message(
|
||||
actual_event_object.from_user.id,
|
||||
ban_message_text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
else:
|
||||
await bot_instance.send_message(
|
||||
actual_event_object.from_user.id,
|
||||
ban_message_text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
else:
|
||||
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: Bot is blocked by user {event_user.id}.")
|
||||
except Exception as e_send:
|
||||
logging.error(
|
||||
f"BanCheck: Failed to notify banned user {event_user.id}: {type(e_send).__name__} - {e_send}", # noqa: E501
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return
|
||||
return await handler(event, data)
|
||||
@@ -0,0 +1,140 @@
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
Message,
|
||||
Update,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_channel_subscription_keyboard
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
|
||||
|
||||
class ChannelSubscriptionMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Blocks access to handlers for users who have not yet passed the required channel subscription check.
|
||||
The /start command is allowed through so that the handler can re-run the verification.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(self, settings: Settings, i18n_instance: JsonI18n):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
self.i18n_main_instance = i18n_instance
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Update,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
required_channel_id = self.settings.REQUIRED_CHANNEL_ID
|
||||
if not required_channel_id:
|
||||
return await handler(event, data)
|
||||
|
||||
event_user = data.get("event_from_user")
|
||||
if not event_user or event_user.id in self.settings.ADMIN_IDS:
|
||||
return await handler(event, data)
|
||||
|
||||
callback_query = event.callback_query
|
||||
if (
|
||||
callback_query
|
||||
and callback_query.data
|
||||
and callback_query.data == "channel_subscription:verify"
|
||||
):
|
||||
return await handler(event, data)
|
||||
|
||||
# Allow /start to reach the handler so the check can be re-run.
|
||||
message_object: Optional[Message] = event.message
|
||||
if message_object and message_object.text and message_object.text.startswith("/start"):
|
||||
return await handler(event, data)
|
||||
|
||||
session: AsyncSession = data["session"]
|
||||
try:
|
||||
db_user = await user_dal.get_user_by_id(session, event_user.id)
|
||||
except Exception as db_error:
|
||||
logging.error(
|
||||
"ChannelSubscriptionMiddleware: failed to fetch user %s: %s",
|
||||
event_user.id,
|
||||
db_error,
|
||||
exc_info=True,
|
||||
)
|
||||
return await handler(event, data)
|
||||
|
||||
if not db_user:
|
||||
return await handler(event, data)
|
||||
|
||||
if (
|
||||
db_user.channel_subscription_verified
|
||||
and db_user.channel_subscription_verified_for == required_channel_id
|
||||
):
|
||||
return await handler(event, data)
|
||||
|
||||
i18n_payload: Dict[str, Any] = data.get("i18n_data", {})
|
||||
current_lang: str = i18n_payload.get("current_language", self.settings.DEFAULT_LANGUAGE)
|
||||
i18n_instance: Optional[JsonI18n] = i18n_payload.get(
|
||||
"i18n_instance", self.i18n_main_instance
|
||||
)
|
||||
|
||||
def translate(key: str) -> str:
|
||||
if i18n_instance:
|
||||
return i18n_instance.gettext(current_lang, key)
|
||||
return key
|
||||
|
||||
keyboard = (
|
||||
get_channel_subscription_keyboard(
|
||||
current_lang, i18n_instance, self.settings.REQUIRED_CHANNEL_LINK
|
||||
)
|
||||
if i18n_instance
|
||||
else None
|
||||
)
|
||||
prompt_text = translate("channel_subscription_required")
|
||||
|
||||
if event.callback_query:
|
||||
await self._handle_callback(event.callback_query, prompt_text, keyboard, data)
|
||||
return
|
||||
|
||||
if message_object:
|
||||
await message_object.answer(prompt_text, reply_markup=keyboard)
|
||||
else:
|
||||
bot_instance = data["bot"]
|
||||
await bot_instance.send_message(
|
||||
chat_id=event_user.id,
|
||||
text=prompt_text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
return
|
||||
|
||||
async def _handle_callback(
|
||||
self,
|
||||
callback: CallbackQuery,
|
||||
prompt_text: str,
|
||||
keyboard,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
try:
|
||||
await callback.answer(prompt_text, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.answer(prompt_text, reply_markup=keyboard)
|
||||
except Exception as send_error:
|
||||
logging.error(
|
||||
"ChannelSubscriptionMiddleware: failed to send prompt for callback in chat %s: %s", # noqa: E501
|
||||
callback.message.chat.id,
|
||||
send_error,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
bot_instance = data["bot"]
|
||||
await bot_instance.send_message(
|
||||
chat_id=callback.from_user.id,
|
||||
text=prompt_text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Update
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
|
||||
class DBSessionMiddleware(BaseMiddleware):
|
||||
def __init__(self, async_session_factory: sessionmaker):
|
||||
super().__init__()
|
||||
self.async_session_factory = async_session_factory
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Update,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
if self.async_session_factory is None:
|
||||
logging.critical("DBSessionMiddleware: async_session_factory is None!")
|
||||
raise RuntimeError("async_session_factory not provided to DBSessionMiddleware")
|
||||
|
||||
async with self.async_session_factory() as session:
|
||||
data["session"] = session
|
||||
try:
|
||||
result = await handler(event, data)
|
||||
|
||||
await session.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logging.error("DBSessionMiddleware: Exception caused rollback.", exc_info=True)
|
||||
raise
|
||||
@@ -0,0 +1,161 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Update, User
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
|
||||
|
||||
class JsonI18n:
|
||||
def __init__(self, path: str, default: str = "en", domain: str = "bot"):
|
||||
self.domain = domain
|
||||
self.path = path
|
||||
self.default_lang = default
|
||||
self.locales_data: Dict[str, Dict[str, str]] = {}
|
||||
self._load_locales()
|
||||
logging.info(
|
||||
f"JsonI18n initialized. Loaded languages: {list(self.locales_data.keys())}. Default: {self.default_lang}" # noqa: E501
|
||||
)
|
||||
|
||||
def _load_locales(self):
|
||||
if not os.path.isdir(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"):
|
||||
lang_code = item.split(".")[0]
|
||||
file_path = os.path.join(self.path, item)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
self.locales_data[lang_code] = json.load(f)
|
||||
except json.JSONDecodeError as e_json_load:
|
||||
logging.error(
|
||||
f"Error loading locale {lang_code} from {file_path} (JSON Decode Error): {e_json_load}" # noqa: E501
|
||||
)
|
||||
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:
|
||||
# Determine effective language with robust fallback
|
||||
if lang_code and lang_code in self.locales_data:
|
||||
effective_lang_code = lang_code
|
||||
elif self.default_lang in self.locales_data:
|
||||
effective_lang_code = self.default_lang
|
||||
elif "en" in self.locales_data:
|
||||
effective_lang_code = "en"
|
||||
else:
|
||||
effective_lang_code = lang_code or self.default_lang
|
||||
|
||||
lang_data = self.locales_data.get(effective_lang_code)
|
||||
if lang_data is None:
|
||||
# Try explicit fallback to English if available
|
||||
fallback_data = self.locales_data.get("en")
|
||||
if fallback_data is not None:
|
||||
text = fallback_data.get(key)
|
||||
if text is not None:
|
||||
try:
|
||||
return text.format(**kwargs) if kwargs else text
|
||||
except Exception:
|
||||
return text
|
||||
logging.warning(
|
||||
f"No language data for '{effective_lang_code}' (default '{self.default_lang}' also missing). Key '{key}' will be returned as is." # noqa: E501
|
||||
)
|
||||
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:
|
||||
logging.warning(
|
||||
f"Translation key '{key}' not found for lang '{effective_lang_code}' or default '{self.default_lang}'. Returning key." # noqa: E501
|
||||
)
|
||||
return key.format(**kwargs) if kwargs else key
|
||||
try:
|
||||
return text.format(**kwargs) if kwargs else text
|
||||
except KeyError as e_format:
|
||||
logging.warning(
|
||||
f"Missing format key '{e_format}' for i18n key '{key}' (lang: {effective_lang_code}). Original text: '{text}'" # noqa: E501
|
||||
)
|
||||
return text
|
||||
except Exception as e_general_format:
|
||||
logging.error(
|
||||
f"General error formatting i18n key '{key}' (lang: {effective_lang_code}): {e_general_format}. Original text: '{text}'", # noqa: E501
|
||||
exc_info=True,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
_i18n_instance_singleton: Optional[JsonI18n] = None
|
||||
|
||||
|
||||
def get_i18n_instance(path: str = "locales", default: str = "en", domain: str = "bot") -> JsonI18n:
|
||||
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):
|
||||
def __init__(self, i18n: JsonI18n, settings: Settings):
|
||||
super().__init__()
|
||||
self.i18n = i18n
|
||||
self.settings = settings
|
||||
|
||||
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:
|
||||
try:
|
||||
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.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.", # noqa: E501
|
||||
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}
|
||||
return await handler(event, data)
|
||||
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Update
|
||||
from aiogram.types import User as TgUser
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username, username_for_display
|
||||
from db.dal import user_dal
|
||||
|
||||
|
||||
class ProfileSyncMiddleware(BaseMiddleware):
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Update,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
session: AsyncSession = data.get("session")
|
||||
tg_user: Optional[TgUser] = data.get("event_from_user")
|
||||
|
||||
if session and tg_user:
|
||||
try:
|
||||
db_user = await user_dal.get_user_by_telegram_id(session, tg_user.id)
|
||||
if not db_user:
|
||||
db_user = await user_dal.get_user_by_id(session, tg_user.id)
|
||||
if db_user:
|
||||
update_payload: Dict[str, Any] = {}
|
||||
sanitized_username = sanitize_username(tg_user.username)
|
||||
sanitized_first_name = sanitize_display_name(tg_user.first_name)
|
||||
sanitized_last_name = sanitize_display_name(tg_user.last_name)
|
||||
|
||||
if db_user.telegram_id != tg_user.id:
|
||||
update_payload["telegram_id"] = tg_user.id
|
||||
if db_user.username != sanitized_username:
|
||||
update_payload["username"] = sanitized_username
|
||||
if db_user.first_name != sanitized_first_name:
|
||||
update_payload["first_name"] = sanitized_first_name
|
||||
if db_user.last_name != sanitized_last_name:
|
||||
update_payload["last_name"] = sanitized_last_name
|
||||
|
||||
if update_payload:
|
||||
await user_dal.update_user(session, db_user.user_id, update_payload)
|
||||
logging.info(
|
||||
f"ProfileSyncMiddleware: Updated user {tg_user.id} profile fields: {list(update_payload.keys())}" # noqa: E501
|
||||
)
|
||||
|
||||
# Also update description on panel if linked
|
||||
try:
|
||||
panel_service = data.get("panel_service")
|
||||
if panel_service and db_user.panel_user_uuid:
|
||||
description_text = "\n".join(
|
||||
[
|
||||
db_user.email or "",
|
||||
username_for_display(tg_user.username, with_at=False)
|
||||
if sanitized_username is not None
|
||||
else "",
|
||||
sanitized_first_name or "",
|
||||
sanitized_last_name or "",
|
||||
]
|
||||
).strip()
|
||||
panel_payload = {
|
||||
"description": description_text,
|
||||
"telegramId": tg_user.id,
|
||||
}
|
||||
if db_user.email:
|
||||
panel_payload["email"] = db_user.email
|
||||
await panel_service.update_user_details_on_panel(
|
||||
db_user.panel_user_uuid,
|
||||
panel_payload,
|
||||
)
|
||||
except Exception as e_upd_desc:
|
||||
logging.warning(
|
||||
f"ProfileSyncMiddleware: Failed to update panel description for user {tg_user.id}: {e_upd_desc}" # noqa: E501
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"ProfileSyncMiddleware: Failed to sync profile for user {getattr(tg_user, 'id', 'N/A')}: {e}", # noqa: E501
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return await handler(event, data)
|
||||
Reference in New Issue
Block a user