fix: log account merge notifications

This commit is contained in:
3252a8
2026-05-22 22:02:28 +03:00
parent c4c5b8e3a0
commit 19aa2ec9c9
7 changed files with 181 additions and 2 deletions
+25 -1
View File
@@ -2,7 +2,11 @@
from ._runtime import * # noqa: F403,F405
from bot.app.web.webapp.cache_helpers import webapp_cached_user_payload
from .auth import _hash_email_password, _sync_merged_panel_identity_for_user
from .auth import (
_hash_email_password,
_notify_account_merged,
_sync_merged_panel_identity_for_user,
)
from .common import _invalidate_webapp_user_caches
@@ -164,6 +168,16 @@ async def account_email_verify_route(request: web.Request) -> web.Response:
return _json_error(500, "link_failed", "Link failed")
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
if merge_notice:
await _notify_account_merged(
request,
settings,
merge_notice=merge_notice,
email=final_email,
telegram_id=final_telegram_id,
username=final_username,
first_name=final_first_name,
)
if should_notify_email_linked:
try:
from bot.services.notification_service import NotificationService
@@ -372,6 +386,16 @@ async def account_telegram_link_route(request: web.Request) -> web.Response:
return _json_error(500, "link_failed", "Link failed")
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
if merge_notice:
await _notify_account_merged(
request,
settings,
merge_notice=merge_notice,
email=final_email,
telegram_id=final_telegram_id,
username=final_username,
first_name=final_first_name,
)
if should_notify_telegram_linked and final_telegram_id:
try:
from bot.services.notification_service import NotificationService
+45
View File
@@ -426,6 +426,15 @@ async def telegram_oauth_callback_route(request: web.Request) -> web.Response:
final_panel_uuid=link_final_panel_uuid,
expire_at=merge_end_date,
)
await _notify_account_merged(
request,
settings,
merge_notice=link_merge_notice,
email=linked_user_for_panel.email,
telegram_id=_telegram_id_for_user(linked_user_for_panel),
username=linked_user_for_panel.username,
first_name=linked_user_for_panel.first_name,
)
token = create_webapp_session_token(settings, int(final_user_id))
response = web.HTTPFound(_telegram_oauth_redirect_url(redirect_path, status="success"))
@@ -1148,6 +1157,42 @@ async def _build_account_merge_notice(
}
async def _notify_account_merged(
request: web.Request,
settings: Settings,
*,
merge_notice: Optional[Dict[str, Any]],
email: Optional[str],
telegram_id: Optional[int],
username: Optional[str],
first_name: Optional[str],
) -> None:
if not merge_notice:
return
try:
from bot.services.notification_service import NotificationService
bot: Bot = request.app["bot"]
notification_service = NotificationService(
bot,
settings,
request.app.get("i18n"),
)
await notification_service.notify_account_merged(
primary_user_id=int(merge_notice.get("primary_user_id") or 0),
removed_user_id=int(merge_notice.get("removed_user_id") or 0),
email=email,
telegram_id=telegram_id,
username=username,
first_name=first_name,
final_end_date_text=str(merge_notice.get("final_end_date_text") or ""),
primary_panel_user_uuid=merge_notice.get("primary_panel_user_uuid"),
removed_panel_user_uuid=merge_notice.get("removed_panel_user_uuid"),
)
except Exception:
logger.exception("Failed to send account merged notification")
def _apply_telegram_profile_to_user(
user: User,
telegram_user: Dict[str, Any],
@@ -690,6 +690,51 @@ class NotificationService:
profile_keyboard = self._build_profile_keyboard(_, telegram_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_account_merged(
self,
*,
primary_user_id: int,
removed_user_id: int,
email: Optional[str],
telegram_id: Optional[int],
username: Optional[str] = None,
first_name: Optional[str] = None,
final_end_date_text: Optional[str] = None,
primary_panel_user_uuid: Optional[str] = None,
removed_panel_user_uuid: Optional[str] = None,
):
"""Send notification when duplicate email/Telegram accounts are merged."""
if not self.settings.LOG_NEW_USERS:
return
admin_lang = self.settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k
display_user_id = int(telegram_id or primary_user_id)
user_display = self._format_user_display(
user_id=display_user_id,
username=username,
first_name=first_name,
)
message = _(
"log_account_merged",
primary_user_id=primary_user_id,
removed_user_id=removed_user_id,
telegram_id=telegram_id or "",
user_display=user_display,
email=hd.quote(email or ""),
final_end_date=hd.quote(final_end_date_text or ""),
primary_panel_user_uuid=hd.quote(primary_panel_user_uuid or ""),
removed_panel_user_uuid=hd.quote(removed_panel_user_uuid or ""),
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
profile_keyboard = (
self._build_profile_keyboard(_, int(telegram_id)) if telegram_id else None
)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
def _format_traffic_gb_admin(self, traffic_gb: float) -> str:
value = float(traffic_gb)
if value.is_integer():
+1
View File
@@ -363,6 +363,7 @@
"log_new_email_user_registration": "📧 <b>New User (email)</b>\n\n🆔 ID: <code>{user_id}</code>\n📧 Email: <code>{email}</code>{referral_text}\n📅 Time: {timestamp}",
"log_account_email_linked": "📧 <b>Email linked</b>\n\n🆔 User ID: <code>{user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 User: {user_display}\n📧 Email: <code>{email}</code>\n🕐 Time: {timestamp}",
"log_account_telegram_linked": "📨 <b>Telegram linked</b>\n\n🆔 User ID: <code>{user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 User: {user_display}\n📧 Email: <code>{email}</code>\n🕐 Time: {timestamp}",
"log_account_merged": "🔗 <b>Accounts merged</b>\n\n🆔 Kept user ID: <code>{primary_user_id}</code>\n🗑 Removed user ID: <code>{removed_user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 User: {user_display}\n📧 Email: <code>{email}</code>\n⏰ New end date: <b>{final_end_date}</b>\n📋 Kept panel UUID: <code>{primary_panel_user_uuid}</code>\n📋 Removed panel UUID: <code>{removed_panel_user_uuid}</code>\n🕐 Time: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
"log_payment_received_traffic": "{provider_emoji} <b>Payment Received (traffic top-up)</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n🗂 {traffic_summary}\n{tariff_line}🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
"log_payment_traffic_purchase_line": "<b>{gb} GB</b> · {kind}",
+1
View File
@@ -363,6 +363,7 @@
"log_new_email_user_registration": "📧 <b>Новый пользователь (email)</b>\n\n🆔 ID: <code>{user_id}</code>\n📧 Email: <code>{email}</code>{referral_text}\n📅 Время: {timestamp}",
"log_account_email_linked": "📧 <b>Привязана почта</b>\n\n🆔 ID пользователя: <code>{user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 Пользователь: {user_display}\n📧 Email: <code>{email}</code>\n🕐 Время: {timestamp}",
"log_account_telegram_linked": "📨 <b>Привязан Telegram</b>\n\n🆔 ID пользователя: <code>{user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 Пользователь: {user_display}\n📧 Email: <code>{email}</code>\n🕐 Время: {timestamp}",
"log_account_merged": "🔗 <b>Аккаунты объединены</b>\n\n🆔 Оставлен ID: <code>{primary_user_id}</code>\n🗑 Удалён ID: <code>{removed_user_id}</code>\n📨 Telegram ID: <code>{telegram_id}</code>\n👤 Пользователь: {user_display}\n📧 Email: <code>{email}</code>\n⏰ Новая дата окончания: <b>{final_end_date}</b>\n📋 UUID оставленного в панели: <code>{primary_panel_user_uuid}</code>\n📋 UUID удалённого в панели: <code>{removed_panel_user_uuid}</code>\n🕐 Время: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
"log_payment_received_traffic": "{provider_emoji} <b>Получен платеж (докупка трафика)</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n🗂 {traffic_summary}\n{tariff_line}🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
"log_payment_traffic_purchase_line": "<b>{gb} ГБ</b> · {kind}",
+16 -1
View File
@@ -269,6 +269,10 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase):
"last_name": "",
"language_code": "ru",
}
notification_service = SimpleNamespace(
notify_account_telegram_linked=AsyncMock(),
notify_account_merged=AsyncMock(),
)
with (
patch.object(account_routes, "_require_user_id", return_value=-100),
@@ -299,7 +303,7 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase):
),
patch(
"bot.services.notification_service.NotificationService",
return_value=SimpleNamespace(notify_account_telegram_linked=AsyncMock()),
return_value=notification_service,
),
):
response = await account_routes.account_telegram_link_route(request)
@@ -325,4 +329,15 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(update_uuid, "panel-telegram")
self.assertEqual(update_payload["email"], "linked@example.com")
self.assertEqual(update_payload["telegramId"], 42)
notification_service.notify_account_merged.assert_awaited_once_with(
primary_user_id=42,
removed_user_id=-100,
email="linked@example.com",
telegram_id=42,
username="alice",
first_name="Alice",
final_end_date_text="",
primary_panel_user_uuid="panel-telegram",
removed_panel_user_uuid="panel-email",
)
self.assertIn("rw_webapp_session", response.cookies)
+48
View File
@@ -364,3 +364,51 @@ def test_support_user_reply_can_send_email_without_telegram_channels():
assert channels == []
assert emails[0]["ticket_id"] == 7
def test_account_merge_notification_goes_to_log_channel():
messages = []
class I18n:
def gettext(self, _language, key, **kwargs):
if key == "log_open_profile_link":
return "Open profile"
assert key == "log_account_merged"
return (
f"merged primary={kwargs['primary_user_id']} "
f"removed={kwargs['removed_user_id']} "
f"email={kwargs['email']} end={kwargs['final_end_date']}"
)
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(LOG_CHAT_ID=-100123, DEFAULT_LANGUAGE="en"),
i18n=I18n(),
)
async def send_to_log_channel(message, thread_id=None, reply_markup=None):
messages.append((message, thread_id, reply_markup))
service._send_to_log_channel = send_to_log_channel
asyncio.run(
service.notify_account_merged(
primary_user_id=42,
removed_user_id=-100,
email="paid@example.com",
telegram_id=100200300,
username="alice",
first_name="Alice",
final_end_date_text="2026-06-21 10:00",
primary_panel_user_uuid="panel-telegram",
removed_panel_user_uuid="panel-email",
)
)
assert len(messages) == 1
message, thread_id, reply_markup = messages[0]
assert "primary=42" in message
assert "removed=-100" in message
assert "paid@example.com" in message
assert thread_id is None
assert reply_markup.inline_keyboard[0][0].url == "tg://user?id=100200300"