fix: clean legacy emails from panel descriptions
This commit is contained in:
@@ -62,6 +62,29 @@ def _description_matches(current: Optional[str], desired: str) -> bool:
|
|||||||
return bool(_description_variants(current) & _description_variants(desired))
|
return bool(_description_variants(current) & _description_variants(desired))
|
||||||
|
|
||||||
|
|
||||||
|
def _description_contains_email(value: Optional[str], email: Optional[str]) -> bool:
|
||||||
|
normalized_email = _normalize_panel_email(email)
|
||||||
|
if not normalized_email:
|
||||||
|
return False
|
||||||
|
return normalized_email in _normalize_description(value).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _description_without_email(value: Optional[str], email: Optional[str]) -> str:
|
||||||
|
normalized_email = _normalize_panel_email(email)
|
||||||
|
if not normalized_email:
|
||||||
|
return (value or "").strip()
|
||||||
|
|
||||||
|
cleaned_lines = []
|
||||||
|
for raw_line in (value or "").splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.lower() == normalized_email:
|
||||||
|
continue
|
||||||
|
cleaned_lines.append(line)
|
||||||
|
return "\n".join(cleaned_lines).strip()
|
||||||
|
|
||||||
|
|
||||||
def _panel_identity_matches_user(
|
def _panel_identity_matches_user(
|
||||||
panel_user: dict[str, Any],
|
panel_user: dict[str, Any],
|
||||||
user: User,
|
user: User,
|
||||||
@@ -106,11 +129,27 @@ def _panel_identity_needs_full_fetch(panel_user: dict[str, Any], user: User) ->
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _panel_identity_needs_legacy_description_cleanup(
|
||||||
|
panel_user: dict[str, Any],
|
||||||
|
user: User,
|
||||||
|
desired_description: str,
|
||||||
|
) -> bool:
|
||||||
|
if not user.email:
|
||||||
|
return False
|
||||||
|
current_description = panel_user.get("description")
|
||||||
|
if _description_contains_email(current_description, user.email):
|
||||||
|
return False
|
||||||
|
if not desired_description:
|
||||||
|
return not _normalize_description(current_description)
|
||||||
|
return _description_matches(current_description, desired_description)
|
||||||
|
|
||||||
|
|
||||||
async def _panel_identity_view_for_comparison(
|
async def _panel_identity_view_for_comparison(
|
||||||
panel_service: PanelApiService,
|
panel_service: PanelApiService,
|
||||||
panel_uuid: str,
|
panel_uuid: str,
|
||||||
panel_user: dict[str, Any],
|
panel_user: dict[str, Any],
|
||||||
user: User,
|
user: User,
|
||||||
|
desired_description: str = "",
|
||||||
) -> tuple[dict[str, Any], bool]:
|
) -> tuple[dict[str, Any], bool]:
|
||||||
"""Return the most reliable panel user view available for identity comparison.
|
"""Return the most reliable panel user view available for identity comparison.
|
||||||
|
|
||||||
@@ -119,7 +158,15 @@ async def _panel_identity_view_for_comparison(
|
|||||||
repair PATCH.
|
repair PATCH.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not _panel_identity_needs_full_fetch(panel_user, user):
|
needs_full_fetch = _panel_identity_needs_full_fetch(panel_user, user)
|
||||||
|
if not needs_full_fetch and _panel_identity_needs_legacy_description_cleanup(
|
||||||
|
panel_user,
|
||||||
|
user,
|
||||||
|
desired_description,
|
||||||
|
):
|
||||||
|
needs_full_fetch = True
|
||||||
|
|
||||||
|
if not needs_full_fetch:
|
||||||
return panel_user, True
|
return panel_user, True
|
||||||
try:
|
try:
|
||||||
full_panel_user = await panel_service.get_user_by_uuid(panel_uuid)
|
full_panel_user = await panel_service.get_user_by_uuid(panel_uuid)
|
||||||
@@ -1023,13 +1070,25 @@ async def _perform_sync_impl(
|
|||||||
panel_uuid,
|
panel_uuid,
|
||||||
panel_user_dict,
|
panel_user_dict,
|
||||||
existing_user,
|
existing_user,
|
||||||
|
desired_description,
|
||||||
)
|
)
|
||||||
if desired_description and not _panel_identity_matches_user(
|
current_description = panel_user_for_identity.get("description")
|
||||||
|
description_has_email = _description_contains_email(
|
||||||
|
current_description,
|
||||||
|
existing_user.email,
|
||||||
|
)
|
||||||
|
identity_matches = _panel_identity_matches_user(
|
||||||
panel_user_for_identity,
|
panel_user_for_identity,
|
||||||
existing_user,
|
existing_user,
|
||||||
desired_description,
|
desired_description,
|
||||||
missing_identity_fields_match=missing_identity_fields_match,
|
missing_identity_fields_match=missing_identity_fields_match,
|
||||||
):
|
)
|
||||||
|
if description_has_email:
|
||||||
|
description_text = _description_without_email(
|
||||||
|
current_description,
|
||||||
|
existing_user.email,
|
||||||
|
)
|
||||||
|
if description_has_email or not identity_matches:
|
||||||
await panel_service.update_user_details_on_panel(
|
await panel_service.update_user_details_on_panel(
|
||||||
panel_uuid,
|
panel_uuid,
|
||||||
_panel_identity_update_payload(existing_user, description_text),
|
_panel_identity_update_payload(existing_user, description_text),
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ from unittest.mock import AsyncMock, patch
|
|||||||
from bot.handlers.admin.sync_admin import (
|
from bot.handlers.admin.sync_admin import (
|
||||||
_absorb_duplicate_panel_identity,
|
_absorb_duplicate_panel_identity,
|
||||||
_coerce_panel_telegram_id,
|
_coerce_panel_telegram_id,
|
||||||
|
_description_contains_email,
|
||||||
_description_matches,
|
_description_matches,
|
||||||
|
_description_without_email,
|
||||||
_panel_description_for_user,
|
_panel_description_for_user,
|
||||||
_panel_identity_matches_user,
|
_panel_identity_matches_user,
|
||||||
_panel_identity_needs_full_fetch,
|
_panel_identity_needs_full_fetch,
|
||||||
|
_panel_identity_needs_legacy_description_cleanup,
|
||||||
_panel_identity_payload_with_expiry,
|
_panel_identity_payload_with_expiry,
|
||||||
_panel_identity_view_for_comparison,
|
_panel_identity_view_for_comparison,
|
||||||
_should_update_lifetime_used_traffic,
|
_should_update_lifetime_used_traffic,
|
||||||
@@ -52,6 +55,25 @@ def test_panel_description_for_user_excludes_email():
|
|||||||
assert _panel_description_for_user(user) == "alice\nAlice\nSmith"
|
assert _panel_description_for_user(user) == "alice\nAlice\nSmith"
|
||||||
|
|
||||||
|
|
||||||
|
def test_description_contains_email_detects_legacy_panel_description():
|
||||||
|
assert _description_contains_email(
|
||||||
|
"Linked@Example.com\nalice\nAlice",
|
||||||
|
"linked@example.com",
|
||||||
|
)
|
||||||
|
assert not _description_contains_email("alice\nAlice", "linked@example.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_description_without_email_preserves_panel_text():
|
||||||
|
assert (
|
||||||
|
_description_without_email(
|
||||||
|
"linked@example.com\nalice\nAlice",
|
||||||
|
"linked@example.com",
|
||||||
|
)
|
||||||
|
== "alice\nAlice"
|
||||||
|
)
|
||||||
|
assert _description_without_email("linked@example.com", "linked@example.com") == ""
|
||||||
|
|
||||||
|
|
||||||
def test_panel_identity_match_accepts_list_description_without_email():
|
def test_panel_identity_match_accepts_list_description_without_email():
|
||||||
user = SimpleNamespace(
|
user = SimpleNamespace(
|
||||||
email="linked@example.com",
|
email="linked@example.com",
|
||||||
@@ -90,20 +112,61 @@ def test_panel_identity_payload_with_expiry_keeps_email_out_of_description():
|
|||||||
assert payload["telegramId"] == 42
|
assert payload["telegramId"] == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_panel_identity_detects_legacy_full_description_cleanup_need():
|
||||||
|
user = SimpleNamespace(
|
||||||
|
email="linked@example.com",
|
||||||
|
telegram_id=42,
|
||||||
|
username="alice",
|
||||||
|
first_name="Alice",
|
||||||
|
last_name=None,
|
||||||
|
)
|
||||||
|
panel_user = {
|
||||||
|
"description": "alice\nAlice",
|
||||||
|
"email": "linked@example.com",
|
||||||
|
"telegramId": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert _panel_identity_needs_legacy_description_cleanup(
|
||||||
|
panel_user,
|
||||||
|
user,
|
||||||
|
_panel_description_for_user(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_panel_identity_detects_email_only_legacy_cleanup_need():
|
||||||
|
user = SimpleNamespace(
|
||||||
|
email="linked@example.com",
|
||||||
|
telegram_id=None,
|
||||||
|
username=None,
|
||||||
|
first_name=None,
|
||||||
|
last_name=None,
|
||||||
|
)
|
||||||
|
panel_user = {
|
||||||
|
"description": "",
|
||||||
|
"email": "linked@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert _panel_identity_needs_legacy_description_cleanup(
|
||||||
|
panel_user,
|
||||||
|
user,
|
||||||
|
_panel_description_for_user(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_panel_identity_match_treats_missing_list_email_as_unknown():
|
def test_panel_identity_match_treats_missing_list_email_as_unknown():
|
||||||
user = SimpleNamespace(
|
user = SimpleNamespace(
|
||||||
email="linked@example.com",
|
email="linked@example.com",
|
||||||
telegram_id=42,
|
telegram_id=42,
|
||||||
)
|
)
|
||||||
panel_user = {
|
panel_user = {
|
||||||
"description": "linked@example.com\nalice",
|
"description": "alice",
|
||||||
"telegramId": 42,
|
"telegramId": 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert _panel_identity_matches_user(
|
assert _panel_identity_matches_user(
|
||||||
panel_user,
|
panel_user,
|
||||||
user,
|
user,
|
||||||
"linked@example.com\nalice",
|
"alice",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,14 +176,14 @@ def test_panel_identity_match_treats_missing_full_email_as_mismatch():
|
|||||||
telegram_id=42,
|
telegram_id=42,
|
||||||
)
|
)
|
||||||
panel_user = {
|
panel_user = {
|
||||||
"description": "linked@example.com\nalice",
|
"description": "alice",
|
||||||
"telegramId": 42,
|
"telegramId": 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert not _panel_identity_matches_user(
|
assert not _panel_identity_matches_user(
|
||||||
panel_user,
|
panel_user,
|
||||||
user,
|
user,
|
||||||
"linked@example.com\nalice",
|
"alice",
|
||||||
missing_identity_fields_match=False,
|
missing_identity_fields_match=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -131,7 +194,7 @@ def test_panel_identity_match_rejects_different_returned_email():
|
|||||||
telegram_id=42,
|
telegram_id=42,
|
||||||
)
|
)
|
||||||
panel_user = {
|
panel_user = {
|
||||||
"description": "linked@example.com\nalice",
|
"description": "alice",
|
||||||
"email": "other@example.com",
|
"email": "other@example.com",
|
||||||
"telegramId": 42,
|
"telegramId": 42,
|
||||||
}
|
}
|
||||||
@@ -139,7 +202,7 @@ def test_panel_identity_match_rejects_different_returned_email():
|
|||||||
assert not _panel_identity_matches_user(
|
assert not _panel_identity_matches_user(
|
||||||
panel_user,
|
panel_user,
|
||||||
user,
|
user,
|
||||||
"linked@example.com\nalice",
|
"alice",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +255,51 @@ def test_panel_identity_view_fetches_full_user_when_list_email_missing():
|
|||||||
panel_service.get_user_by_uuid.assert_awaited_once_with("panel-1")
|
panel_service.get_user_by_uuid.assert_awaited_once_with("panel-1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_panel_identity_view_fetches_full_user_to_clean_legacy_description_email():
|
||||||
|
panel_service = SimpleNamespace(
|
||||||
|
get_user_by_uuid=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"uuid": "panel-1",
|
||||||
|
"description": "linked@example.com\nalice\nAlice",
|
||||||
|
"email": "linked@example.com",
|
||||||
|
"telegramId": 42,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user = SimpleNamespace(
|
||||||
|
email="linked@example.com",
|
||||||
|
telegram_id=42,
|
||||||
|
username="alice",
|
||||||
|
first_name="Alice",
|
||||||
|
last_name=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
panel_user, missing_fields_match = asyncio.run(
|
||||||
|
_panel_identity_view_for_comparison(
|
||||||
|
panel_service,
|
||||||
|
"panel-1",
|
||||||
|
{
|
||||||
|
"uuid": "panel-1",
|
||||||
|
"description": "alice\nAlice",
|
||||||
|
"email": "linked@example.com",
|
||||||
|
"telegramId": 42,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
_panel_description_for_user(user),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert panel_user["description"] == "linked@example.com\nalice\nAlice"
|
||||||
|
assert not missing_fields_match
|
||||||
|
assert not _panel_identity_matches_user(
|
||||||
|
panel_user,
|
||||||
|
user,
|
||||||
|
_panel_description_for_user(user),
|
||||||
|
missing_identity_fields_match=missing_fields_match,
|
||||||
|
)
|
||||||
|
panel_service.get_user_by_uuid.assert_awaited_once_with("panel-1")
|
||||||
|
|
||||||
|
|
||||||
def test_subscription_update_delta_skips_unchanged_fields():
|
def test_subscription_update_delta_skips_unchanged_fields():
|
||||||
end_date = datetime(2026, 5, 20, 12, 0, tzinfo=timezone.utc)
|
end_date = datetime(2026, 5, 20, 12, 0, tzinfo=timezone.utc)
|
||||||
subscription = Subscription(
|
subscription = Subscription(
|
||||||
|
|||||||
Reference in New Issue
Block a user