fix: log account merge notifications
This commit is contained in:
@@ -2,7 +2,11 @@
|
|||||||
from ._runtime import * # noqa: F403,F405
|
from ._runtime import * # noqa: F403,F405
|
||||||
|
|
||||||
from bot.app.web.webapp.cache_helpers import webapp_cached_user_payload
|
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
|
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")
|
return _json_error(500, "link_failed", "Link failed")
|
||||||
|
|
||||||
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
|
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:
|
if should_notify_email_linked:
|
||||||
try:
|
try:
|
||||||
from bot.services.notification_service import NotificationService
|
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")
|
return _json_error(500, "link_failed", "Link failed")
|
||||||
|
|
||||||
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
|
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:
|
if should_notify_telegram_linked and final_telegram_id:
|
||||||
try:
|
try:
|
||||||
from bot.services.notification_service import NotificationService
|
from bot.services.notification_service import NotificationService
|
||||||
|
|||||||
@@ -426,6 +426,15 @@ async def telegram_oauth_callback_route(request: web.Request) -> web.Response:
|
|||||||
final_panel_uuid=link_final_panel_uuid,
|
final_panel_uuid=link_final_panel_uuid,
|
||||||
expire_at=merge_end_date,
|
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))
|
token = create_webapp_session_token(settings, int(final_user_id))
|
||||||
response = web.HTTPFound(_telegram_oauth_redirect_url(redirect_path, status="success"))
|
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(
|
def _apply_telegram_profile_to_user(
|
||||||
user: User,
|
user: User,
|
||||||
telegram_user: Dict[str, Any],
|
telegram_user: Dict[str, Any],
|
||||||
|
|||||||
@@ -690,6 +690,51 @@ class NotificationService:
|
|||||||
profile_keyboard = self._build_profile_keyboard(_, telegram_id)
|
profile_keyboard = self._build_profile_keyboard(_, telegram_id)
|
||||||
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
|
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:
|
def _format_traffic_gb_admin(self, traffic_gb: float) -> str:
|
||||||
value = float(traffic_gb)
|
value = float(traffic_gb)
|
||||||
if value.is_integer():
|
if value.is_integer():
|
||||||
|
|||||||
@@ -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_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_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_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": "{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_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}",
|
"log_payment_traffic_purchase_line": "<b>{gb} GB</b> · {kind}",
|
||||||
|
|||||||
@@ -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_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_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_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": "{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_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}",
|
"log_payment_traffic_purchase_line": "<b>{gb} ГБ</b> · {kind}",
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"last_name": "",
|
"last_name": "",
|
||||||
"language_code": "ru",
|
"language_code": "ru",
|
||||||
}
|
}
|
||||||
|
notification_service = SimpleNamespace(
|
||||||
|
notify_account_telegram_linked=AsyncMock(),
|
||||||
|
notify_account_merged=AsyncMock(),
|
||||||
|
)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch.object(account_routes, "_require_user_id", return_value=-100),
|
patch.object(account_routes, "_require_user_id", return_value=-100),
|
||||||
@@ -299,7 +303,7 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"bot.services.notification_service.NotificationService",
|
"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)
|
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_uuid, "panel-telegram")
|
||||||
self.assertEqual(update_payload["email"], "linked@example.com")
|
self.assertEqual(update_payload["email"], "linked@example.com")
|
||||||
self.assertEqual(update_payload["telegramId"], 42)
|
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)
|
self.assertIn("rw_webapp_session", response.cookies)
|
||||||
|
|||||||
@@ -364,3 +364,51 @@ def test_support_user_reply_can_send_email_without_telegram_channels():
|
|||||||
|
|
||||||
assert channels == []
|
assert channels == []
|
||||||
assert emails[0]["ticket_id"] == 7
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user