Tune users rating
This commit is contained in:
@@ -315,13 +315,16 @@ async def show_user_ratings_handler(
|
|||||||
e_get_me)
|
e_get_me)
|
||||||
|
|
||||||
traffic_top = await user_dal.get_top_users_by_traffic_used(session, limit=top_limit)
|
traffic_top = await user_dal.get_top_users_by_traffic_used(session, limit=top_limit)
|
||||||
|
lifetime_traffic_top = await user_dal.get_top_users_by_lifetime_traffic_used(
|
||||||
|
session, limit=top_limit
|
||||||
|
)
|
||||||
invited_top = await user_dal.get_top_users_by_referrals_count(session, limit=top_limit)
|
invited_top = await user_dal.get_top_users_by_referrals_count(session, limit=top_limit)
|
||||||
revenue_top = await user_dal.get_top_users_by_referral_revenue(session, limit=top_limit)
|
revenue_top = await user_dal.get_top_users_by_referral_revenue(session, limit=top_limit)
|
||||||
|
|
||||||
text_parts: List[str] = [
|
text_parts: List[str] = [
|
||||||
_("admin_user_ratings_header", top_limit=top_limit),
|
_("admin_user_ratings_header", top_limit=top_limit),
|
||||||
"",
|
"",
|
||||||
f"<b>{_('admin_user_ratings_traffic_title')}</b>",
|
f"<b>{_('admin_user_ratings_traffic_month_title')}</b>",
|
||||||
]
|
]
|
||||||
|
|
||||||
if traffic_top:
|
if traffic_top:
|
||||||
@@ -338,6 +341,21 @@ async def show_user_ratings_handler(
|
|||||||
else:
|
else:
|
||||||
text_parts.append(_("admin_user_ratings_empty"))
|
text_parts.append(_("admin_user_ratings_empty"))
|
||||||
|
|
||||||
|
text_parts.extend(["", f"<b>{_('admin_user_ratings_traffic_lifetime_title')}</b>"])
|
||||||
|
if lifetime_traffic_top:
|
||||||
|
for idx, row in enumerate(lifetime_traffic_top, start=1):
|
||||||
|
traffic_gb = float(row.get("lifetime_used_traffic_bytes") or 0) / (1024**3)
|
||||||
|
text_parts.append(
|
||||||
|
_(
|
||||||
|
"admin_user_ratings_traffic_item",
|
||||||
|
rank=idx,
|
||||||
|
user=_format_rating_user_label(row, bot_username),
|
||||||
|
traffic_gb=f"{traffic_gb:.2f}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text_parts.append(_("admin_user_ratings_empty"))
|
||||||
|
|
||||||
text_parts.extend(["", f"<b>{_('admin_user_ratings_invited_title')}</b>"])
|
text_parts.extend(["", f"<b>{_('admin_user_ratings_invited_title')}</b>"])
|
||||||
if invited_top:
|
if invited_top:
|
||||||
for idx, row in enumerate(invited_top, start=1):
|
for idx, row in enumerate(invited_top, start=1):
|
||||||
|
|||||||
@@ -18,6 +18,24 @@ from bot.middlewares.i18n import JsonI18n
|
|||||||
router = Router(name="admin_sync_router")
|
router = Router(name="admin_sync_router")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_lifetime_used_traffic_bytes(panel_user_data: dict) -> Optional[int]:
|
||||||
|
user_traffic = panel_user_data.get("userTraffic") or {}
|
||||||
|
raw_value = (
|
||||||
|
user_traffic.get("lifetimeUsedTrafficBytes")
|
||||||
|
if isinstance(user_traffic, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if raw_value is None:
|
||||||
|
raw_value = panel_user_data.get("lifetimeUsedTrafficBytes")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if raw_value is None:
|
||||||
|
return None
|
||||||
|
return int(raw_value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def perform_sync(
|
async def perform_sync(
|
||||||
panel_service: PanelApiService,
|
panel_service: PanelApiService,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
@@ -176,6 +194,14 @@ async def perform_sync(
|
|||||||
f"Updated panel UUID for user {actual_user_id}: {panel_uuid}"
|
f"Updated panel UUID for user {actual_user_id}: {panel_uuid}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lifetime_used = _extract_lifetime_used_traffic_bytes(panel_user_dict)
|
||||||
|
if (
|
||||||
|
lifetime_used is not None
|
||||||
|
and existing_user.lifetime_used_traffic_bytes != lifetime_used
|
||||||
|
):
|
||||||
|
existing_user.lifetime_used_traffic_bytes = lifetime_used
|
||||||
|
user_was_updated = True
|
||||||
|
|
||||||
# Ensure panel description contains Telegram fields
|
# Ensure panel description contains Telegram fields
|
||||||
try:
|
try:
|
||||||
if panel_uuid and existing_user:
|
if panel_uuid and existing_user:
|
||||||
|
|||||||
@@ -68,6 +68,20 @@ class SubscriptionService:
|
|||||||
strategy = traffic_stats.get("trafficLimitStrategy")
|
strategy = traffic_stats.get("trafficLimitStrategy")
|
||||||
return used, limit, strategy
|
return used, limit, strategy
|
||||||
|
|
||||||
|
def _extract_lifetime_used_traffic(
|
||||||
|
self, panel_user_data: Dict[str, Any]
|
||||||
|
) -> Optional[int]:
|
||||||
|
traffic_stats = panel_user_data.get("userTraffic") or {}
|
||||||
|
lifetime = traffic_stats.get("lifetimeUsedTrafficBytes")
|
||||||
|
if lifetime is None:
|
||||||
|
lifetime = panel_user_data.get("lifetimeUsedTrafficBytes")
|
||||||
|
try:
|
||||||
|
if lifetime is None:
|
||||||
|
return None
|
||||||
|
return int(lifetime)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
async def _notify_admin_panel_user_creation_failed(self, user_id: int):
|
async def _notify_admin_panel_user_creation_failed(self, user_id: int):
|
||||||
if not self.bot or not self.i18n or not self.settings.ADMIN_IDS:
|
if not self.bot or not self.i18n or not self.settings.ADMIN_IDS:
|
||||||
return
|
return
|
||||||
@@ -853,6 +867,17 @@ class SubscriptionService:
|
|||||||
await user_dal.update_user(session, user_id, {"panel_user_uuid": None})
|
await user_dal.update_user(session, user_id, {"panel_user_uuid": None})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
panel_lifetime_used = self._extract_lifetime_used_traffic(panel_user_data)
|
||||||
|
if (
|
||||||
|
panel_lifetime_used is not None
|
||||||
|
and db_user.lifetime_used_traffic_bytes != panel_lifetime_used
|
||||||
|
):
|
||||||
|
await user_dal.update_user(
|
||||||
|
session,
|
||||||
|
user_id,
|
||||||
|
{"lifetime_used_traffic_bytes": panel_lifetime_used},
|
||||||
|
)
|
||||||
|
|
||||||
if local_active_sub:
|
if local_active_sub:
|
||||||
update_payload_local = {}
|
update_payload_local = {}
|
||||||
panel_status = panel_user_data.get("status", "UNKNOWN").upper()
|
panel_status = panel_user_data.get("status", "UNKNOWN").upper()
|
||||||
|
|||||||
@@ -391,6 +391,31 @@ async def get_top_users_by_traffic_used(
|
|||||||
return [dict(row._mapping) for row in result]
|
return [dict(row._mapping) for row in result]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_top_users_by_lifetime_traffic_used(
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
limit: int = 10,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Return top users by lifetime used traffic from panel data."""
|
||||||
|
safe_limit = max(1, limit)
|
||||||
|
lifetime_used = func.coalesce(User.lifetime_used_traffic_bytes, 0)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
User.user_id,
|
||||||
|
User.username,
|
||||||
|
User.first_name,
|
||||||
|
lifetime_used.label("lifetime_used_traffic_bytes"),
|
||||||
|
)
|
||||||
|
.where(lifetime_used > 0)
|
||||||
|
.order_by(desc("lifetime_used_traffic_bytes"), User.user_id.asc())
|
||||||
|
.limit(safe_limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return [dict(row._mapping) for row in result]
|
||||||
|
|
||||||
|
|
||||||
async def get_top_users_by_referrals_count(
|
async def get_top_users_by_referrals_count(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -112,6 +112,19 @@ def _migration_0003_normalize_referral_codes(connection: Connection) -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _migration_0004_add_lifetime_used_traffic(connection: Connection) -> None:
|
||||||
|
inspector = inspect(connection)
|
||||||
|
columns: Set[str] = {col["name"] for col in inspector.get_columns("users")}
|
||||||
|
if "lifetime_used_traffic_bytes" in columns:
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE users ADD COLUMN lifetime_used_traffic_bytes BIGINT"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
MIGRATIONS: List[Migration] = [
|
MIGRATIONS: List[Migration] = [
|
||||||
Migration(
|
Migration(
|
||||||
id="0001_add_channel_subscription_fields",
|
id="0001_add_channel_subscription_fields",
|
||||||
@@ -128,6 +141,11 @@ MIGRATIONS: List[Migration] = [
|
|||||||
description="Normalize referral codes to uppercase for consistent lookups",
|
description="Normalize referral codes to uppercase for consistent lookups",
|
||||||
upgrade=_migration_0003_normalize_referral_codes,
|
upgrade=_migration_0003_normalize_referral_codes,
|
||||||
),
|
),
|
||||||
|
Migration(
|
||||||
|
id="0004_add_lifetime_used_traffic",
|
||||||
|
description="Store lifetime traffic usage for users",
|
||||||
|
upgrade=_migration_0004_add_lifetime_used_traffic,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class User(Base):
|
|||||||
referred_by_id = Column(BigInteger,
|
referred_by_id = Column(BigInteger,
|
||||||
ForeignKey("users.user_id"),
|
ForeignKey("users.user_id"),
|
||||||
nullable=True)
|
nullable=True)
|
||||||
|
lifetime_used_traffic_bytes = Column(BigInteger, nullable=True)
|
||||||
channel_subscription_verified = Column(Boolean, nullable=True)
|
channel_subscription_verified = Column(Boolean, nullable=True)
|
||||||
channel_subscription_checked_at = Column(DateTime(timezone=True),
|
channel_subscription_checked_at = Column(DateTime(timezone=True),
|
||||||
nullable=True)
|
nullable=True)
|
||||||
|
|||||||
@@ -136,6 +136,8 @@
|
|||||||
"back_to_stats_monitoring_button": "⬅️ To Statistics",
|
"back_to_stats_monitoring_button": "⬅️ To Statistics",
|
||||||
"admin_user_ratings_header": "🏆 <b>User Ratings (TOP {top_limit})</b>",
|
"admin_user_ratings_header": "🏆 <b>User Ratings (TOP {top_limit})</b>",
|
||||||
"admin_user_ratings_traffic_title": "📊 By used traffic",
|
"admin_user_ratings_traffic_title": "📊 By used traffic",
|
||||||
|
"admin_user_ratings_traffic_month_title": "📊 By used traffic (month)",
|
||||||
|
"admin_user_ratings_traffic_lifetime_title": "📊 By used traffic (lifetime)",
|
||||||
"admin_user_ratings_invited_title": "👥 By invited users",
|
"admin_user_ratings_invited_title": "👥 By invited users",
|
||||||
"admin_user_ratings_revenue_title": "💸 By referral revenue",
|
"admin_user_ratings_revenue_title": "💸 By referral revenue",
|
||||||
"admin_user_ratings_traffic_item": "{rank}. {user} — <b>{traffic_gb} GB</b>",
|
"admin_user_ratings_traffic_item": "{rank}. {user} — <b>{traffic_gb} GB</b>",
|
||||||
|
|||||||
@@ -136,6 +136,8 @@
|
|||||||
"back_to_stats_monitoring_button": "⬅️ К статистике",
|
"back_to_stats_monitoring_button": "⬅️ К статистике",
|
||||||
"admin_user_ratings_header": "🏆 <b>Рейтинг пользователей (ТОП {top_limit})</b>",
|
"admin_user_ratings_header": "🏆 <b>Рейтинг пользователей (ТОП {top_limit})</b>",
|
||||||
"admin_user_ratings_traffic_title": "📊 По использованному трафику",
|
"admin_user_ratings_traffic_title": "📊 По использованному трафику",
|
||||||
|
"admin_user_ratings_traffic_month_title": "📊 По использованному трафику за месяц",
|
||||||
|
"admin_user_ratings_traffic_lifetime_title": "📊 По использованному трафику за всё время",
|
||||||
"admin_user_ratings_invited_title": "👥 По количеству приглашенных",
|
"admin_user_ratings_invited_title": "👥 По количеству приглашенных",
|
||||||
"admin_user_ratings_revenue_title": "💸 По доходу с приглашенных",
|
"admin_user_ratings_revenue_title": "💸 По доходу с приглашенных",
|
||||||
"admin_user_ratings_traffic_item": "{rank}. {user} — <b>{traffic_gb} ГБ</b>",
|
"admin_user_ratings_traffic_item": "{rank}. {user} — <b>{traffic_gb} ГБ</b>",
|
||||||
|
|||||||
Reference in New Issue
Block a user