mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
175 lines
7.6 KiB
Python
175 lines
7.6 KiB
Python
from collections import defaultdict
|
|
from collections.abc import Callable
|
|
from datetime import datetime, timedelta, timezone
|
|
from hashlib import sha512
|
|
from math import floor
|
|
from statistics import mean
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from aiofiles import open
|
|
from nonebot import get_driver
|
|
from nonebot.compat import model_dump
|
|
from nonebot.matcher import Matcher
|
|
from nonebot.utils import run_sync
|
|
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
|
from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyped]
|
|
from nonebot_plugin_orm import get_session
|
|
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
|
|
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
|
|
from sqlalchemy import func, select
|
|
from zstandard import ZstdCompressor
|
|
|
|
from ...db import trigger
|
|
from ...utils.exception import RequestError
|
|
from ...utils.metrics import get_metrics
|
|
from ...utils.retry import retry
|
|
from . import alc
|
|
from .api.schemas.base import FailedModel
|
|
from .api.schemas.tetra_league import ValidUser
|
|
from .api.schemas.user import User
|
|
from .api.tetra_league import full_export
|
|
from .api.typing import Rank
|
|
from .constant import GAME_TYPE, RANK_PERCENTILE
|
|
from .models import IORank
|
|
|
|
UTC = timezone.utc
|
|
|
|
driver = get_driver()
|
|
|
|
|
|
@alc.assign('TETRIO.rank')
|
|
async def _(matcher: Matcher, rank: Rank, event_session: EventSession):
|
|
async with trigger(
|
|
session_persist_id=await get_session_persist_id(event_session),
|
|
game_platform=GAME_TYPE,
|
|
command_type='rank',
|
|
command_args=[],
|
|
):
|
|
if rank == 'z':
|
|
await matcher.finish('暂不支持查询未知段位')
|
|
async with get_session() as session:
|
|
latest_data = (
|
|
await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(1))
|
|
).one()
|
|
compare_data = (
|
|
await session.scalars(
|
|
select(IORank)
|
|
.where(IORank.rank == rank)
|
|
.order_by(
|
|
func.abs(
|
|
func.julianday(IORank.update_time)
|
|
- func.julianday(latest_data.update_time - timedelta(hours=24))
|
|
)
|
|
)
|
|
.limit(1)
|
|
)
|
|
).one()
|
|
message = ''
|
|
if (datetime.now(UTC) - latest_data.update_time.replace(tzinfo=UTC)) > timedelta(hours=7):
|
|
message += 'Warning: 数据超过7小时未更新, 请联系Bot主人查看后台\n'
|
|
message += f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
|
|
if compare_data.id != latest_data.id:
|
|
message += f'对比 {(latest_data.update_time-compare_data.update_time).total_seconds()/3600:.2f} 小时前趋势: {f"↑{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"↓{-difference:.2f}" if difference < 0 else "→"}'
|
|
else:
|
|
message += '暂无对比数据'
|
|
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
|
|
low_pps = get_metrics(pps=latest_data.low_pps[1])
|
|
low_vs = get_metrics(vs=latest_data.low_vs[1])
|
|
max_pps = get_metrics(pps=latest_data.high_pps[1])
|
|
max_vs = get_metrics(vs=latest_data.high_vs[1])
|
|
message += (
|
|
'\n'
|
|
'平均数据:\n'
|
|
f"L'PM: {avg.lpm} ( {avg.pps} pps )\n"
|
|
f'APM: {avg.apm} ( x{avg.apl} )\n'
|
|
f'ADPM: {avg.adpm} ( x{avg.adpl} ) ( {avg.vs}vs )\n'
|
|
'\n'
|
|
'最低数据:\n'
|
|
f"L'PM: {low_pps.lpm} ( {low_pps.pps} pps ) By: {latest_data.low_pps[0]['name'].upper()}\n"
|
|
f'APM: {latest_data.low_apm[1]} By: {latest_data.low_apm[0]["name"].upper()}\n'
|
|
f'ADPM: {low_vs.adpm} ( {low_vs.vs}vs ) By: {latest_data.low_vs[0]["name"].upper()}\n'
|
|
'\n'
|
|
'最高数据:\n'
|
|
f"L'PM: {max_pps.lpm} ( {max_pps.pps} pps ) By: {latest_data.high_pps[0]['name'].upper()}\n"
|
|
f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n'
|
|
f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n'
|
|
'\n'
|
|
f'数据更新时间: {latest_data.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
|
)
|
|
await matcher.finish(message)
|
|
|
|
|
|
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
|
|
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
|
|
async def get_tetra_league_data() -> None:
|
|
league, original = await full_export(with_original=True)
|
|
if isinstance(league, FailedModel):
|
|
msg = f'排行榜数据请求错误:\n{league.error}'
|
|
raise RequestError(msg)
|
|
|
|
def pps(user: ValidUser) -> float:
|
|
return user.league.pps
|
|
|
|
def apm(user: ValidUser) -> float:
|
|
return user.league.apm
|
|
|
|
def vs(user: ValidUser) -> float:
|
|
return user.league.vs
|
|
|
|
def _min(users: list[ValidUser], field: Callable[[ValidUser], float]) -> ValidUser:
|
|
return min(users, key=field)
|
|
|
|
def _max(users: list[ValidUser], field: Callable[[ValidUser], float]) -> ValidUser:
|
|
return max(users, key=field)
|
|
|
|
def build_extremes_data(
|
|
users: list[ValidUser],
|
|
field: Callable[[ValidUser], float],
|
|
sort: Callable[[list[ValidUser], Callable[[ValidUser], float]], ValidUser],
|
|
) -> tuple[dict[str, str], float]:
|
|
user = sort(users, field)
|
|
return model_dump(User(ID=user.id, name=user.username)), field(user)
|
|
|
|
data_hash: str | None = await run_sync((await run_sync(sha512)(original)).hexdigest)()
|
|
async with open(get_data_file('nonebot_plugin_tetris_stats', f'{data_hash}.json.zst'), mode='wb') as file:
|
|
await file.write(await run_sync(ZstdCompressor(level=12, threads=-1).compress)(original))
|
|
|
|
users = [i for i in league.data.users if isinstance(i, ValidUser)]
|
|
rank_to_users: defaultdict[Rank, list[ValidUser]] = defaultdict(list)
|
|
for i in users:
|
|
rank_to_users[i.league.rank].append(i)
|
|
rank_info: list[IORank] = []
|
|
for rank, percentile in RANK_PERCENTILE.items():
|
|
offset = floor((percentile / 100) * len(users)) - 1
|
|
tr_line = users[offset].league.rating
|
|
rank_users = rank_to_users[rank]
|
|
rank_info.append(
|
|
IORank(
|
|
rank=rank,
|
|
tr_line=tr_line,
|
|
player_count=len(rank_users),
|
|
low_pps=(build_extremes_data(rank_users, pps, _min)),
|
|
low_apm=(build_extremes_data(rank_users, apm, _min)),
|
|
low_vs=(build_extremes_data(rank_users, vs, _min)),
|
|
avg_pps=mean({i.league.pps for i in rank_users}),
|
|
avg_apm=mean({i.league.apm for i in rank_users}),
|
|
avg_vs=mean({i.league.vs for i in rank_users}),
|
|
high_pps=(build_extremes_data(rank_users, pps, _max)),
|
|
high_apm=(build_extremes_data(rank_users, apm, _max)),
|
|
high_vs=(build_extremes_data(rank_users, vs, _max)),
|
|
update_time=league.cache.cached_at,
|
|
file_hash=data_hash,
|
|
)
|
|
)
|
|
async with get_session() as session:
|
|
session.add_all(rank_info)
|
|
await session.commit()
|
|
|
|
|
|
@driver.on_startup
|
|
async def _() -> None:
|
|
async with get_session() as session:
|
|
latest_time = await session.scalar(select(IORank.update_time).order_by(IORank.id.desc()).limit(1))
|
|
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
|
await get_tetra_league_data()
|