diff --git a/app/calculator.py b/app/calculator.py index 8e09e67..d56ac80 100644 --- a/app/calculator.py +++ b/app/calculator.py @@ -1,9 +1,7 @@ from __future__ import annotations import math -import os from typing import TYPE_CHECKING -import zipfile from app.config import settings from app.log import logger @@ -11,10 +9,9 @@ from app.models.beatmap import BeatmapAttributes from app.models.mods import APIMod from app.models.score import GameMode -import httpx from osupyparser import OsuFile from osupyparser.osu.objects import Slider -from sqlmodel import Session, create_engine, select +from sqlmodel import col, exists, select from sqlmodel.ext.asyncio.session import AsyncSession try: @@ -63,10 +60,25 @@ def calculate_beatmap_attribute( ) -def calculate_pp( - score: "Score", - beatmap: str, -) -> float: +async def calculate_pp(score: "Score", beatmap: str, session: AsyncSession) -> float: + from app.database.beatmap import BannedBeatmaps + + if settings.suspicious_score_check: + beatmap_banned = ( + await session.exec( + select(exists()).where( + col(BannedBeatmaps.beatmap_id) == score.beatmap_id + ) + ) + ).first() + if beatmap_banned: + return 0 + is_suspicious = is_suspicious_beatmap(beatmap) + if is_suspicious: + session.add(BannedBeatmaps(beatmap_id=score.beatmap_id)) + logger.warning(f"Beatmap {score.beatmap_id} is suspicious, banned") + return 0 + map = rosu.Beatmap(content=beatmap) map.convert(score.gamemode.to_rosu(), score.mods) # pyright: ignore[reportArgumentType] perf = rosu.Performance( @@ -86,21 +98,10 @@ def calculate_pp( ) attrs = perf.calculate(map) pp = attrs.pp - engine = create_engine(settings.database_url) - from app.database.beatmap import BannedBeatmaps - beatmap_banned = False - with Session(engine) as session: - beatmap_id = session.exec( - select(BannedBeatmaps).where(BannedBeatmaps.beatmap_id == score.beatmap_id) - ).first() - if beatmap_id: - beatmap_banned = True # mrekk bp1: 2048pp; ppy-sb top1 rxbp1: 2198pp if settings.suspicious_score_check and ( - beatmap_banned - or (attrs.difficulty.stars > 25 and score.accuracy < 0.8) - or pp > 2300 + (attrs.difficulty.stars > 25 and score.accuracy < 0.8) or pp > 2300 ): logger.warning( f"User {score.user_id} played {score.beatmap_id} with {pp=} " @@ -237,32 +238,6 @@ def calculate_score_to_level(total_score: int) -> float: 99999999999, 99999999999, 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, - 99999999999, ] remaining_score = total_score @@ -291,48 +266,13 @@ def calculate_weighted_acc(acc: float, index: int) -> float: return calculate_pp_weight(index) * acc if acc > 0 else 0.0 -async def get_suspscious_beatmap(beatmapset_id: int, session: AsyncSession): - url = ( - f"https://txy1.sayobot.cn/beatmaps/download/novideo/{beatmapset_id}?server=auto" - ) - async with httpx.AsyncClient() as client: - resp = await client.get(url) - if resp.status_code == 200: - import aiofiles - - async with aiofiles.open(f"temp_beatmaps/{beatmapset_id}.osz", "wb") as f: - await f.write(resp.content) - with zipfile.ZipFile(f"temp_beatmaps/{beatmapset_id}.osz", "r") as beatmap_ref: - beatmap_ref.extractall(f"temp_beatmaps/{beatmapset_id}") - os.remove(f"temp_beatmaps/{beatmapset_id}.osz") - all_osu_files = [] - for root, dirs, files in os.walk(f"temp_beatmaps/{beatmapset_id}"): - for name in files: - if name.endswith(".osu"): - all_osu_files.append(os.path.join(root, name)) - for file in all_osu_files: - osufile = OsuFile(file).parse_file() - for obj in osufile.hit_objects: - if obj.pos.x < 0 or obj.pos.y < 0 or obj.pos.x > 512 or obj.pos.y > 384: - # 延迟导入以解决循环导入问题 - from app.database.beatmap import BannedBeatmaps - - session.add( - BannedBeatmaps(id=osufile.beatmap_id, beatmap_id=osufile.beatmap_id) - ) - break - if type(obj) is Slider: - for point in obj.points: - if point.x < 0 or point.y < 0 or point.x > 512 or point.y > 384: - # 延迟导入以解决循环导入问题 - from app.database.beatmap import BannedBeatmaps - - session.add( - BannedBeatmaps( - id=osufile.beatmap_id, beatmap_id=osufile.beatmap_id - ) - ) - break - os.remove(file) - os.remove(f"temp_beatmaps/{beatmapset_id}") - return None +def is_suspicious_beatmap(content: str) -> bool: + osufile = OsuFile(content=content.encode("utf-8-sig")).parse_file() + for obj in osufile.hit_objects: + if obj.pos.x < 0 or obj.pos.y < 0 or obj.pos.x > 512 or obj.pos.y > 384: + return True + if isinstance(obj, Slider): + for point in obj.points: + if point.x < 0 or point.y < 0 or point.x > 512 or point.y > 384: + return True + return False diff --git a/app/database/beatmap.py b/app/database/beatmap.py index cc80b6d..36ca577 100644 --- a/app/database/beatmap.py +++ b/app/database/beatmap.py @@ -3,6 +3,7 @@ from datetime import datetime import hashlib from typing import TYPE_CHECKING +from app.calculator import calculate_beatmap_attribute from app.config import settings from app.models.beatmap import BeatmapAttributes, BeatmapRankStatus from app.models.mods import APIMod @@ -203,7 +204,7 @@ class BeatmapResp(BeatmapBase): class BannedBeatmaps(SQLModel, table=True): __tablename__ = "banned_beatmaps" # pyright: ignore[reportAssignmentType] - id: int = Field(primary_key=True, index=True) + id: int | None = Field(primary_key=True, index=True, default=None) beatmap_id: int = Field(index=True) @@ -221,9 +222,6 @@ async def calculate_beatmap_attributes( if await redis.exists(key): return BeatmapAttributes.model_validate_json(await redis.get(key)) # pyright: ignore[reportArgumentType] resp = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) - # 延迟导入以解决循环导入问题 - from app.calculator import calculate_beatmap_attribute - attr = await asyncio.get_event_loop().run_in_executor( None, calculate_beatmap_attribute, resp, ruleset, mods_ ) diff --git a/app/database/beatmapset.py b/app/database/beatmapset.py index 711ab33..6a5ca2b 100644 --- a/app/database/beatmapset.py +++ b/app/database/beatmapset.py @@ -183,10 +183,6 @@ class Beatmapset(AsyncAttrs, BeatmapsetBase, table=True): if not beatmapset: resp = await fetcher.get_beatmapset(sid) beatmapset = await cls.from_resp(session, resp) - # 检查可疑谱面 - from app.calculator import get_suspscious_beatmap - - await get_suspscious_beatmap(sid, session) return beatmapset diff --git a/app/database/score.py b/app/database/score.py index 0de6647..90b764f 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -1,4 +1,3 @@ -import asyncio from collections.abc import Sequence from datetime import UTC, date, datetime import json @@ -726,9 +725,7 @@ async def process_score( ) if can_get_pp: beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) - pp = await asyncio.get_event_loop().run_in_executor( - None, calculate_pp, score, beatmap_raw - ) + pp = await calculate_pp(score, beatmap_raw, session) score.pp = pp session.add(score) user_id = user.id diff --git a/app/service/pp_recalculate.py b/app/service/pp_recalculate.py index 4bb9a68..7f438c7 100644 --- a/app/service/pp_recalculate.py +++ b/app/service/pp_recalculate.py @@ -85,9 +85,7 @@ async def _recalculate_pp( break try: beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) - pp = await asyncio.get_event_loop().run_in_executor( - None, calculate_pp, score, beatmap_raw - ) + pp = await calculate_pp(score, beatmap_raw, session) score.pp = pp if score.beatmap_id not in prev or prev[score.beatmap_id].pp < pp: best_score = PPBestScore( diff --git a/pyproject.toml b/pyproject.toml index d828667..bc1c652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,10 +96,12 @@ reportIncompatibleVariableOverride = false [tool.uv.workspace] members = [ "packages/msgpack_lazer_api", + "packages/osupyparser", ] [tool.uv.sources] msgpack-lazer-api = { workspace = true } +osupyparser = { git = "https://github.com/MingxuanGame/osupyparser.git" } [tool.uv] cache-keys = [{file = "pyproject.toml"}, {file = "packages/msgpack_lazer_api/Cargo.toml"}, {file = "**/*.rs"}] diff --git a/uv.lock b/uv.lock index 8525d55..5466c65 100644 --- a/uv.lock +++ b/uv.lock @@ -858,7 +858,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "msgpack-lazer-api", editable = "packages/msgpack_lazer_api" }, - { name = "osupyparser", specifier = ">=1.0.7" }, + { name = "osupyparser", git = "https://github.com/MingxuanGame/osupyparser.git" }, { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.5.0" }, @@ -883,9 +883,8 @@ dev = [ [[package]] name = "osupyparser" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6b/7f567c2acd1f2028603353da40ad7411bb47754994552d3f0c4cfa6703f9/OsuPyParser-1.0.7.tar.gz", hash = "sha256:67f530c31dd5c288c8fff8f583c899c673536681f8cc3699d0afc5e4d8c2b1ff", size = 9095, upload-time = "2021-08-29T23:48:22.752Z" } +version = "1.0.8" +source = { git = "https://github.com/MingxuanGame/osupyparser.git#e41ec1db87ab64531897127a44b86351c21322bd" } [[package]] name = "passlib"