feat: show linked emails in telegram logs

This commit is contained in:
3252a8
2026-05-24 22:32:35 +03:00
parent a60de46173
commit bafc5f4709
12 changed files with 129 additions and 31 deletions
@@ -234,6 +234,7 @@ def _serialize_log(entry: MessageLog) -> Dict[str, Any]:
"user_id": int(entry.user_id) if entry.user_id else None,
"telegram_username": entry.telegram_username,
"telegram_first_name": entry.telegram_first_name,
"email": getattr(getattr(entry, "author_user", None), "email", None),
"event_type": entry.event_type,
"content": entry.content,
"is_admin_event": bool(entry.is_admin_event),
+6 -1
View File
@@ -311,7 +311,12 @@ async def activate_trial_route(request: web.Request) -> web.Response:
notification_service = NotificationService(
request.app["bot"], settings, i18n_instance
)
await notification_service.notify_trial_activation(user_id, end_date)
await notification_service.notify_trial_activation(
user_id,
end_date,
username=db_user.username,
email=getattr(db_user, "email", None),
)
except Exception:
logger.exception("Failed to send WebApp trial activation notification")
+52 -21
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.utils.text_decorations import html_decoration as hd
from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.inline.admin_keyboards import (
@@ -25,6 +26,44 @@ USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _user_email(user: Optional[User]) -> str:
return str(getattr(user, "email", None) or "").strip()
def _format_user_with_email(
*,
first_name: Optional[str] = None,
username: Optional[str] = None,
email: Optional[str] = None,
fallback: str = "",
) -> str:
parts = []
if first_name:
parts.append(first_name)
if username:
parts.append(f"(@{username})")
display = " ".join(parts).strip() or str(fallback or "").strip()
clean_email = str(email or "").strip()
if clean_email:
display = (
f"{display} · {clean_email}" if display and display != clean_email else clean_email
)
return hd.quote(display)
def _format_log_entry_user(log_entry: MessageLog, translate) -> str:
fallback = (
translate("system_or_unknown_user") if not log_entry.user_id else f"ID: {log_entry.user_id}"
)
return _format_user_with_email(
first_name=log_entry.telegram_first_name,
username=log_entry.telegram_username,
email=_user_email(getattr(log_entry, "author_user", None)),
fallback=fallback,
)
async def display_logs_menu(
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
):
@@ -94,19 +133,7 @@ async def _display_formatted_logs(
log_entries_text = []
for log_entry_model in logs:
user_display_parts = []
if log_entry_model.telegram_first_name:
user_display_parts.append(log_entry_model.telegram_first_name)
if log_entry_model.telegram_username:
user_display_parts.append(f"(@{log_entry_model.telegram_username})")
user_display = " ".join(user_display_parts).strip()
if not user_display:
user_display = (
_("system_or_unknown_user")
if not log_entry_model.user_id
else f"ID: {log_entry_model.user_id}"
)
user_display = _format_log_entry_user(log_entry_model, _)
user_id_display = (
str(log_entry_model.user_id) if log_entry_model.user_id is not None else "N/A"
@@ -270,10 +297,11 @@ async def process_user_id_for_logs_handler(
return
target_user_id = user_model_for_logs.user_id
user_display_name = user_model_for_logs.first_name or (
f"@{user_model_for_logs.username}"
if user_model_for_logs.username
else (user_model_for_logs.email or f"ID {target_user_id}")
user_display_name = _format_user_with_email(
first_name=user_model_for_logs.first_name,
username=user_model_for_logs.username,
email=user_model_for_logs.email,
fallback=f"ID {target_user_id}",
)
logs_models = await message_log_dal.get_user_message_logs(
@@ -319,10 +347,11 @@ async def view_user_logs_paginated_handler(
await callback.answer()
return
user_display_name = user_model_for_logs.first_name or (
f"@{user_model_for_logs.username}"
if user_model_for_logs.username
else (user_model_for_logs.email or f"ID {target_user_id}")
user_display_name = _format_user_with_email(
first_name=user_model_for_logs.first_name,
username=user_model_for_logs.username,
email=user_model_for_logs.email,
fallback=f"ID {target_user_id}",
)
logs_models = await message_log_dal.get_user_message_logs(
@@ -392,6 +421,7 @@ async def export_logs_csv_handler(
_("admin_csv_header_user_id"),
_("admin_csv_header_telegram_username"),
_("admin_csv_header_telegram_first_name"),
_("admin_csv_header_email"),
_("admin_csv_header_event_type"),
_("admin_csv_header_content"),
_("admin_csv_header_is_admin_event"),
@@ -417,6 +447,7 @@ async def export_logs_csv_handler(
log.user_id or "",
log.telegram_username or "",
log.telegram_first_name or "",
_user_email(getattr(log, "author_user", None)),
log.event_type or "",
content_clean,
"Yes" if log.is_admin_event else "No",
+3
View File
@@ -21,6 +21,7 @@ from bot.utils.install_links import (
ensure_user_install_guide_links,
)
from config.settings import Settings
from db.dal import user_dal
from .start import send_main_menu
@@ -137,10 +138,12 @@ async def process_promo_code_input(
from bot.services.notification_service import NotificationService
notification_service = NotificationService(bot, settings, i18n)
db_user = await user_dal.get_user_by_id(session, user.id)
await notification_service.notify_suspicious_promo_attempt(
user_id=user.id,
username=user.username,
first_name=user.first_name,
email=getattr(db_user, "email", None) if db_user else None,
suspicious_input=code_input,
)
except Exception as e:
+15 -2
View File
@@ -19,6 +19,7 @@ from bot.utils.install_links import (
ensure_user_install_guide_links,
)
from config.settings import Settings
from db.dal import user_dal
from .start import send_main_menu
@@ -119,7 +120,13 @@ async def request_trial_confirmation_handler(
# Send notification to admin about new trial
notification_service = NotificationService(callback.bot, settings, i18n)
await notification_service.notify_trial_activation(user_id, end_date_obj)
db_user = await user_dal.get_user_by_id(session, user_id)
await notification_service.notify_trial_activation(
user_id,
end_date_obj,
username=db_user.username if db_user else callback.from_user.username,
email=getattr(db_user, "email", None) if db_user else None,
)
# Mark ad attribution trial if exists
try:
from db.dal import ad_dal as _ad_dal
@@ -316,7 +323,13 @@ async def confirm_activate_trial_handler(
if activation_result and activation_result.get("activated") and end_date_obj:
notification_service = NotificationService(callback.bot, settings, i18n)
await notification_service.notify_trial_activation(user_id, end_date_obj)
db_user = await user_dal.get_user_by_id(session, user_id)
await notification_service.notify_trial_activation(
user_id,
end_date_obj,
username=db_user.username if db_user else callback.from_user.username,
email=getattr(db_user, "email", None) if db_user else None,
)
try:
from db.dal import ad_dal as _ad_dal
@@ -190,6 +190,7 @@ async def notify_admins_payment_received(
traffic_is_premium: bool,
tariff_key: Optional[str],
log_prefix: str = "payment_providers",
email: Optional[str] = None,
) -> None:
"""Push the standard ``notify_payment_received`` to the admin log channel."""
try:
@@ -202,6 +203,7 @@ async def notify_admins_payment_received(
traffic_gb=traffic_gb_for_admin,
payment_provider=payment_provider,
username=username,
email=email,
traffic_is_premium=traffic_is_premium,
tariff_key=tariff_key,
)
@@ -398,6 +400,7 @@ async def finalize_successful_payment(
traffic_gb_for_admin=traffic_gb_for_activation,
payment_provider=req.provider_notification,
username=db_user.username if db_user else None,
email=getattr(db_user, "email", None) if db_user else None,
traffic_is_premium=base == "premium_topup",
tariff_key=tariff_key,
log_prefix=req.log_prefix,
@@ -874,6 +874,7 @@ async def process_successful_payment(
),
payment_provider="yookassa",
username=user_for_notify.username if user_for_notify else None,
email=getattr(user_for_notify, "email", None) if user_for_notify else None,
traffic_is_premium=sale_mode_base == "premium_topup",
tariff_key=tariff_for_log,
log_prefix="YooKassa webhook",
+36 -6
View File
@@ -56,11 +56,16 @@ class NotificationService:
user_id: int,
username: Optional[str] = None,
first_name: Optional[str] = None,
email: Optional[str] = None,
) -> str:
base_display = display_name_or_fallback(first_name, f"ID {user_id}")
if username:
base_display = f"{base_display} ({username_for_display(username)})"
return base_display
safe_display = hd.quote(base_display)
clean_email = str(email or "").strip()
if clean_email:
safe_display = f"{safe_display} · <code>{hd.quote(clean_email)}</code>"
return safe_display
@staticmethod
def _build_profile_keyboard(
@@ -254,8 +259,13 @@ class NotificationService:
part for part in [user.first_name, getattr(user, "last_name", None)] if part
)
if user.username:
return f"{name or user.username} (@{user.username})"
return name or getattr(user, "email", None) or f"ID {user.user_id}"
display = f"{name or user.username} (@{user.username})"
else:
display = name or f"ID {user.user_id}"
email = str(getattr(user, "email", None) or "").strip()
if email:
return f"{display} · {email}" if display and display != email else email
return display
@staticmethod
def _support_snapshot_rows(snapshot: Optional[dict]) -> list[tuple[str, str]]:
@@ -540,6 +550,7 @@ class NotificationService:
user_id: int,
username: Optional[str] = None,
first_name: Optional[str] = None,
email: Optional[str] = None,
referred_by_id: Optional[int] = None,
):
"""Send notification about new user registration"""
@@ -553,6 +564,7 @@ class NotificationService:
user_id=user_id,
username=username,
first_name=first_name,
email=email,
)
referral_text = ""
@@ -640,6 +652,7 @@ class NotificationService:
user_id=telegram_id or user_id,
username=username,
first_name=first_name,
email=email,
)
message = _(
@@ -676,6 +689,7 @@ class NotificationService:
user_id=telegram_id,
username=username,
first_name=first_name,
email=email,
)
message = _(
@@ -715,6 +729,7 @@ class NotificationService:
user_id=display_user_id,
username=username,
first_name=first_name,
email=email,
)
message = _(
@@ -761,6 +776,7 @@ class NotificationService:
months: int,
payment_provider: str,
username: Optional[str] = None,
email: Optional[str] = None,
traffic_gb: Optional[float] = None,
*,
traffic_is_premium: bool = False,
@@ -776,6 +792,7 @@ class NotificationService:
user_display = self._format_user_display(
user_id=user_id,
username=username,
email=email,
)
try:
@@ -827,7 +844,12 @@ class NotificationService:
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_promo_activation(
self, user_id: int, promo_code: str, bonus_days: int, username: Optional[str] = None
self,
user_id: int,
promo_code: str,
bonus_days: int,
username: Optional[str] = None,
email: Optional[str] = None,
):
"""Send notification about promo code activation"""
if not self.settings.LOG_PROMO_ACTIVATIONS:
@@ -839,6 +861,7 @@ class NotificationService:
user_display = self._format_user_display(
user_id=user_id,
username=username,
email=email,
)
message = _(
@@ -854,7 +877,11 @@ class NotificationService:
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_trial_activation(
self, user_id: int, end_date: datetime, username: Optional[str] = None
self,
user_id: int,
end_date: datetime,
username: Optional[str] = None,
email: Optional[str] = None,
):
"""Send notification about trial activation"""
if not self.settings.LOG_TRIAL_ACTIVATIONS:
@@ -866,6 +893,7 @@ class NotificationService:
user_display = self._format_user_display(
user_id=user_id,
username=username,
email=email,
)
message = _(
@@ -918,6 +946,7 @@ class NotificationService:
suspicious_input: str,
username: Optional[str] = None,
first_name: Optional[str] = None,
email: Optional[str] = None,
):
"""Send notification about a suspicious promo code attempt."""
if not self.settings.LOG_SUSPICIOUS_ACTIVITY:
@@ -930,11 +959,12 @@ class NotificationService:
user_id=user_id,
username=username,
first_name=first_name,
email=email,
)
message = _(
"log_suspicious_promo",
user_display=hd.quote(user_display),
user_display=user_display,
user_id=user_id,
suspicious_input=hd.quote(suspicious_input),
timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"),
@@ -112,6 +112,7 @@ class PromoCodeService:
promo_code=code_input_upper,
bonus_days=bonus_days,
username=user.username if user else None,
email=getattr(user, "email", None) if user else None,
)
except Exception as e:
logging.error(f"Failed to send promo activation notification: {e}")
+9 -1
View File
@@ -4,6 +4,7 @@ from typing import List, Optional
from sqlalchemy import func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from ..models import MessageLog
@@ -22,7 +23,13 @@ async def create_message_log(session: AsyncSession, log_data: dict) -> Optional[
async def get_all_message_logs(session: AsyncSession, limit: int, offset: int) -> List[MessageLog]:
stmt = select(MessageLog).order_by(MessageLog.timestamp.desc()).limit(limit).offset(offset)
stmt = (
select(MessageLog)
.options(selectinload(MessageLog.author_user), selectinload(MessageLog.target_user))
.order_by(MessageLog.timestamp.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(stmt)
return result.scalars().all()
@@ -38,6 +45,7 @@ async def get_user_message_logs(
) -> List[MessageLog]:
stmt = (
select(MessageLog)
.options(selectinload(MessageLog.author_user), selectinload(MessageLog.target_user))
.where(
or_(
MessageLog.user_id == user_id_to_search,
+1
View File
@@ -396,6 +396,7 @@
"admin_csv_header_user_id": "User ID",
"admin_csv_header_telegram_username": "Username",
"admin_csv_header_telegram_first_name": "First Name",
"admin_csv_header_email": "Email",
"admin_csv_header_event_type": "Event Type",
"admin_csv_header_content": "Content",
"admin_csv_header_is_admin_event": "Admin Event",
+1
View File
@@ -396,6 +396,7 @@
"admin_csv_header_user_id": "ID Пользователя",
"admin_csv_header_telegram_username": "Username",
"admin_csv_header_telegram_first_name": "Имя",
"admin_csv_header_email": "Email",
"admin_csv_header_event_type": "Тип события",
"admin_csv_header_content": "Содержание",
"admin_csv_header_is_admin_event": "Админ событие",