feat: email login, smtp codes
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
import smtplib
|
||||
import ssl
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.models import EmailVerificationCode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SmtpAttempt:
|
||||
port: int
|
||||
use_ssl: bool
|
||||
starttls: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailCodeRequestResult:
|
||||
ok: bool
|
||||
error: Optional[str] = None
|
||||
retry_after: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailCodeVerifyResult:
|
||||
ok: bool
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def normalize_email(value: str) -> str:
|
||||
return (value or "").strip().lower()
|
||||
|
||||
|
||||
def is_valid_email(value: str) -> bool:
|
||||
email = normalize_email(value)
|
||||
return bool(email and len(email) <= 254 and EMAIL_RE.match(email))
|
||||
|
||||
|
||||
class EmailAuthService:
|
||||
def __init__(self, settings: Settings):
|
||||
self.settings = settings
|
||||
|
||||
def _smtp_attempts(self) -> list[SmtpAttempt]:
|
||||
attempts: list[SmtpAttempt] = []
|
||||
primary_port = int(self.settings.SMTP_PORT)
|
||||
|
||||
for port in self.settings.smtp_ports_to_try:
|
||||
if port == primary_port:
|
||||
use_ssl = bool(self.settings.SMTP_USE_SSL or port == 465)
|
||||
starttls = bool(self.settings.SMTP_STARTTLS and not use_ssl)
|
||||
else:
|
||||
use_ssl = port == 465
|
||||
starttls = bool(self.settings.SMTP_STARTTLS and not use_ssl)
|
||||
attempts.append(SmtpAttempt(port=port, use_ssl=use_ssl, starttls=starttls))
|
||||
|
||||
return attempts or [
|
||||
SmtpAttempt(
|
||||
port=primary_port,
|
||||
use_ssl=bool(self.settings.SMTP_USE_SSL or primary_port == 465),
|
||||
starttls=bool(
|
||||
self.settings.SMTP_STARTTLS
|
||||
and not self.settings.SMTP_USE_SSL
|
||||
and primary_port != 465
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
def _hash_code(self, email: str, purpose: str, code: str) -> str:
|
||||
secret = hmac.new(
|
||||
self.settings.BOT_TOKEN.encode("utf-8"),
|
||||
b"remnawave-tg-shop-email-code",
|
||||
hashlib.sha256,
|
||||
).digest()
|
||||
payload = f"{purpose}:{email}:{code}".encode("utf-8")
|
||||
return hmac.new(secret, payload, hashlib.sha256).hexdigest()
|
||||
|
||||
async def request_code(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
email: str,
|
||||
purpose: str,
|
||||
language_code: str,
|
||||
target_user_id: Optional[int] = None,
|
||||
) -> EmailCodeRequestResult:
|
||||
normalized_email = normalize_email(email)
|
||||
if not self.settings.email_auth_configured:
|
||||
return EmailCodeRequestResult(ok=False, error="email_auth_not_configured")
|
||||
if not is_valid_email(normalized_email):
|
||||
return EmailCodeRequestResult(ok=False, error="invalid_email")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
latest_code = await self._get_latest_code(
|
||||
session,
|
||||
email=normalized_email,
|
||||
purpose=purpose,
|
||||
target_user_id=target_user_id,
|
||||
)
|
||||
if latest_code and latest_code.created_at:
|
||||
created_at = latest_code.created_at
|
||||
if created_at.tzinfo is None:
|
||||
created_at = created_at.replace(tzinfo=timezone.utc)
|
||||
resend_after = max(1, int(self.settings.EMAIL_CODE_RESEND_SECONDS))
|
||||
elapsed = int((now - created_at).total_seconds())
|
||||
if elapsed < resend_after and latest_code.consumed_at is None:
|
||||
return EmailCodeRequestResult(
|
||||
ok=False,
|
||||
error="rate_limited",
|
||||
retry_after=resend_after - elapsed,
|
||||
)
|
||||
|
||||
code = f"{secrets.randbelow(1_000_000):06d}"
|
||||
code_model = EmailVerificationCode(
|
||||
email=normalized_email,
|
||||
code_hash=self._hash_code(normalized_email, purpose, code),
|
||||
purpose=purpose,
|
||||
target_user_id=target_user_id,
|
||||
expires_at=now + timedelta(seconds=max(60, int(self.settings.EMAIL_CODE_TTL_SECONDS))),
|
||||
)
|
||||
session.add(code_model)
|
||||
await session.flush()
|
||||
|
||||
await self._send_code_email(
|
||||
email=normalized_email,
|
||||
code=code,
|
||||
language_code=language_code,
|
||||
)
|
||||
return EmailCodeRequestResult(ok=True)
|
||||
|
||||
async def verify_code(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
email: str,
|
||||
purpose: str,
|
||||
code: str,
|
||||
target_user_id: Optional[int] = None,
|
||||
) -> EmailCodeVerifyResult:
|
||||
normalized_email = normalize_email(email)
|
||||
normalized_code = re.sub(r"\D", "", code or "")
|
||||
if not is_valid_email(normalized_email) or len(normalized_code) != 6:
|
||||
return EmailCodeVerifyResult(ok=False, error="invalid_code")
|
||||
|
||||
latest_code = await self._get_latest_code(
|
||||
session,
|
||||
email=normalized_email,
|
||||
purpose=purpose,
|
||||
target_user_id=target_user_id,
|
||||
)
|
||||
if not latest_code or latest_code.consumed_at is not None:
|
||||
return EmailCodeVerifyResult(ok=False, error="invalid_code")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
expires_at = latest_code.expires_at
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
if expires_at < now:
|
||||
return EmailCodeVerifyResult(ok=False, error="expired_code")
|
||||
|
||||
max_attempts = max(1, int(self.settings.EMAIL_CODE_MAX_ATTEMPTS))
|
||||
if int(latest_code.attempts or 0) >= max_attempts:
|
||||
return EmailCodeVerifyResult(ok=False, error="too_many_attempts")
|
||||
|
||||
expected_hash = self._hash_code(normalized_email, purpose, normalized_code)
|
||||
if not hmac.compare_digest(expected_hash, latest_code.code_hash):
|
||||
latest_code.attempts = int(latest_code.attempts or 0) + 1
|
||||
await session.flush()
|
||||
return EmailCodeVerifyResult(ok=False, error="invalid_code")
|
||||
|
||||
latest_code.consumed_at = now
|
||||
await session.flush()
|
||||
return EmailCodeVerifyResult(ok=True)
|
||||
|
||||
async def _get_latest_code(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
email: str,
|
||||
purpose: str,
|
||||
target_user_id: Optional[int],
|
||||
) -> Optional[EmailVerificationCode]:
|
||||
stmt = (
|
||||
select(EmailVerificationCode)
|
||||
.where(
|
||||
EmailVerificationCode.email == email,
|
||||
EmailVerificationCode.purpose == purpose,
|
||||
EmailVerificationCode.target_user_id == target_user_id,
|
||||
)
|
||||
.order_by(EmailVerificationCode.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _send_code_email(
|
||||
self,
|
||||
*,
|
||||
email: str,
|
||||
code: str,
|
||||
language_code: str,
|
||||
) -> None:
|
||||
await asyncio.to_thread(
|
||||
self._send_code_email_sync,
|
||||
email=email,
|
||||
code=code,
|
||||
language_code=language_code,
|
||||
)
|
||||
|
||||
def _send_code_email_sync(
|
||||
self,
|
||||
*,
|
||||
email: str,
|
||||
code: str,
|
||||
language_code: str,
|
||||
) -> None:
|
||||
lang = (language_code or self.settings.DEFAULT_LANGUAGE or "ru").split("-")[0]
|
||||
if lang == "en":
|
||||
subject = "Your login code"
|
||||
body = (
|
||||
f"Your verification code: {code}\n\n"
|
||||
f"The code expires in {max(1, int(self.settings.EMAIL_CODE_TTL_SECONDS) // 60)} minutes."
|
||||
)
|
||||
else:
|
||||
subject = "Код подтверждения"
|
||||
body = (
|
||||
f"Ваш код подтверждения: {code}\n\n"
|
||||
f"Код действует {max(1, int(self.settings.EMAIL_CODE_TTL_SECONDS) // 60)} мин."
|
||||
)
|
||||
|
||||
message = EmailMessage()
|
||||
message["Subject"] = subject
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
self.settings.SMTP_FROM_NAME or self.settings.WEBAPP_TITLE,
|
||||
self.settings.SMTP_FROM_EMAIL or "",
|
||||
)
|
||||
)
|
||||
message["To"] = email
|
||||
message.set_content(body)
|
||||
|
||||
context = ssl.create_default_context()
|
||||
smtp_host = self.settings.SMTP_HOST
|
||||
timeout = max(5, int(self.settings.SMTP_TIMEOUT_SECONDS))
|
||||
attempts = self._smtp_attempts()
|
||||
last_error: Optional[BaseException] = None
|
||||
|
||||
for attempt_number, attempt in enumerate(attempts, start=1):
|
||||
try:
|
||||
self._send_message_via_smtp(
|
||||
message=message,
|
||||
smtp_host=smtp_host,
|
||||
smtp_port=attempt.port,
|
||||
timeout=timeout,
|
||||
context=context,
|
||||
use_ssl=attempt.use_ssl,
|
||||
starttls=attempt.starttls,
|
||||
)
|
||||
logger.info(
|
||||
"Email verification code sent to %s via %s:%s",
|
||||
email,
|
||||
smtp_host,
|
||||
attempt.port,
|
||||
)
|
||||
return
|
||||
except (OSError, smtplib.SMTPException, TimeoutError) as exc:
|
||||
last_error = exc
|
||||
log_level = logging.WARNING if attempt_number < len(attempts) else logging.ERROR
|
||||
logger.log(
|
||||
log_level,
|
||||
"SMTP send attempt %s/%s failed via %s:%s (ssl=%s, starttls=%s): %s",
|
||||
attempt_number,
|
||||
len(attempts),
|
||||
smtp_host,
|
||||
attempt.port,
|
||||
attempt.use_ssl,
|
||||
attempt.starttls,
|
||||
exc,
|
||||
)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
def _send_message_via_smtp(
|
||||
self,
|
||||
*,
|
||||
message: EmailMessage,
|
||||
smtp_host: str,
|
||||
smtp_port: int,
|
||||
timeout: int,
|
||||
context: ssl.SSLContext,
|
||||
use_ssl: bool,
|
||||
starttls: bool,
|
||||
) -> None:
|
||||
if use_ssl:
|
||||
with smtplib.SMTP_SSL(
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
context=context,
|
||||
timeout=timeout,
|
||||
) as smtp:
|
||||
smtp.ehlo()
|
||||
smtp.login(self.settings.SMTP_USERNAME, self.settings.SMTP_PASSWORD)
|
||||
smtp.send_message(message)
|
||||
return
|
||||
|
||||
with smtplib.SMTP(smtp_host, smtp_port, timeout=timeout) as smtp:
|
||||
smtp.ehlo()
|
||||
if starttls:
|
||||
smtp.starttls(context=context)
|
||||
smtp.ehlo()
|
||||
smtp.login(self.settings.SMTP_USERNAME, self.settings.SMTP_PASSWORD)
|
||||
smtp.send_message(message)
|
||||
@@ -54,7 +54,10 @@ class PanelWebhookService:
|
||||
return
|
||||
|
||||
async with self.async_session_factory() as session:
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
db_user = await user_dal.get_user_by_telegram_id(session, user_id)
|
||||
if not db_user:
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
internal_user_id = db_user.user_id if db_user else user_id
|
||||
lang = db_user.language_code if db_user and db_user.language_code else self.settings.DEFAULT_LANGUAGE
|
||||
first_name = db_user.first_name or f"User {user_id}" if db_user else f"User {user_id}"
|
||||
|
||||
@@ -69,7 +72,7 @@ class PanelWebhookService:
|
||||
if subscription_service:
|
||||
async with self.async_session_factory() as session:
|
||||
from db.dal import subscription_dal
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, internal_user_id)
|
||||
if sub and sub.auto_renew_enabled and sub.provider == 'yookassa':
|
||||
try:
|
||||
ok = await subscription_service.charge_subscription_renewal(session, sub)
|
||||
@@ -89,7 +92,7 @@ class PanelWebhookService:
|
||||
if days_left == 2:
|
||||
async with self.async_session_factory() as session:
|
||||
from db.dal import subscription_dal
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, internal_user_id)
|
||||
logging.info(
|
||||
"48h webhook check: user_id=%s sub_found=%s auto_renew=%s provider=%s",
|
||||
user_id,
|
||||
|
||||
@@ -96,6 +96,42 @@ class SubscriptionService:
|
||||
f"Failed to notify admin {admin_id} about panel user creation failure: {e}"
|
||||
)
|
||||
|
||||
def _telegram_id_for_panel(self, db_user: User) -> Optional[int]:
|
||||
if db_user.telegram_id:
|
||||
return int(db_user.telegram_id)
|
||||
if db_user.user_id and int(db_user.user_id) > 0:
|
||||
return int(db_user.user_id)
|
||||
return None
|
||||
|
||||
async def _panel_username_for_user(
|
||||
self, session: AsyncSession, db_user: User
|
||||
) -> str:
|
||||
telegram_id = self._telegram_id_for_panel(db_user)
|
||||
if telegram_id and int(db_user.user_id) == telegram_id:
|
||||
return f"tg_{telegram_id}"
|
||||
referral_code = await user_dal.ensure_referral_code(session, db_user)
|
||||
return f"em_{referral_code}"
|
||||
|
||||
def _panel_description_for_user(self, db_user: User) -> str:
|
||||
lines = [
|
||||
db_user.email or "",
|
||||
db_user.username or "",
|
||||
db_user.first_name or "",
|
||||
db_user.last_name or "",
|
||||
]
|
||||
return "\n".join(line for line in lines if line).strip()
|
||||
|
||||
def _panel_identity_payload_for_user(self, db_user: User) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"description": self._panel_description_for_user(db_user),
|
||||
}
|
||||
telegram_id = self._telegram_id_for_panel(db_user)
|
||||
if telegram_id:
|
||||
payload["telegramId"] = telegram_id
|
||||
if db_user.email:
|
||||
payload["email"] = db_user.email
|
||||
return payload
|
||||
|
||||
async def _get_or_create_panel_user_link_details(
|
||||
self, session: AsyncSession, user_id: int, db_user: Optional[User] = None
|
||||
) -> Tuple[Optional[str], Optional[str], Optional[str], bool]:
|
||||
@@ -109,25 +145,45 @@ class SubscriptionService:
|
||||
return None, None, None, False
|
||||
|
||||
current_local_panel_uuid = db_user.panel_user_uuid
|
||||
panel_username_on_panel_standard = f"tg_{user_id}"
|
||||
panel_username_on_panel_standard = await self._panel_username_for_user(
|
||||
session, db_user
|
||||
)
|
||||
telegram_id_for_panel = self._telegram_id_for_panel(db_user)
|
||||
|
||||
panel_user_obj_from_api = None
|
||||
panel_user_created_or_linked_now = False
|
||||
|
||||
panel_users_by_tg_id_list = await self.panel_service.get_users_by_filter(
|
||||
telegram_id=user_id
|
||||
)
|
||||
panel_users_by_tg_id_list = None
|
||||
if telegram_id_for_panel:
|
||||
panel_users_by_tg_id_list = await self.panel_service.get_users_by_filter(
|
||||
telegram_id=telegram_id_for_panel
|
||||
)
|
||||
if panel_users_by_tg_id_list and len(panel_users_by_tg_id_list) == 1:
|
||||
panel_user_obj_from_api = panel_users_by_tg_id_list[0]
|
||||
logging.info(
|
||||
f"Found panel user by telegramId {user_id}: UUID {panel_user_obj_from_api.get('uuid')}, Username: {panel_user_obj_from_api.get('username')}"
|
||||
f"Found panel user by telegramId {telegram_id_for_panel}: UUID {panel_user_obj_from_api.get('uuid')}, Username: {panel_user_obj_from_api.get('username')}"
|
||||
)
|
||||
elif panel_users_by_tg_id_list and len(panel_users_by_tg_id_list) > 1:
|
||||
logging.error(
|
||||
f"CRITICAL: Multiple panel users found for telegramId {user_id}. Manual intervention needed."
|
||||
f"CRITICAL: Multiple panel users found for telegramId {telegram_id_for_panel}. Manual intervention needed."
|
||||
)
|
||||
return None, None, None, False
|
||||
|
||||
if not panel_user_obj_from_api and db_user.email:
|
||||
panel_users_by_email_list = await self.panel_service.get_users_by_filter(
|
||||
email=db_user.email
|
||||
)
|
||||
if panel_users_by_email_list and len(panel_users_by_email_list) == 1:
|
||||
panel_user_obj_from_api = panel_users_by_email_list[0]
|
||||
logging.info(
|
||||
f"Found panel user by email {db_user.email}: UUID {panel_user_obj_from_api.get('uuid')}, Username: {panel_user_obj_from_api.get('username')}"
|
||||
)
|
||||
elif panel_users_by_email_list and len(panel_users_by_email_list) > 1:
|
||||
logging.error(
|
||||
f"CRITICAL: Multiple panel users found for email {db_user.email}. Manual intervention needed."
|
||||
)
|
||||
return None, None, None, False
|
||||
|
||||
if not panel_user_obj_from_api:
|
||||
if current_local_panel_uuid:
|
||||
|
||||
@@ -146,12 +202,9 @@ class SubscriptionService:
|
||||
)
|
||||
creation_response = await self.panel_service.create_panel_user(
|
||||
username_on_panel=panel_username_on_panel_standard,
|
||||
telegram_id=user_id,
|
||||
description="\n".join([
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]),
|
||||
telegram_id=telegram_id_for_panel,
|
||||
email=db_user.email,
|
||||
description=self._panel_description_for_user(db_user),
|
||||
specific_squad_uuids=self.settings.parsed_user_squad_uuids,
|
||||
external_squad_uuid=self.settings.parsed_user_external_squad_uuid,
|
||||
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||
@@ -175,12 +228,9 @@ class SubscriptionService:
|
||||
)
|
||||
creation_response = await self.panel_service.create_panel_user(
|
||||
username_on_panel=panel_username_on_panel_standard,
|
||||
telegram_id=user_id,
|
||||
description="\n".join([
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]),
|
||||
telegram_id=telegram_id_for_panel,
|
||||
email=db_user.email,
|
||||
description=self._panel_description_for_user(db_user),
|
||||
specific_squad_uuids=self.settings.parsed_user_squad_uuids,
|
||||
external_squad_uuid=self.settings.parsed_user_external_squad_uuid,
|
||||
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||
@@ -226,7 +276,6 @@ class SubscriptionService:
|
||||
)
|
||||
|
||||
actual_panel_uuid_from_api = panel_user_obj_from_api.get("uuid")
|
||||
actual_panel_username_from_api = panel_user_obj_from_api.get("username")
|
||||
panel_telegram_id_from_api = panel_user_obj_from_api.get("telegramId")
|
||||
|
||||
if not actual_panel_uuid_from_api:
|
||||
@@ -293,24 +342,15 @@ class SubscriptionService:
|
||||
if (
|
||||
panel_user_obj_from_api
|
||||
and current_local_panel_uuid
|
||||
and panel_telegram_id_int != user_id
|
||||
and telegram_id_for_panel
|
||||
and panel_telegram_id_int != telegram_id_for_panel
|
||||
):
|
||||
logging.info(
|
||||
f"Panel user {current_local_panel_uuid} has telegramId '{panel_telegram_id_from_api}'. Updating on panel to '{user_id}'."
|
||||
f"Panel user {current_local_panel_uuid} has telegramId '{panel_telegram_id_from_api}'. Updating on panel to '{telegram_id_for_panel}'."
|
||||
)
|
||||
# Also set readable description with Telegram fields
|
||||
await self.panel_service.update_user_details_on_panel(
|
||||
current_local_panel_uuid,
|
||||
{
|
||||
"telegramId": user_id,
|
||||
"description": "\n".join(
|
||||
[
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]
|
||||
),
|
||||
},
|
||||
self._panel_identity_payload_for_user(db_user),
|
||||
)
|
||||
|
||||
panel_sub_link_id = panel_user_obj_from_api.get(
|
||||
@@ -408,14 +448,7 @@ class SubscriptionService:
|
||||
traffic_limit_bytes=self.settings.trial_traffic_limit_bytes,
|
||||
)
|
||||
|
||||
# Add user description based on Telegram profile
|
||||
panel_update_payload["description"] = "\n".join(
|
||||
[
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]
|
||||
)
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, panel_update_payload
|
||||
@@ -525,13 +558,7 @@ class SubscriptionService:
|
||||
traffic_limit_strategy="NO_RESET",
|
||||
)
|
||||
|
||||
panel_update_payload["description"] = "\n".join(
|
||||
[
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]
|
||||
)
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, panel_update_payload
|
||||
@@ -695,14 +722,7 @@ class SubscriptionService:
|
||||
traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||
)
|
||||
|
||||
# Add user description based on Telegram profile
|
||||
panel_update_payload["description"] = "\n".join(
|
||||
[
|
||||
(db_user.username or "") if db_user else "",
|
||||
(db_user.first_name or "") if db_user else "",
|
||||
(db_user.last_name or "") if db_user else "",
|
||||
]
|
||||
)
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, panel_update_payload
|
||||
|
||||
Reference in New Issue
Block a user