fix: repair linked panel email during sync

This commit is contained in:
3252a8
2026-05-23 22:27:51 +03:00
parent 60d8c297f9
commit 31eb5c06ad
4 changed files with 291 additions and 9 deletions
+9 -1
View File
@@ -1058,11 +1058,19 @@ async def _sync_panel_identity_for_user(
payload["status"] = "ACTIVE"
try:
await subscription_service.panel_service.update_user_details_on_panel(
updated_panel_user = await subscription_service.panel_service.update_user_details_on_panel(
user.panel_user_uuid,
payload,
log_response=False,
)
if not updated_panel_user or (
isinstance(updated_panel_user, dict) and updated_panel_user.get("error")
):
logger.warning(
"Panel identity update returned no success payload for user %s",
user.user_id,
)
return False
return True
except Exception as exc:
logger.warning(
+134 -8
View File
@@ -66,6 +66,8 @@ def _panel_identity_matches_user(
panel_user: dict[str, Any],
user: User,
desired_description: str,
*,
missing_identity_fields_match: bool = True,
) -> bool:
if desired_description and not _description_matches(
panel_user.get("description"),
@@ -73,17 +75,65 @@ def _panel_identity_matches_user(
):
return False
if user.email and _normalize_panel_email(panel_user.get("email")) != user.email.strip().lower():
return False
if user.email:
if "email" not in panel_user:
return missing_identity_fields_match
panel_email = _normalize_panel_email(panel_user.get("email"))
if panel_email != user.email.strip().lower():
return False
if user.telegram_id and _coerce_panel_telegram_id(panel_user.get("telegramId")) != int(
user.telegram_id
):
return False
if user.telegram_id:
if "telegramId" not in panel_user:
return missing_identity_fields_match
panel_telegram_id = _coerce_panel_telegram_id(panel_user.get("telegramId"))
if panel_telegram_id != int(user.telegram_id):
return False
return True
def _panel_identity_needs_full_fetch(panel_user: dict[str, Any], user: User) -> bool:
if user.email:
if "email" not in panel_user:
return True
if not _normalize_panel_email(panel_user.get("email")):
return True
if user.telegram_id:
if "telegramId" not in panel_user:
return True
if _coerce_panel_telegram_id(panel_user.get("telegramId")) is None:
return True
return False
async def _panel_identity_view_for_comparison(
panel_service: PanelApiService,
panel_uuid: str,
panel_user: dict[str, Any],
user: User,
) -> tuple[dict[str, Any], bool]:
"""Return the most reliable panel user view available for identity comparison.
Remnawave list responses may omit identity fields. When that happens, fetch
the concrete user by UUID before deciding whether the panel really needs a
repair PATCH.
"""
if not _panel_identity_needs_full_fetch(panel_user, user):
return panel_user, True
try:
full_panel_user = await panel_service.get_user_by_uuid(panel_uuid)
except Exception:
logging.exception(
"Sync: failed to fetch full panel user %s for identity comparison",
panel_uuid,
)
return panel_user, True
if not full_panel_user:
return panel_user, True
return full_panel_user, False
def _panel_identity_update_payload(user: User, description_text: str) -> dict[str, Any]:
payload: dict[str, Any] = {"description": description_text}
if user.email:
@@ -802,6 +852,72 @@ async def _perform_sync_impl(
and str(linked_uuid)
in panel_uuids_by_telegram_id.get(telegram_id_from_panel, set())
)
linked_uuid_present_on_panel = bool(
linked_uuid and str(linked_uuid) in panel_users_by_uuid
)
panel_uuid_owner = users_by_panel_uuid.get(panel_uuid)
if (
panel_uuid_owner
and panel_uuid_owner.user_id != existing_user.user_id
and not linked_uuid_still_present
):
if linked_uuid_present_on_panel:
msg = (
f"Panel UUID {panel_uuid} for user {actual_user_id} is already "
f"linked to local user {panel_uuid_owner.user_id}, while current "
f"local panel UUID {linked_uuid} still exists on panel."
)
sync_errors.append(msg)
logging.warning("Sync: %s", msg)
continue
previous_panel_uuid = existing_user.panel_user_uuid
previous_owner_user_id = panel_uuid_owner.user_id
previous_owner_email = panel_uuid_owner.email
previous_owner_telegram_id = panel_uuid_owner.telegram_id
(
existing_user,
can_merge_panel_uuid_owner,
) = await _merge_local_duplicate_panel_user_if_needed(
session,
existing_user=existing_user,
duplicate_panel_uuid=panel_uuid,
)
if not can_merge_panel_uuid_owner:
logging.warning(
"Sync: panel UUID %s is already linked to local user %s; "
"skipping reassignment to user %s because local merge failed.",
panel_uuid,
previous_owner_user_id,
actual_user_id,
)
continue
existing_user.panel_user_uuid = panel_uuid
actual_user_id = existing_user.user_id
user_was_updated = True
users_uuid_updated += 1
if previous_panel_uuid:
users_by_panel_uuid.pop(str(previous_panel_uuid), None)
users_by_panel_uuid[panel_uuid] = existing_user
users_by_user_id.pop(int(previous_owner_user_id), None)
users_by_user_id[int(existing_user.user_id)] = existing_user
if previous_owner_telegram_id is not None:
users_by_telegram_id.pop(int(previous_owner_telegram_id), None)
if existing_user.telegram_id is not None:
users_by_telegram_id[int(existing_user.telegram_id)] = existing_user
if previous_owner_email:
users_by_email.pop(previous_owner_email.strip().lower(), None)
if existing_user.email:
users_by_email[existing_user.email.strip().lower()] = existing_user
logging.info(
"Sync: merged local user %s owning panel UUID %s into user %s "
"and reassigned stale local panel UUID %s.",
previous_owner_user_id,
panel_uuid,
actual_user_id,
previous_panel_uuid,
)
if linked_uuid_still_present:
is_duplicate_panel_identity = True
(
@@ -859,7 +975,7 @@ async def _perform_sync_impl(
panel_uuid,
)
continue
else:
elif existing_user.panel_user_uuid != panel_uuid:
existing_user.panel_user_uuid = panel_uuid
user_was_updated = True
users_uuid_updated += 1
@@ -911,10 +1027,20 @@ async def _perform_sync_impl(
)
# Update description only when it differs from the current one on panel
desired_description = description_text.strip()
if desired_description and not _panel_identity_matches_user(
(
panel_user_for_identity,
missing_identity_fields_match,
) = await _panel_identity_view_for_comparison(
panel_service,
panel_uuid,
panel_user_dict,
existing_user,
)
if desired_description and not _panel_identity_matches_user(
panel_user_for_identity,
existing_user,
desired_description,
missing_identity_fields_match=missing_identity_fields_match,
):
await panel_service.update_user_details_on_panel(
panel_uuid,