rewrite v3
This commit is contained in:
+7
-6
@@ -48,7 +48,7 @@ class Settings(BaseSettings):
|
||||
bot_admin_ids_raw: str = Field(default="", validation_alias="BOT_ADMIN_IDS")
|
||||
bot_moderator_ids_raw: str = Field(default="", validation_alias="BOT_MODERATOR_IDS")
|
||||
telegram_proxy_url: str = Field(default="", validation_alias="TELEGRAM_PROXY_URL")
|
||||
bot_brand_name: str = Field(default="OREOL VPN", validation_alias="BOT_BRAND_NAME")
|
||||
bot_brand_name: str = Field(default="Example VPN", validation_alias="BOT_BRAND_NAME")
|
||||
bot_public_username: str = Field(default="", validation_alias="BOT_PUBLIC_USERNAME")
|
||||
bot_support_url: str = Field(default="", validation_alias="BOT_SUPPORT_URL")
|
||||
bot_terms_url: str = Field(default="", validation_alias="BOT_TERMS_URL")
|
||||
@@ -68,6 +68,7 @@ class Settings(BaseSettings):
|
||||
bot_start_image_path: str = Field(default="", validation_alias="BOT_START_IMAGE_PATH")
|
||||
|
||||
remnawave_base_url: str = Field(validation_alias="REMNAWAVE_BASE_URL")
|
||||
remnawave_api_version: int = Field(default=2, ge=2, le=3, validation_alias="REMNAWAVE_API_VERSION")
|
||||
remnawave_api_token: str = Field(default="", validation_alias="REMNAWAVE_API_TOKEN")
|
||||
remnawave_caddy_api_key: str = Field(default="", validation_alias="REMNAWAVE_CADDY_API_KEY")
|
||||
remnawave_timeout_seconds: float = Field(
|
||||
@@ -97,7 +98,7 @@ class Settings(BaseSettings):
|
||||
log_level: str = Field(default="INFO", validation_alias="LOG_LEVEL")
|
||||
|
||||
payment_plans_raw: str = Field(
|
||||
default="30:250,180:600,365:1000",
|
||||
default="30:100",
|
||||
validation_alias="PAYMENT_PLANS",
|
||||
)
|
||||
payment_product_plans_raw: str = Field(
|
||||
@@ -122,7 +123,7 @@ class Settings(BaseSettings):
|
||||
validation_alias="PAYMENT_REVIEW_THREAD_ID",
|
||||
)
|
||||
broadcast_results_link: str = Field(
|
||||
default="https://t.me/c/3646494169/67/68",
|
||||
default="",
|
||||
validation_alias="BROADCAST_RESULTS_LINK",
|
||||
)
|
||||
broadcast_results_chat_id_raw: str = Field(
|
||||
@@ -143,7 +144,7 @@ class Settings(BaseSettings):
|
||||
)
|
||||
payment_plan_code: str = Field(default="vpn_30d", validation_alias="PAYMENT_PLAN_CODE")
|
||||
payment_plan_title: str = Field(
|
||||
default="OREOL VPN на 30 дней",
|
||||
default="Example VPN на 30 дней",
|
||||
validation_alias="PAYMENT_PLAN_TITLE",
|
||||
)
|
||||
payment_plan_description: str = Field(
|
||||
@@ -184,7 +185,7 @@ class Settings(BaseSettings):
|
||||
validation_alias="PAYMENT_SUPPORT_TEXT",
|
||||
)
|
||||
payment_username_prefix: str = Field(
|
||||
default="Oreol",
|
||||
default="Example",
|
||||
validation_alias="PAYMENT_USERNAME_PREFIX",
|
||||
)
|
||||
payment_user_tag: str = Field(default="BOT", validation_alias="PAYMENT_USER_TAG")
|
||||
@@ -323,7 +324,7 @@ class Settings(BaseSettings):
|
||||
normalized = "".join(
|
||||
ch for ch in self.payment_username_prefix.strip() if ch.isalnum() or ch in "_-"
|
||||
)
|
||||
return normalized[:12] or "Oreol"
|
||||
return normalized[:12] or "Example"
|
||||
|
||||
@cached_property
|
||||
def payment_user_tag_normalized(self) -> str | None:
|
||||
|
||||
+19
@@ -2,6 +2,8 @@ import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
@@ -11,6 +13,8 @@ from aiogram.types import BotCommand
|
||||
|
||||
from app.bot.handlers.main import build_router
|
||||
from app.config import Settings
|
||||
from app.db.models import RemnawaveUser
|
||||
from app.services.remnawave_identity import load_legacy_user_ids
|
||||
from app.db.session import create_engine_and_session_factory, ensure_database_exists, init_db
|
||||
from app.services.bot_config_service import BotConfigService
|
||||
from app.services.notification_service import SubscriptionNotificationService
|
||||
@@ -55,12 +59,27 @@ async def main() -> None:
|
||||
if settings.create_tables_on_start:
|
||||
await init_db(engine)
|
||||
|
||||
legacy_user_ids = None
|
||||
if settings.remnawave_api_version == 3:
|
||||
identity_map = Path(__file__).with_name("legacy_user_ids.json")
|
||||
legacy_user_ids = load_legacy_user_ids(identity_map)
|
||||
remnawave_client = RemnawaveApiClient(
|
||||
base_url=settings.remnawave_base_url,
|
||||
api_token=settings.remnawave_api_token,
|
||||
caddy_api_key=settings.remnawave_caddy_api_key,
|
||||
timeout_seconds=settings.remnawave_timeout_seconds,
|
||||
api_version=settings.remnawave_api_version,
|
||||
legacy_user_ids=legacy_user_ids,
|
||||
)
|
||||
if settings.remnawave_api_version == 3:
|
||||
async with session_factory() as session:
|
||||
cached_identities = await session.execute(
|
||||
select(RemnawaveUser.rw_id, RemnawaveUser.rw_uuid, RemnawaveUser.short_uuid)
|
||||
)
|
||||
historical_count = await remnawave_client.validate_cached_identities(
|
||||
cached_identities.all()
|
||||
)
|
||||
logger.info("Validated Remnawave identities; preserved %s deleted historical cache rows", historical_count)
|
||||
sync_service = SyncService(
|
||||
settings=settings,
|
||||
session_factory=session_factory,
|
||||
|
||||
@@ -71,7 +71,7 @@ class BotConfigSnapshot:
|
||||
normalized = "".join(
|
||||
ch for ch in self.payment_username_prefix.strip() if ch.isalnum() or ch in "_-"
|
||||
)
|
||||
return normalized[:12] or "Oreol"
|
||||
return normalized[:12] or "Example"
|
||||
|
||||
@property
|
||||
def payment_user_tag_normalized(self) -> str | None:
|
||||
@@ -323,7 +323,7 @@ class BotConfigService(StaticBotConfigService):
|
||||
section="brand",
|
||||
prompt="Введите новое название бренда.",
|
||||
description="Используется в главной панели и пользовательских сообщениях.",
|
||||
placeholder="OREOL VPN",
|
||||
placeholder="Example VPN",
|
||||
),
|
||||
"bot_public_username": BotConfigFieldSpec(
|
||||
key="bot_public_username",
|
||||
@@ -331,7 +331,7 @@ class BotConfigService(StaticBotConfigService):
|
||||
section="brand",
|
||||
prompt="Введите username бота без @. Для очистки отправьте `-`.",
|
||||
description="Нужен для генерации реферальных ссылок.",
|
||||
placeholder="oreol_vpn_bot",
|
||||
placeholder="example_vpn_bot",
|
||||
),
|
||||
"bot_start_image_enabled": BotConfigFieldSpec(
|
||||
key="bot_start_image_enabled",
|
||||
@@ -450,7 +450,7 @@ class BotConfigService(StaticBotConfigService):
|
||||
section="links",
|
||||
prompt="Введите приватную ссылку на чат/топик, куда будут приходить ответы опросов. Для очистки отправьте `-`.",
|
||||
description="Формат: `https://t.me/c/<chat>/<topic>/<message>`.",
|
||||
placeholder="https://t.me/c/3646494169/67/68",
|
||||
placeholder="https://t.me/c/1234567890/67/68",
|
||||
),
|
||||
"payment_transfer_text": BotConfigFieldSpec(
|
||||
key="payment_transfer_text",
|
||||
@@ -458,7 +458,7 @@ class BotConfigService(StaticBotConfigService):
|
||||
section="texts",
|
||||
prompt="Введите текст с реквизитами или инструкцией перевода.",
|
||||
description="Показывается пользователю после выбора тарифа.",
|
||||
placeholder="Карта 0000 0000 0000 0000; банк OREOL; получатель OREOL VPN",
|
||||
placeholder="Карта 0000 0000 0000 0000; банк Пример; получатель Example VPN",
|
||||
),
|
||||
"payment_support_text": BotConfigFieldSpec(
|
||||
key="payment_support_text",
|
||||
@@ -514,7 +514,7 @@ class BotConfigService(StaticBotConfigService):
|
||||
section="provisioning",
|
||||
prompt="Введите префикс логина для новых пользователей Remnawave.",
|
||||
description="Итоговый логин строится как `<prefix>-<telegram_id>-<name>`.",
|
||||
placeholder="Oreol",
|
||||
placeholder="Example",
|
||||
),
|
||||
"payment_user_tag": BotConfigFieldSpec(
|
||||
key="payment_user_tag",
|
||||
|
||||
@@ -1285,7 +1285,7 @@ class PaymentService:
|
||||
base_name = username or first_name or "user"
|
||||
normalized_name = self._normalize_provision_name(base_name)
|
||||
|
||||
prefix = config.payment_username_prefix_normalized or "Oreol"
|
||||
prefix = config.payment_username_prefix_normalized or "Example"
|
||||
unique_suffix = order_uuid.replace("-", "")[:6]
|
||||
provision_username = f"{prefix}-{telegram_id}-{unique_suffix}-{normalized_name}"
|
||||
return provision_username[:36]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from uuid import UUID
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.services.remnawave_identity import deterministic_user_uuid, validate_cached_identities as validate_identity_cache
|
||||
from app.schemas.remnawave import HwidDevicesResponse, PaginatedUsers, RemnawaveUser, ResolvedUser, SubscriptionRequestHistory
|
||||
|
||||
|
||||
@@ -35,7 +37,20 @@ class RemnawaveApiClient:
|
||||
caddy_api_key: str = "",
|
||||
timeout_seconds: float = 20.0,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
api_version: int = 2,
|
||||
legacy_user_ids: dict[int, str] | None = None,
|
||||
) -> None:
|
||||
if api_version not in (2, 3):
|
||||
raise ValueError("Supported Remnawave API versions: 2, 3")
|
||||
self._api_version = api_version
|
||||
self._local_uuid_by_id = {
|
||||
int(user_id): str(UUID(local_uuid))
|
||||
for user_id, local_uuid in (legacy_user_ids or {}).items()
|
||||
}
|
||||
self._id_by_local_uuid = {value: key for key, value in self._local_uuid_by_id.items()}
|
||||
self._username_by_id: dict[int, str] = {}
|
||||
if len(self._id_by_local_uuid) != len(self._local_uuid_by_id):
|
||||
raise ValueError("Duplicate UUID in legacy identity map")
|
||||
self._managed_client = client is None
|
||||
self._client = client or httpx.AsyncClient(
|
||||
base_url=self.normalize_base_url(base_url),
|
||||
@@ -83,11 +98,24 @@ class RemnawaveApiClient:
|
||||
if self._managed_client:
|
||||
await self._client.aclose()
|
||||
|
||||
async def validate_cached_identities(self, rows: list[tuple[int, str] | tuple[int, str, str]]) -> int:
|
||||
if self._api_version != 3:
|
||||
raise ValueError("Remote identity validation requires Remnawave v3")
|
||||
users = await self._stream_users()
|
||||
return validate_identity_cache(
|
||||
self._local_uuid_by_id, rows,
|
||||
remote_identities=[(user.id, str(user.uuid), user.short_uuid) for user in users],
|
||||
)
|
||||
|
||||
async def get_users_by_telegram_id(self, telegram_id: int) -> list[RemnawaveUser]:
|
||||
if self._api_version == 3:
|
||||
return await self._stream_users(telegram_id=telegram_id)
|
||||
payload = await self._request_json("GET", f"users/by-telegram-id/{telegram_id}")
|
||||
return [RemnawaveUser.model_validate(item) for item in payload]
|
||||
|
||||
async def get_user_by_uuid(self, user_uuid: str) -> RemnawaveUser:
|
||||
if self._api_version == 3:
|
||||
user_uuid = str(await self._remote_id(user_uuid))
|
||||
payload = await self._request_json("GET", f"users/{user_uuid}")
|
||||
return RemnawaveUser.model_validate(payload)
|
||||
|
||||
@@ -100,10 +128,24 @@ class RemnawaveApiClient:
|
||||
return RemnawaveUser.model_validate(payload)
|
||||
|
||||
async def create_user(self, body: dict[str, Any]) -> RemnawaveUser:
|
||||
if self._api_version == 3 and "uuid" in body:
|
||||
raise ValueError("Remnawave v3 does not accept custom user UUIDs")
|
||||
payload = await self._request_json("POST", "users", json=body)
|
||||
return RemnawaveUser.model_validate(payload)
|
||||
|
||||
async def update_user(self, body: dict[str, Any]) -> RemnawaveUser:
|
||||
if self._api_version == 3 and "uuid" in body:
|
||||
body = dict(body)
|
||||
local_uuid = body.pop("uuid")
|
||||
user_id = await self._remote_id(str(local_uuid))
|
||||
if "id" in body and body["id"] != user_id:
|
||||
raise ValueError("Conflicting user identifiers")
|
||||
if "username" in body:
|
||||
if user_id not in self._username_by_id:
|
||||
await self.get_user_by_uuid(str(user_id))
|
||||
if body.pop("username") != self._username_by_id[user_id]:
|
||||
raise ValueError("Remnawave v3 cannot rename a user with PATCH /users")
|
||||
body["id"] = user_id
|
||||
payload = await self._request_json("PATCH", "users", json=body)
|
||||
return RemnawaveUser.model_validate(payload)
|
||||
|
||||
@@ -116,6 +158,8 @@ class RemnawaveApiClient:
|
||||
return PaginatedUsers.model_validate(payload)
|
||||
|
||||
async def get_user_subscription_history(self, user_uuid: str) -> SubscriptionRequestHistory:
|
||||
if self._api_version == 3:
|
||||
user_uuid = str(await self._remote_id(user_uuid))
|
||||
payload = await self._request_json(
|
||||
"GET",
|
||||
f"users/{user_uuid}/subscription-request-history",
|
||||
@@ -126,7 +170,8 @@ class RemnawaveApiClient:
|
||||
body: dict[str, Any]
|
||||
|
||||
if UUID_RE.match(identifier):
|
||||
body = {"uuid": identifier}
|
||||
body = ({"id": await self._remote_id(identifier)}
|
||||
if self._api_version == 3 else {"uuid": identifier})
|
||||
elif identifier.isdigit():
|
||||
body = {"id": int(identifier)}
|
||||
else:
|
||||
@@ -146,22 +191,28 @@ class RemnawaveApiClient:
|
||||
return ResolvedUser.model_validate(payload)
|
||||
|
||||
async def get_user_hwid_devices(self, user_uuid: str) -> HwidDevicesResponse:
|
||||
if self._api_version == 3:
|
||||
user_uuid = str(await self._remote_id(user_uuid))
|
||||
payload = await self._request_json("GET", f"hwid/devices/{user_uuid}")
|
||||
return HwidDevicesResponse.model_validate(payload)
|
||||
|
||||
async def delete_user_hwid_device(self, *, user_uuid: str, hwid: str) -> HwidDevicesResponse:
|
||||
user_field = ({"userId": await self._remote_id(user_uuid)}
|
||||
if self._api_version == 3 else {"userUuid": user_uuid})
|
||||
payload = await self._request_json(
|
||||
"POST",
|
||||
"hwid/devices/delete",
|
||||
json={"userUuid": user_uuid, "hwid": hwid},
|
||||
json={**user_field, "hwid": hwid},
|
||||
)
|
||||
return HwidDevicesResponse.model_validate(payload)
|
||||
|
||||
async def delete_all_user_hwid_devices(self, user_uuid: str) -> HwidDevicesResponse:
|
||||
user_field = ({"userId": await self._remote_id(user_uuid)}
|
||||
if self._api_version == 3 else {"userUuid": user_uuid})
|
||||
payload = await self._request_json(
|
||||
"POST",
|
||||
"hwid/devices/delete-all",
|
||||
json={"userUuid": user_uuid},
|
||||
json=user_field,
|
||||
)
|
||||
return HwidDevicesResponse.model_validate(payload)
|
||||
|
||||
@@ -173,28 +224,12 @@ class RemnawaveApiClient:
|
||||
params: dict[str, Any] | None = None,
|
||||
json: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
try:
|
||||
response = await self._client.request(
|
||||
method=method,
|
||||
url=path.lstrip("/"),
|
||||
params=params,
|
||||
json=json,
|
||||
)
|
||||
except httpx.TimeoutException as exc:
|
||||
raise RemnawaveApiError(
|
||||
status_code=0,
|
||||
message=f"Remnawave API timeout: {exc}",
|
||||
) from exc
|
||||
except httpx.NetworkError as exc:
|
||||
raise RemnawaveApiError(
|
||||
status_code=0,
|
||||
message=f"Remnawave API network error: {exc}",
|
||||
) from exc
|
||||
except httpx.HTTPError as exc:
|
||||
raise RemnawaveApiError(
|
||||
status_code=0,
|
||||
message=f"Remnawave API HTTP error: {exc}",
|
||||
) from exc
|
||||
response = await self._client.request(
|
||||
method=method,
|
||||
url=path.lstrip("/"),
|
||||
params=params,
|
||||
json=json,
|
||||
)
|
||||
|
||||
if response.is_error:
|
||||
raise self._build_error(response)
|
||||
@@ -204,7 +239,63 @@ class RemnawaveApiClient:
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
return self.unwrap_payload(payload)
|
||||
payload = self.unwrap_payload(payload)
|
||||
return self._normalize_v3(payload) if self._api_version == 3 else payload
|
||||
|
||||
def _local_uuid(self, user_id: int) -> str:
|
||||
"""Keep bot DB references stable; these UUIDs are never sent to API v3."""
|
||||
user_id = int(user_id)
|
||||
if user_id not in self._local_uuid_by_id:
|
||||
# Deterministic local identity for accounts created after migration.
|
||||
self._local_uuid_by_id[user_id] = deterministic_user_uuid(user_id)
|
||||
local_uuid = self._local_uuid_by_id[user_id]
|
||||
self._id_by_local_uuid[local_uuid] = user_id
|
||||
return local_uuid
|
||||
|
||||
def _normalize_v3(self, payload: Any) -> Any:
|
||||
if isinstance(payload, list):
|
||||
return [self._normalize_v3(item) for item in payload]
|
||||
if not isinstance(payload, dict):
|
||||
return payload
|
||||
result = {key: self._normalize_v3(value) for key, value in payload.items()}
|
||||
if all(key in result for key in ("id", "username", "shortUuid")):
|
||||
result["uuid"] = self._local_uuid(result["id"])
|
||||
self._username_by_id[int(result["id"])] = result["username"]
|
||||
if "userId" in result and result["userId"] is not None:
|
||||
result["userUuid"] = self._local_uuid(result["userId"])
|
||||
return result
|
||||
|
||||
async def _stream_users(self, *, telegram_id: int | None = None) -> list[RemnawaveUser]:
|
||||
params: dict[str, Any] = {"size": 250}
|
||||
if telegram_id is not None:
|
||||
params["telegramId"] = telegram_id
|
||||
users: list[RemnawaveUser] = []
|
||||
seen_cursors: set[str] = set()
|
||||
while True:
|
||||
payload = await self._request_json("GET", "users/stream", params=params)
|
||||
users.extend(RemnawaveUser.model_validate(item) for item in payload["users"])
|
||||
if not payload.get("hasMore"):
|
||||
return users
|
||||
cursor = payload.get("nextCursor")
|
||||
if cursor is None or str(cursor) in seen_cursors:
|
||||
raise RemnawaveApiError(status_code=502, message="Invalid users stream cursor")
|
||||
seen_cursors.add(str(cursor))
|
||||
params["cursor"] = cursor
|
||||
|
||||
async def _remote_id(self, identifier: str) -> int:
|
||||
if identifier.isdigit():
|
||||
return int(identifier)
|
||||
try:
|
||||
local_uuid = str(UUID(identifier))
|
||||
except ValueError as exc:
|
||||
raise RemnawaveApiError(status_code=404, message="Invalid user identifier") from exc
|
||||
if local_uuid not in self._id_by_local_uuid:
|
||||
# Rebuild deterministic aliases after a restart, including an order
|
||||
# saved before its new user reached the bot's local cache.
|
||||
await self._stream_users()
|
||||
if local_uuid not in self._id_by_local_uuid:
|
||||
raise RemnawaveApiError(status_code=404, message="User not found")
|
||||
return self._id_by_local_uuid[local_uuid]
|
||||
|
||||
def _build_error(self, response: httpx.Response) -> RemnawaveApiError:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Migration identity map validation, before starting Telegram polling."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid5
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
def deterministic_user_uuid(user_id: int) -> str:
|
||||
return str(uuid5(UUID("2a438583-9a10-4b90-831f-7db0432b5578"), str(user_id)))
|
||||
|
||||
|
||||
def load_legacy_user_ids(path: Path) -> dict[int, str]:
|
||||
entries = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(entries, list) or not entries:
|
||||
raise ValueError("Legacy Remnawave identity map must be a non-empty list")
|
||||
result: dict[int, str] = {}
|
||||
seen_uuids: set[str] = set()
|
||||
for entry in entries:
|
||||
user_id = entry["id"]
|
||||
if not isinstance(user_id, int) or isinstance(user_id, bool) or user_id <= 0:
|
||||
raise ValueError("Legacy Remnawave user ID must be a positive integer")
|
||||
local_uuid = str(UUID(entry["uuid"]))
|
||||
if user_id in result or local_uuid in seen_uuids:
|
||||
raise ValueError("Duplicate user identity in legacy Remnawave map")
|
||||
result[user_id] = local_uuid
|
||||
seen_uuids.add(local_uuid)
|
||||
return result
|
||||
|
||||
|
||||
def validate_cached_identities(
|
||||
legacy: dict[int, str],
|
||||
rows: Iterable[tuple[int, str] | tuple[int, str, str]],
|
||||
*,
|
||||
remote_identities: Iterable[tuple[int, str] | tuple[int, str, str]] | None = None,
|
||||
) -> int:
|
||||
"""Keep historical deleted cache rows only after a complete remote snapshot.
|
||||
|
||||
Never turn a deleted account's UUID into an alias for its old numeric ID.
|
||||
A reused ID or a UUID assigned to a different existing ID fails closed.
|
||||
"""
|
||||
snapshot = None if remote_identities is None else list(remote_identities)
|
||||
remote = None if snapshot is None else {
|
||||
item[0]: str(UUID(item[1])) for item in snapshot
|
||||
}
|
||||
remote_short_uuids = {} if snapshot is None else {
|
||||
item[0]: item[2] for item in snapshot if len(item) > 2
|
||||
}
|
||||
legacy_ids_by_uuid = {local_uuid: user_id for user_id, local_uuid in legacy.items()}
|
||||
remote_ids_by_uuid = {} if remote is None else {
|
||||
local_uuid: user_id for user_id, local_uuid in remote.items()
|
||||
}
|
||||
historical_deleted = 0
|
||||
for row in rows:
|
||||
user_id, value = row[:2]
|
||||
local_uuid = str(UUID(value))
|
||||
if len(row) > 2 and user_id in remote_short_uuids and row[2] != remote_short_uuids[user_id]:
|
||||
raise ValueError("Remote Remnawave short UUID conflicts with the bot cache")
|
||||
expected = legacy.get(user_id, deterministic_user_uuid(user_id))
|
||||
if legacy_ids_by_uuid.get(local_uuid, user_id) != user_id:
|
||||
raise ValueError("Legacy Remnawave UUID belongs to another cached ID")
|
||||
if remote_ids_by_uuid.get(local_uuid, user_id) != user_id:
|
||||
raise ValueError("Remote Remnawave UUID belongs to another cached ID")
|
||||
if local_uuid == expected:
|
||||
if remote is not None and user_id in remote and remote[user_id] != local_uuid:
|
||||
raise ValueError("Remote Remnawave identity conflicts with the bot cache")
|
||||
continue
|
||||
if user_id in legacy or remote is None or user_id in remote:
|
||||
raise ValueError("Legacy Remnawave map does not match the existing bot cache")
|
||||
historical_deleted += 1
|
||||
return historical_deleted
|
||||
Reference in New Issue
Block a user