Ruff format/ Cleanup
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
from . import identities
|
||||
from .audit import *
|
||||
from .bans import *
|
||||
from .coupons import *
|
||||
from .db import Base, async_session_maker, engine, reset_async_db_engine
|
||||
from .gifts import *
|
||||
from . import identities
|
||||
from .hot_leads import *
|
||||
from .setup.init_db import *
|
||||
from .keys import *
|
||||
from .notifications import *
|
||||
from .payments import *
|
||||
from .referrals import *
|
||||
from .servers import *
|
||||
from .scheduled_broadcasts import *
|
||||
from .servers import *
|
||||
from .settings_cache import settings_cache
|
||||
from .setup.init_db import *
|
||||
from .statistics import *
|
||||
from .tariffs import *
|
||||
from .temporary_data import *
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from database.models import Identity, User
|
||||
|
||||
|
||||
class ActorSurface(str, Enum):
|
||||
class ActorSurface(StrEnum):
|
||||
TELEGRAM = "telegram"
|
||||
WEB = "web"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@@ -37,14 +37,8 @@ async def refresh_tg_mirrors_for_user(session: AsyncSession, user_id: int) -> No
|
||||
await session.execute(update(BlockedUser).where(BlockedUser.user_id == user_id).values(tg_id=tg))
|
||||
await session.execute(update(ManualBan).where(ManualBan.user_id == user_id).values(tg_id=tg))
|
||||
|
||||
await session.execute(
|
||||
update(Referral).where(Referral.referred_user_id == user_id).values(referred_tg_id=tg)
|
||||
)
|
||||
await session.execute(
|
||||
update(Referral).where(Referral.referrer_user_id == user_id).values(referrer_tg_id=tg)
|
||||
)
|
||||
await session.execute(update(Referral).where(Referral.referred_user_id == user_id).values(referred_tg_id=tg))
|
||||
await session.execute(update(Referral).where(Referral.referrer_user_id == user_id).values(referrer_tg_id=tg))
|
||||
|
||||
await session.execute(update(Gift).where(Gift.sender_user_id == user_id).values(sender_tg_id=tg))
|
||||
await session.execute(
|
||||
update(Gift).where(Gift.recipient_user_id == user_id).values(recipient_tg_id=tg)
|
||||
)
|
||||
await session.execute(update(Gift).where(Gift.recipient_user_id == user_id).values(recipient_tg_id=tg))
|
||||
|
||||
+12
-2
@@ -1,13 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Iterable
|
||||
|
||||
from sqlalchemy import DateTime as SQLADateTime, and_, cast, delete, desc, func, or_, select
|
||||
from sqlalchemy import (
|
||||
DateTime as SQLADateTime,
|
||||
and_,
|
||||
cast,
|
||||
delete,
|
||||
desc,
|
||||
func,
|
||||
or_,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database.models import AuditEvent, Payment
|
||||
|
||||
|
||||
try:
|
||||
from core.constants import PAYMENT_SYSTEMS_EXCLUDED
|
||||
except ImportError:
|
||||
|
||||
+2
-6
@@ -2,8 +2,8 @@ from sqlalchemy import select
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database.models import BlockedUser, User
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from database.models import BlockedUser, User
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -30,11 +30,7 @@ async def save_blocked_user_ids(session: AsyncSession, tg_ids: list[int]) -> Non
|
||||
res = await session.execute(select(User.id, User.tg_id).where(User.tg_id.in_(batch)))
|
||||
rows = res.all()
|
||||
uid_by_tg = {int(tgid): int(uid) for uid, tgid in rows if tgid is not None}
|
||||
values = [
|
||||
{"user_id": uid_by_tg[int(tg)], "tg_id": int(tg)}
|
||||
for tg in batch
|
||||
if int(tg) in uid_by_tg
|
||||
]
|
||||
values = [{"user_id": uid_by_tg[int(tg)], "tg_id": int(tg)} for tg in batch if int(tg) in uid_by_tg]
|
||||
if not values:
|
||||
continue
|
||||
stmt = insert(BlockedUser).values(values).on_conflict_do_nothing(index_elements=[BlockedUser.user_id])
|
||||
|
||||
+1
-3
@@ -74,9 +74,7 @@ async def get_gift_usage(session: AsyncSession, gift_id: str, user_id: int) -> G
|
||||
|
||||
async def count_gift_usages(session: AsyncSession, gift_id: str) -> int:
|
||||
"""Сколько раз подарок был активирован (для `is_unlimited=False` с лимитом)."""
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(GiftUsage).where(GiftUsage.gift_id == gift_id)
|
||||
)
|
||||
result = await session.execute(select(func.count()).select_from(GiftUsage).where(GiftUsage.gift_id == gift_id))
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
|
||||
|
||||
+21
-29
@@ -1,8 +1,10 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import bcrypt
|
||||
|
||||
from sqlalchemy import delete, func, select, text, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -422,9 +424,7 @@ async def _transfer_user_data(
|
||||
await session.execute(update(Notification).where(Notification.user_id == src_uid).values(user_id=dst_uid))
|
||||
|
||||
await session.execute(update(Gift).where(Gift.sender_user_id == src_uid).values(sender_user_id=dst_uid))
|
||||
await session.execute(
|
||||
update(Gift).where(Gift.recipient_user_id == src_uid).values(recipient_user_id=dst_uid)
|
||||
)
|
||||
await session.execute(update(Gift).where(Gift.recipient_user_id == src_uid).values(recipient_user_id=dst_uid))
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
@@ -468,31 +468,27 @@ async def _transfer_user_data(
|
||||
),
|
||||
{"src": src_uid, "dst": dst_uid},
|
||||
)
|
||||
await session.execute(
|
||||
update(Referral).where(Referral.referred_user_id == src_uid).values(referred_user_id=dst_uid)
|
||||
)
|
||||
await session.execute(
|
||||
update(Referral).where(Referral.referrer_user_id == src_uid).values(referrer_user_id=dst_uid)
|
||||
)
|
||||
await session.execute(update(Referral).where(Referral.referred_user_id == src_uid).values(referred_user_id=dst_uid))
|
||||
await session.execute(update(Referral).where(Referral.referrer_user_id == src_uid).values(referrer_user_id=dst_uid))
|
||||
|
||||
await session.execute(
|
||||
update(WebPushSubscription).where(WebPushSubscription.user_id == src_uid).values(user_id=dst_uid)
|
||||
)
|
||||
await session.execute(
|
||||
update(WebNotification).where(WebNotification.user_id == src_uid).values(user_id=dst_uid)
|
||||
)
|
||||
await session.execute(update(WebNotification).where(WebNotification.user_id == src_uid).values(user_id=dst_uid))
|
||||
|
||||
dst_ban = (await session.execute(select(ManualBan).where(ManualBan.user_id == dst_uid))).scalar_one_or_none()
|
||||
src_ban = (await session.execute(select(ManualBan).where(ManualBan.user_id == src_uid))).scalar_one_or_none()
|
||||
if src_ban is not None and dst_ban is None:
|
||||
session.add(ManualBan(
|
||||
user_id=dst_uid,
|
||||
tg_id=dst_tg,
|
||||
banned_at=src_ban.banned_at,
|
||||
reason=src_ban.reason,
|
||||
banned_by=src_ban.banned_by,
|
||||
until=src_ban.until,
|
||||
))
|
||||
session.add(
|
||||
ManualBan(
|
||||
user_id=dst_uid,
|
||||
tg_id=dst_tg,
|
||||
banned_at=src_ban.banned_at,
|
||||
reason=src_ban.reason,
|
||||
banned_by=src_ban.banned_by,
|
||||
until=src_ban.until,
|
||||
)
|
||||
)
|
||||
|
||||
dst_block = (await session.execute(select(BlockedUser).where(BlockedUser.user_id == dst_uid))).scalar_one_or_none()
|
||||
src_block = (await session.execute(select(BlockedUser).where(BlockedUser.user_id == src_uid))).scalar_one_or_none()
|
||||
@@ -518,8 +514,8 @@ async def _transfer_user_data(
|
||||
|
||||
|
||||
async def merge_billing_user_into_telegram(session: AsyncSession, identity_id: str, telegram_tg_id: int) -> None:
|
||||
from database.models import User as _User # noqa: F401
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from database.models import User as _User # noqa: F401
|
||||
from database.users import update_balance
|
||||
|
||||
res = await session.execute(select(User).where(User.identity_id == identity_id))
|
||||
@@ -593,9 +589,7 @@ async def attach_email(session: AsyncSession, identity_id: str, email: str) -> I
|
||||
if not can_merge:
|
||||
return None
|
||||
|
||||
src_user = (
|
||||
await session.execute(select(User).where(User.identity_id == existing.id))
|
||||
).scalars().first()
|
||||
src_user = (await session.execute(select(User).where(User.identity_id == existing.id))).scalars().first()
|
||||
dst_uid = await ensure_billing_user_for_identity(session, identity)
|
||||
dst_tg = int(identity.tg_id) if identity.tg_id is not None else None
|
||||
|
||||
@@ -633,8 +627,8 @@ async def attach_telegram(session: AsyncSession, identity_id: str, tg_id: int) -
|
||||
return None
|
||||
existing = await get_identity_by_tg_id(session, tg_id)
|
||||
if existing and existing.id != identity_id:
|
||||
our_email = (str(identity.email).strip().lower() if identity.email else None)
|
||||
their_email = (str(existing.email).strip().lower() if existing.email else None)
|
||||
our_email = str(identity.email).strip().lower() if identity.email else None
|
||||
their_email = str(existing.email).strip().lower() if existing.email else None
|
||||
can_merge = their_email is None or (our_email is not None and their_email == our_email)
|
||||
if not can_merge:
|
||||
return None
|
||||
@@ -687,9 +681,7 @@ async def detach_telegram(session: AsyncSession, identity_id: str) -> Identity |
|
||||
old_tg = int(identity.tg_id)
|
||||
identity.tg_id = None
|
||||
identity.is_admin = False
|
||||
await session.execute(
|
||||
update(User).where(User.identity_id == identity_id, User.tg_id == old_tg).values(tg_id=None)
|
||||
)
|
||||
await session.execute(update(User).where(User.identity_id == identity_id, User.tg_id == old_tg).values(tg_id=None))
|
||||
await session.refresh(identity)
|
||||
return identity
|
||||
|
||||
|
||||
+12
-25
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -6,9 +7,9 @@ from sqlalchemy import delete, func, select, text, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.cache_config import (
|
||||
KEYS_LIST_CACHE_TTL_SEC,
|
||||
KEY_COUNT_CACHE_TTL_SEC,
|
||||
KEY_DETAILS_CACHE_TTL_SEC,
|
||||
KEYS_LIST_CACHE_TTL_SEC,
|
||||
)
|
||||
from core.redis_cache import cache_delete, cache_get, cache_key, cache_set
|
||||
from database.access.resolution import resolve_user_optional
|
||||
@@ -312,17 +313,13 @@ async def get_key_count(session: AsyncSession, legacy_user_ref: int) -> int:
|
||||
|
||||
async def get_key_by_user_and_email(session: AsyncSession, user_id: int, email: str) -> Key | None:
|
||||
"""Возвращает ORM-объект Key по паре (users.id, email) или None."""
|
||||
result = await session.execute(
|
||||
select(Key).where(Key.user_id == int(user_id), Key.email == email)
|
||||
)
|
||||
result = await session.execute(select(Key).where(Key.user_id == int(user_id), Key.email == email))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def delete_key_by_user_and_email(session: AsyncSession, user_id: int, email: str) -> None:
|
||||
"""Удаляет ключ по паре (users.id, email). Commit — ответственность caller'а."""
|
||||
await session.execute(
|
||||
delete(Key).where(Key.user_id == int(user_id), Key.email == email)
|
||||
)
|
||||
await session.execute(delete(Key).where(Key.user_id == int(user_id), Key.email == email))
|
||||
|
||||
|
||||
async def get_user_keys_with_servers_by_email(
|
||||
@@ -366,19 +363,13 @@ async def get_user_keys_with_servers_by_email(
|
||||
return rows
|
||||
|
||||
|
||||
async def get_key_client_id_by_email_and_server(
|
||||
session: AsyncSession, email: str, server_id: str
|
||||
) -> str | None:
|
||||
async def get_key_client_id_by_email_and_server(session: AsyncSession, email: str, server_id: str) -> str | None:
|
||||
"""Возвращает ``client_id`` первого ключа для пары (email, server_id).
|
||||
|
||||
Используется для remnawave traffic reset, где нам нужен только client_id,
|
||||
без остальных полей ключа.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Key.client_id)
|
||||
.where(Key.email == email, Key.server_id == server_id)
|
||||
.limit(1)
|
||||
)
|
||||
result = await session.execute(select(Key.client_id).where(Key.email == email, Key.server_id == server_id).limit(1))
|
||||
return result.scalar()
|
||||
|
||||
|
||||
@@ -389,9 +380,7 @@ async def count_keys_by_server_id(session: AsyncSession, server_id: str) -> int:
|
||||
(у ``keys.server_id`` колонка типа String, содержит либо cluster_name,
|
||||
либо server_name в зависимости от страны/кластера).
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(Key).where(Key.server_id == server_id)
|
||||
)
|
||||
result = await session.execute(select(func.count()).select_from(Key).where(Key.server_id == server_id))
|
||||
return int(result.scalar() or 0)
|
||||
|
||||
|
||||
@@ -413,9 +402,7 @@ async def count_active_keys_for_user(session: AsyncSession, user_id: int) -> int
|
||||
Используется в проверке "новый пользователь" для купонных правил.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(func.count())
|
||||
.select_from(Key)
|
||||
.where(Key.user_id == int(user_id), Key.is_frozen.is_(False))
|
||||
select(func.count()).select_from(Key).where(Key.user_id == int(user_id), Key.is_frozen.is_(False))
|
||||
)
|
||||
return int(result.scalar() or 0)
|
||||
|
||||
@@ -424,9 +411,7 @@ async def delete_key(session: AsyncSession, identifier: int | str):
|
||||
legacy_for_cache = None
|
||||
email_for_cache = None
|
||||
if isinstance(identifier, str):
|
||||
res = await session.execute(
|
||||
select(Key.user_id, Key.email).where(Key.client_id == identifier).limit(1)
|
||||
)
|
||||
res = await session.execute(select(Key.user_id, Key.email).where(Key.client_id == identifier).limit(1))
|
||||
row = res.first()
|
||||
if row:
|
||||
legacy_for_cache, email_for_cache = row[0], row[1]
|
||||
@@ -675,7 +660,9 @@ async def save_key_tariff_selection(
|
||||
if u is None:
|
||||
return
|
||||
selected_devices_val = int(selected_devices) if selected_devices is not None else None
|
||||
selected_traffic_val = int(selected_traffic_gb) if selected_traffic_gb is not None and int(selected_traffic_gb) > 0 else None
|
||||
selected_traffic_val = (
|
||||
int(selected_traffic_gb) if selected_traffic_gb is not None and int(selected_traffic_gb) > 0 else None
|
||||
)
|
||||
|
||||
await session.execute(
|
||||
update(Key)
|
||||
|
||||
@@ -45,9 +45,7 @@ async def _ensure_migrations_table(conn: AsyncConnection) -> None:
|
||||
|
||||
async def _get_current_version(conn: AsyncConnection) -> int:
|
||||
await _ensure_migrations_table(conn)
|
||||
r = await conn.execute(
|
||||
text("SELECT COALESCE(MAX(version), 0) FROM schema_migrations")
|
||||
)
|
||||
r = await conn.execute(text("SELECT COALESCE(MAX(version), 0) FROM schema_migrations"))
|
||||
row = r.first()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
@@ -186,9 +184,7 @@ async def _drop_pk(conn: AsyncConnection, table: str) -> None:
|
||||
|
||||
|
||||
async def _column_has_nulls(conn: AsyncConnection, table: str, column: str) -> bool:
|
||||
r = await conn.execute(
|
||||
text(f'SELECT 1 FROM "{table}" WHERE "{column}" IS NULL LIMIT 1')
|
||||
)
|
||||
r = await conn.execute(text(f'SELECT 1 FROM "{table}" WHERE "{column}" IS NULL LIMIT 1'))
|
||||
return r.first() is not None
|
||||
|
||||
|
||||
@@ -309,9 +305,7 @@ async def _migration_v2_add_user_id_columns(conn: AsyncConnection) -> None:
|
||||
await conn.execute(text(f'ALTER TABLE "{table}" ADD COLUMN {column} BIGINT'))
|
||||
|
||||
|
||||
async def _backfill_users_from_table(
|
||||
conn: AsyncConnection, table: str, tg_col: str = "tg_id"
|
||||
) -> int:
|
||||
async def _backfill_users_from_table(conn: AsyncConnection, table: str, tg_col: str = "tg_id") -> int:
|
||||
"""Auto-создание users для orphan tg_id'ов из указанной таблицы.
|
||||
|
||||
Legacy клиенты обновляются с TG-only схемы (где только tg_id), и в связанных
|
||||
@@ -358,9 +352,7 @@ async def _backfill_users_from_table(
|
||||
)
|
||||
created = result.rowcount or 0
|
||||
if created > 0:
|
||||
logger.info(
|
||||
f"[schema_upgrade] users backfill: создано {created} юзеров из orphan {table}.{tg_col}"
|
||||
)
|
||||
logger.info(f"[schema_upgrade] users backfill: создано {created} юзеров из orphan {table}.{tg_col}")
|
||||
return created
|
||||
|
||||
|
||||
@@ -400,9 +392,7 @@ async def _migration_v3_populate_user_ids(conn: AsyncConnection) -> None:
|
||||
if updated > 0:
|
||||
logger.debug(f"[schema_upgrade] v3: заполнено {updated} записей {user_col} в {table}")
|
||||
|
||||
null_count = await conn.execute(
|
||||
text(f'SELECT COUNT(*) FROM "{table}" WHERE {user_col} IS NULL')
|
||||
)
|
||||
null_count = await conn.execute(text(f'SELECT COUNT(*) FROM "{table}" WHERE {user_col} IS NULL'))
|
||||
nulls = null_count.scalar()
|
||||
if nulls > 0:
|
||||
logger.warning(f"[schema_upgrade] v3: в {table} осталось {nulls} записей с NULL {user_col}")
|
||||
@@ -511,17 +501,13 @@ async def _migration_v5_switch_pks_to_user_id(conn: AsyncConnection) -> None:
|
||||
await _drop_pk(conn, "referrals")
|
||||
await conn.execute(text("ALTER TABLE referrals ALTER COLUMN referred_user_id SET NOT NULL"))
|
||||
await conn.execute(text("ALTER TABLE referrals ALTER COLUMN referrer_user_id SET NOT NULL"))
|
||||
await conn.execute(
|
||||
text("ALTER TABLE referrals ADD PRIMARY KEY (referred_user_id, referrer_user_id)")
|
||||
)
|
||||
await conn.execute(text("ALTER TABLE referrals ADD PRIMARY KEY (referred_user_id, referrer_user_id)"))
|
||||
else:
|
||||
logger.warning("[schema_upgrade] referrals содержит NULL user_id, пропуск перевода PK")
|
||||
|
||||
if await _table_exists(conn, "notifications") and await _safe_set_not_null(conn, "notifications", "user_id"):
|
||||
await _drop_pk(conn, "notifications")
|
||||
await conn.execute(
|
||||
text("ALTER TABLE notifications ADD PRIMARY KEY (user_id, notification_type)")
|
||||
)
|
||||
await conn.execute(text("ALTER TABLE notifications ADD PRIMARY KEY (user_id, notification_type)"))
|
||||
|
||||
if await _table_exists(conn, "gift_usages") and await _safe_set_not_null(conn, "gift_usages", "user_id"):
|
||||
await _drop_pk(conn, "gift_usages")
|
||||
@@ -980,6 +966,7 @@ async def _migration_v14_web_flow_graph_model(conn: AsyncConnection) -> None:
|
||||
"target": node_id,
|
||||
})
|
||||
import json
|
||||
|
||||
await conn.execute(
|
||||
text("UPDATE web_flows SET nodes = :nodes, edges = :edges, entry_node_id = :entry WHERE id = :fid"),
|
||||
{"nodes": json.dumps(new_nodes), "edges": json.dumps(new_edges), "entry": entry_id, "fid": flow_id},
|
||||
@@ -1054,10 +1041,7 @@ async def _migration_v15_recover_orphan_users(conn: AsyncConnection) -> None:
|
||||
)
|
||||
)
|
||||
if result.rowcount and result.rowcount > 0:
|
||||
logger.info(
|
||||
f"[schema_upgrade] v15: повторно заполнено {result.rowcount} записей "
|
||||
f"{table}.{user_col}"
|
||||
)
|
||||
logger.info(f"[schema_upgrade] v15: повторно заполнено {result.rowcount} записей {table}.{user_col}")
|
||||
|
||||
|
||||
async def _migration_v18_web_error_reports(conn: AsyncConnection) -> None:
|
||||
@@ -1206,10 +1190,10 @@ async def _migration_v20_add_identity_google_sub(conn: AsyncConnection) -> None:
|
||||
if not await _table_exists(conn, "identities"):
|
||||
return
|
||||
if not await _column_exists(conn, "identities", "google_sub"):
|
||||
await _exec_ignore(conn, 'ALTER TABLE identities ADD COLUMN google_sub VARCHAR(64)')
|
||||
await _exec_ignore(conn, "ALTER TABLE identities ADD COLUMN google_sub VARCHAR(64)")
|
||||
await _exec_ignore(
|
||||
conn,
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS ix_identities_google_sub ON identities (google_sub) WHERE google_sub IS NOT NULL',
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS ix_identities_google_sub ON identities (google_sub) WHERE google_sub IS NOT NULL",
|
||||
)
|
||||
|
||||
|
||||
@@ -1218,10 +1202,10 @@ async def _migration_v21_add_identity_yandex_sub(conn: AsyncConnection) -> None:
|
||||
if not await _table_exists(conn, "identities"):
|
||||
return
|
||||
if not await _column_exists(conn, "identities", "yandex_sub"):
|
||||
await _exec_ignore(conn, 'ALTER TABLE identities ADD COLUMN yandex_sub VARCHAR(64)')
|
||||
await _exec_ignore(conn, "ALTER TABLE identities ADD COLUMN yandex_sub VARCHAR(64)")
|
||||
await _exec_ignore(
|
||||
conn,
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS ix_identities_yandex_sub ON identities (yandex_sub) WHERE yandex_sub IS NOT NULL',
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS ix_identities_yandex_sub ON identities (yandex_sub) WHERE yandex_sub IS NOT NULL",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from ._base import Base, DictLikeMixin
|
||||
|
||||
class AuditEvent(DictLikeMixin, Base):
|
||||
"""События аудита (флоу пользователя)."""
|
||||
|
||||
__tablename__ = "audit_events"
|
||||
__table_args__ = (
|
||||
Index("ix_audit_events_tg_created", "actor_tg_id", "created_at"),
|
||||
|
||||
@@ -6,7 +6,9 @@ from ._base import Base, DictLikeMixin
|
||||
class Key(DictLikeMixin, Base):
|
||||
__tablename__ = "keys"
|
||||
|
||||
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True, nullable=False, index=True)
|
||||
user_id = Column(
|
||||
BigInteger, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True, nullable=False, index=True
|
||||
)
|
||||
client_id = Column(String, primary_key=True)
|
||||
tg_id = Column(BigInteger, ForeignKey("users.tg_id"), nullable=True, index=True)
|
||||
email = Column(String, unique=True)
|
||||
|
||||
@@ -74,9 +74,7 @@ class WebPageVariantBlock(DictLikeMixin, Base):
|
||||
|
||||
class WebPushSubscription(DictLikeMixin, Base):
|
||||
__tablename__ = "web_push_subscriptions"
|
||||
__table_args__ = (
|
||||
Index("ix_web_push_subscriptions_user_id", "user_id"),
|
||||
)
|
||||
__table_args__ = (Index("ix_web_push_subscriptions_user_id", "user_id"),)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
@@ -124,7 +122,9 @@ class WebErrorReport(DictLikeMixin, Base):
|
||||
count = Column(Integer, nullable=False, default=1)
|
||||
resolved = Column(Boolean, nullable=False, default=False)
|
||||
first_seen_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
||||
last_seen_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
last_seen_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
|
||||
)
|
||||
|
||||
|
||||
class WebFlowEvent(DictLikeMixin, Base):
|
||||
|
||||
+22
-31
@@ -7,8 +7,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import DISCOUNT_ACTIVE_HOURS
|
||||
from core.bootstrap import NOTIFICATIONS_CONFIG
|
||||
from database.models import BlockedUser, Key, Notification, User
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from database.models import BlockedUser, Key, Notification, User
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -93,17 +93,15 @@ async def bulk_add_notifications(session: AsyncSession, items: list[tuple[int, s
|
||||
total = 0
|
||||
for i in range(0, len(mapped), _BULK_ADD_NOTIFICATIONS_BATCH_SIZE):
|
||||
batch = mapped[i : i + _BULK_ADD_NOTIFICATIONS_BATCH_SIZE]
|
||||
ins = insert(Notification).values(
|
||||
[
|
||||
{
|
||||
"user_id": uid,
|
||||
"tg_id": tg_by_uid.get(uid),
|
||||
"notification_type": ntype,
|
||||
"last_notification_time": now,
|
||||
}
|
||||
for uid, ntype in batch
|
||||
]
|
||||
)
|
||||
ins = insert(Notification).values([
|
||||
{
|
||||
"user_id": uid,
|
||||
"tg_id": tg_by_uid.get(uid),
|
||||
"notification_type": ntype,
|
||||
"last_notification_time": now,
|
||||
}
|
||||
for uid, ntype in batch
|
||||
])
|
||||
stmt = ins.on_conflict_do_update(
|
||||
index_elements=[Notification.user_id, Notification.notification_type],
|
||||
set_={
|
||||
@@ -130,15 +128,15 @@ async def bulk_delete_notifications(session: AsyncSession, items: list[tuple[int
|
||||
total = 0
|
||||
for i in range(0, len(mapped), _BULK_ADD_NOTIFICATIONS_BATCH_SIZE):
|
||||
batch = mapped[i : i + _BULK_ADD_NOTIFICATIONS_BATCH_SIZE]
|
||||
stmt = delete(Notification).where(
|
||||
tuple_(Notification.user_id, Notification.notification_type).in_(batch)
|
||||
)
|
||||
stmt = delete(Notification).where(tuple_(Notification.user_id, Notification.notification_type).in_(batch))
|
||||
await session.execute(stmt)
|
||||
total += len(batch)
|
||||
logger.debug(f"🗑 Bulk: удалено {total} уведомлений")
|
||||
|
||||
|
||||
async def check_notification_time(session: AsyncSession, legacy_user_ref: int, notification_type: str, hours: int = 12) -> bool:
|
||||
async def check_notification_time(
|
||||
session: AsyncSession, legacy_user_ref: int, notification_type: str, hours: int = 12
|
||||
) -> bool:
|
||||
u = await resolve_user_optional(session, legacy_user_ref)
|
||||
if u is None:
|
||||
return True
|
||||
@@ -170,8 +168,7 @@ async def check_notification_time_bulk(
|
||||
can_notify = set()
|
||||
found = set()
|
||||
for batch in (
|
||||
items[i : i + _NOTIFICATION_TIME_BATCH_SIZE]
|
||||
for i in range(0, len(items), _NOTIFICATION_TIME_BATCH_SIZE)
|
||||
items[i : i + _NOTIFICATION_TIME_BATCH_SIZE] for i in range(0, len(items), _NOTIFICATION_TIME_BATCH_SIZE)
|
||||
):
|
||||
id_map = await _map_legacy_refs_to_user_ids(session, [p[0] for p in batch])
|
||||
mapped_batch = [(id_map[r], n) for r, n in batch if r in id_map]
|
||||
@@ -255,9 +252,7 @@ _HOT_LEAD_NOTIFICATION_TYPES = (
|
||||
)
|
||||
|
||||
|
||||
async def get_hot_lead_notification_flags(
|
||||
session: AsyncSession, tg_ids: list[int]
|
||||
) -> dict[int, set[str]]:
|
||||
async def get_hot_lead_notification_flags(session: AsyncSession, tg_ids: list[int]) -> dict[int, set[str]]:
|
||||
"""
|
||||
Один запрос: для каждого tg_id возвращает множество типов уведомлений hot_lead_*,
|
||||
которые у него уже есть. Используется в notify_hot_leads для устранения N+1.
|
||||
@@ -336,15 +331,12 @@ async def check_notifications_bulk(
|
||||
now = _utc_now()
|
||||
|
||||
if notification_type == "inactive_trial":
|
||||
stmt_inactive = (
|
||||
select(User.id)
|
||||
.where(
|
||||
and_(
|
||||
User.trial.in_([0, -1]),
|
||||
User.tg_id.isnot(None),
|
||||
~User.id.in_(select(BlockedUser.user_id)),
|
||||
~User.id.in_(select(Key.user_id.distinct())),
|
||||
)
|
||||
stmt_inactive = select(User.id).where(
|
||||
and_(
|
||||
User.trial.in_([0, -1]),
|
||||
User.tg_id.isnot(None),
|
||||
~User.id.in_(select(BlockedUser.user_id)),
|
||||
~User.id.in_(select(Key.user_id.distinct())),
|
||||
)
|
||||
)
|
||||
result_inactive = await session.execute(stmt_inactive)
|
||||
@@ -559,4 +551,3 @@ async def check_notifications_bulk(
|
||||
|
||||
logger.info(f"Найдено {len(users)} пользователей, готовых к уведомлению типа {notification_type}")
|
||||
return users
|
||||
|
||||
|
||||
+4
-12
@@ -128,17 +128,13 @@ async def check_server_name_by_cluster(session: AsyncSession, server_name: str)
|
||||
|
||||
async def get_panel_types_for_cluster(session: AsyncSession, cluster_name: str) -> list[str]:
|
||||
"""Список panel_type всех серверов кластера (для проверки "весь remnawave")."""
|
||||
result = await session.execute(
|
||||
select(Server.panel_type).where(Server.cluster_name == cluster_name)
|
||||
)
|
||||
result = await session.execute(select(Server.panel_type).where(Server.cluster_name == cluster_name))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_panel_type_for_server(session: AsyncSession, server_name: str) -> str | None:
|
||||
"""Возвращает panel_type конкретного сервера по его имени."""
|
||||
result = await session.execute(
|
||||
select(Server.panel_type).where(Server.server_name == server_name)
|
||||
)
|
||||
result = await session.execute(select(Server.panel_type).where(Server.server_name == server_name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -152,17 +148,13 @@ async def get_enabled_server_subscription_url(session: AsyncSession, server_name
|
||||
|
||||
async def cluster_name_exists(session: AsyncSession, cluster_name: str) -> bool:
|
||||
"""Есть ли хоть один сервер с таким cluster_name."""
|
||||
result = await session.execute(
|
||||
select(Server).where(Server.cluster_name == cluster_name).limit(1)
|
||||
)
|
||||
result = await session.execute(select(Server).where(Server.cluster_name == cluster_name).limit(1))
|
||||
return result.scalars().first() is not None
|
||||
|
||||
|
||||
async def get_cluster_name_for_server_name(session: AsyncSession, server_name: str) -> str | None:
|
||||
"""Возвращает cluster_name для указанного server_name (строго по server_name)."""
|
||||
result = await session.execute(
|
||||
select(Server.cluster_name).where(Server.server_name == server_name).limit(1)
|
||||
)
|
||||
result = await session.execute(select(Server.cluster_name).where(Server.server_name == server_name).limit(1))
|
||||
return result.scalar()
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
+5
-13
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from sqlalchemy import delete, func, insert, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.cache_config import TARIFF_BY_ID_CACHE_TTL_SEC, TARIFFS_FOR_CLUSTER_CACHE_TTL_SEC
|
||||
from core.cache_config import TARIFFS_FOR_CLUSTER_CACHE_TTL_SEC, TARIFF_BY_ID_CACHE_TTL_SEC
|
||||
from core.redis_cache import cache_delete, cache_delete_pattern, cache_get, cache_key, cache_set
|
||||
from database.models import Server, Tariff
|
||||
from logger import logger
|
||||
@@ -143,9 +143,7 @@ async def get_tariff_group_codes(session: AsyncSession) -> list[str]:
|
||||
|
||||
async def get_active_tariff_by_id(session: AsyncSession, tariff_id: int) -> Tariff | None:
|
||||
"""Возвращает ORM-объект Tariff по id, если тариф активен (is_active=True)."""
|
||||
result = await session.execute(
|
||||
select(Tariff).where(Tariff.id == int(tariff_id), Tariff.is_active.is_(True))
|
||||
)
|
||||
result = await session.execute(select(Tariff).where(Tariff.id == int(tariff_id), Tariff.is_active.is_(True)))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -161,9 +159,7 @@ async def get_tariffs_for_cluster(session: AsyncSession, cluster_name: str):
|
||||
cached = await cache_get(key)
|
||||
if isinstance(cached, list):
|
||||
return cached
|
||||
server_row = await session.execute(
|
||||
select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1)
|
||||
)
|
||||
server_row = await session.execute(select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1))
|
||||
row = server_row.first()
|
||||
|
||||
if not row:
|
||||
@@ -245,9 +241,7 @@ async def get_vless_enabled(session: AsyncSession, tariff_id: int | None) -> boo
|
||||
return bool(tariff.get("vless"))
|
||||
|
||||
|
||||
async def get_vless_enabled_batch(
|
||||
session: AsyncSession, tariff_ids: list[int]
|
||||
) -> dict[int, bool]:
|
||||
async def get_vless_enabled_batch(session: AsyncSession, tariff_ids: list[int]) -> dict[int, bool]:
|
||||
"""
|
||||
Один запрос: для списка tariff_id возвращает dict[tariff_id -> vless].
|
||||
Использовать в списках ключей вместо N вызовов get_vless_enabled.
|
||||
@@ -255,9 +249,7 @@ async def get_vless_enabled_batch(
|
||||
if not tariff_ids:
|
||||
return {}
|
||||
unique_ids = list(dict.fromkeys(tariff_ids))
|
||||
result = await session.execute(
|
||||
select(Tariff.id, Tariff.vless).where(Tariff.id.in_(unique_ids))
|
||||
)
|
||||
result = await session.execute(select(Tariff.id, Tariff.vless).where(Tariff.id.in_(unique_ids)))
|
||||
return {row[0]: bool(row[1]) for row in result.all()}
|
||||
|
||||
|
||||
|
||||
+3
-9
@@ -156,9 +156,7 @@ async def set_user_balance(
|
||||
|
||||
async def get_user_preferred_currency(session: AsyncSession, tg_id: int) -> str | None:
|
||||
"""Предпочитаемая валюта пользователя по ``tg_id``, если установлена."""
|
||||
result = await session.execute(
|
||||
select(User.preferred_currency).where(User.tg_id == int(tg_id))
|
||||
)
|
||||
result = await session.execute(select(User.preferred_currency).where(User.tg_id == int(tg_id)))
|
||||
return result.scalar()
|
||||
|
||||
|
||||
@@ -169,9 +167,7 @@ async def mark_trial_started_if_eligible(session: AsyncSession, tg_id: int) -> N
|
||||
Используется в `services.operations.creation.create_key_on_cluster` после
|
||||
успешного создания ключа.
|
||||
"""
|
||||
await session.execute(
|
||||
update(User).where(User.tg_id == tg_id, User.trial.in_([0, -1])).values(trial=1)
|
||||
)
|
||||
await session.execute(update(User).where(User.tg_id == tg_id, User.trial.in_([0, -1])).values(trial=1))
|
||||
|
||||
|
||||
async def update_trial(session: AsyncSession, legacy_user_ref: int, status: int):
|
||||
@@ -354,9 +350,7 @@ async def delete_user_data(session: AsyncSession, legacy_user_ref: int):
|
||||
await session.execute(delete(WebPushSubscription).where(WebPushSubscription.user_id == uid))
|
||||
await session.execute(delete(WebNotification).where(WebNotification.user_id == uid))
|
||||
await session.execute(
|
||||
update(ScheduledBroadcast)
|
||||
.where(ScheduledBroadcast.created_by_user_id == uid)
|
||||
.values(created_by_user_id=None)
|
||||
update(ScheduledBroadcast).where(ScheduledBroadcast.created_by_user_id == uid).values(created_by_user_id=None)
|
||||
)
|
||||
|
||||
await session.execute(delete(User).where(User.id == uid))
|
||||
|
||||
@@ -17,49 +17,51 @@ async def upsert_push_subscription(
|
||||
keys_json: dict,
|
||||
) -> WebPushSubscription:
|
||||
"""Upsert push subscription by endpoint (unique)."""
|
||||
stmt = pg_insert(WebPushSubscription).values(
|
||||
user_id=user_id,
|
||||
identity_id=identity_id,
|
||||
endpoint=endpoint,
|
||||
keys_json=keys_json,
|
||||
created_at=datetime.now(UTC),
|
||||
).on_conflict_do_update(
|
||||
index_elements=["endpoint"],
|
||||
set_={
|
||||
"user_id": user_id,
|
||||
"identity_id": identity_id,
|
||||
"keys_json": keys_json,
|
||||
"created_at": datetime.now(UTC),
|
||||
},
|
||||
).returning(WebPushSubscription)
|
||||
stmt = (
|
||||
pg_insert(WebPushSubscription)
|
||||
.values(
|
||||
user_id=user_id,
|
||||
identity_id=identity_id,
|
||||
endpoint=endpoint,
|
||||
keys_json=keys_json,
|
||||
created_at=datetime.now(UTC),
|
||||
)
|
||||
.on_conflict_do_update(
|
||||
index_elements=["endpoint"],
|
||||
set_={
|
||||
"user_id": user_id,
|
||||
"identity_id": identity_id,
|
||||
"keys_json": keys_json,
|
||||
"created_at": datetime.now(UTC),
|
||||
},
|
||||
)
|
||||
.returning(WebPushSubscription)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def get_push_subscriptions_by_user(
|
||||
session: AsyncSession, user_id: int,
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
) -> list[WebPushSubscription]:
|
||||
result = await session.execute(
|
||||
select(WebPushSubscription).where(WebPushSubscription.user_id == user_id)
|
||||
)
|
||||
result = await session.execute(select(WebPushSubscription).where(WebPushSubscription.user_id == user_id))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_push_subscriptions_by_identity(
|
||||
session: AsyncSession, identity_id: str,
|
||||
session: AsyncSession,
|
||||
identity_id: str,
|
||||
) -> list[WebPushSubscription]:
|
||||
result = await session.execute(
|
||||
select(WebPushSubscription).where(WebPushSubscription.identity_id == identity_id)
|
||||
)
|
||||
result = await session.execute(select(WebPushSubscription).where(WebPushSubscription.identity_id == identity_id))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def delete_push_subscription_by_endpoint(
|
||||
session: AsyncSession, endpoint: str,
|
||||
session: AsyncSession,
|
||||
endpoint: str,
|
||||
) -> None:
|
||||
await session.execute(
|
||||
delete(WebPushSubscription).where(WebPushSubscription.endpoint == endpoint)
|
||||
)
|
||||
await session.execute(delete(WebPushSubscription).where(WebPushSubscription.endpoint == endpoint))
|
||||
|
||||
|
||||
async def get_notifications_for_identity(
|
||||
@@ -79,7 +81,8 @@ async def get_notifications_for_identity(
|
||||
|
||||
|
||||
async def count_unread_for_identity(
|
||||
session: AsyncSession, identity_id: str,
|
||||
session: AsyncSession,
|
||||
identity_id: str,
|
||||
) -> int:
|
||||
result = await session.execute(
|
||||
select(func.count())
|
||||
@@ -93,7 +96,8 @@ async def count_unread_for_identity(
|
||||
|
||||
|
||||
async def mark_all_read_for_identity(
|
||||
session: AsyncSession, identity_id: str,
|
||||
session: AsyncSession,
|
||||
identity_id: str,
|
||||
) -> int:
|
||||
result = await session.execute(
|
||||
update(WebNotification)
|
||||
@@ -107,12 +111,11 @@ async def mark_all_read_for_identity(
|
||||
|
||||
|
||||
async def resolve_identity_id_by_tg_id(
|
||||
session: AsyncSession, tg_id: int,
|
||||
session: AsyncSession,
|
||||
tg_id: int,
|
||||
) -> str | None:
|
||||
"""Resolve identity_id from user's tg_id."""
|
||||
result = await session.execute(
|
||||
select(User.identity_id).where(User.tg_id == tg_id)
|
||||
)
|
||||
result = await session.execute(select(User.identity_id).where(User.tg_id == tg_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -153,6 +156,7 @@ def _render_template(template: str, **kwargs: object) -> str:
|
||||
def _get_web_config_str(key: str, default: str) -> str:
|
||||
try:
|
||||
from core.settings.web_config import WEB_CONFIG
|
||||
|
||||
val = WEB_CONFIG.get(key)
|
||||
return str(val).strip() if val else default
|
||||
except Exception:
|
||||
@@ -209,16 +213,13 @@ async def notify_web(
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from services.web_push import push_enabled, send_push_to_many
|
||||
|
||||
if push_enabled():
|
||||
subs = await get_push_subscriptions_by_identity(session, identity_id)
|
||||
if subs:
|
||||
sub_infos = [
|
||||
{"endpoint": s.endpoint, "keys": s.keys_json}
|
||||
for s in subs
|
||||
]
|
||||
sub_infos = [{"endpoint": s.endpoint, "keys": s.keys_json} for s in subs]
|
||||
sent = await send_push_to_many(
|
||||
sub_infos,
|
||||
title=resolved_title,
|
||||
|
||||
Reference in New Issue
Block a user