Files
g0v0-server/app/tasks/cache.py
MingxuanGame febc1d761f feat(user): implement user restrictions
## APIs Restricted for Restricted Users

A restricted user is blocked from performing the following actions, and will typically receive a `403 Forbidden` error:

*   **Chat & Notifications:**
    *   Sending any chat messages (public or private).
    *   Joining or leaving chat channels.
    *   Creating new PM channels.
*   **User Profile & Content:**
    *   Uploading a new avatar.
    *   Uploading a new profile cover image.
    *   Changing their username.
    *   Updating their userpage content.
*   **Scores & Gameplay:**
    *   Submitting scores in multiplayer rooms.
    *   Deleting their own scores (to prevent hiding evidence of cheating).
*   **Beatmaps:**
    *   Rating beatmaps.
    *   Taging beatmaps.
*   **Relationship:**
    *   Adding friends or blocking users.
    *   Removing friends or unblocking users.
*   **Teams:**
    *   Creating, updating, or deleting a team.
    *   Requesting to join a team.
    *   Handling join requests for a team they manage.
    *   Kicking a member from a team they manage.
*   **Multiplayer:**
    *   Creating or deleting multiplayer rooms.
    *   Joining or leaving multiplayer rooms.

## What is Invisible to Normal Users

*   **Leaderboards:**
    *   Beatmap leaderboards.
    *   Multiplayer (playlist) room leaderboards.
*   **User Search/Lists:**
    *   Restricted users will not appear in the results of the `/api/v2/users` endpoint.
    *   They will not appear in the list of a team's members.
*   **Relationship:**
    *   They will not appear in a user's friend list (`/friends`).
*   **Profile & History:**
    *   Attempting to view a restricted user's profile, events, kudosu history, or score history will result in a `404 Not Found` error, effectively making their profile invisible (unless the user viewing the profile is the restricted user themselves).
*   **Chat:**
    *   Normal users cannot start a new PM with a restricted user (they will get a `404 Not Found` error).
*   **Ranking:**
    *   Restricted users are excluded from any rankings.

### How to Restrict a User

Insert into `user_account_history` with `type=restriction`.

```sql
-- length is in seconds
INSERT INTO user_account_history (`description`, `length`, `permanent`, `timestamp`, `type`, `user_id`) VALUE ('some description', 86400, 0, '2025-10-05 01:00:00', 'RESTRICTION', 1);
```

---

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 11:10:25 +08:00

253 lines
7.7 KiB
Python

"""缓存相关的 APScheduler 任务入口。"""
import asyncio
from datetime import UTC, timedelta
from typing import Final
from app.config import settings
from app.database.score import Score
from app.database.user import User
from app.dependencies.database import get_redis, with_db
from app.dependencies.fetcher import get_fetcher
from app.dependencies.scheduler import get_scheduler
from app.log import logger
from app.service.ranking_cache_service import schedule_ranking_refresh_task
from app.service.user_cache_service import get_user_cache_service
from app.utils import utcnow
from apscheduler.jobstores.base import JobLookupError
from apscheduler.triggers.interval import IntervalTrigger
from sqlmodel import col, func, select
CACHE_JOB_IDS: Final[dict[str, str]] = {
"beatmap_warmup": "cache:beatmap:warmup",
"ranking_refresh": "cache:ranking:refresh",
"user_preload": "cache:user:preload",
"user_cleanup": "cache:user:cleanup",
}
async def warmup_cache() -> None:
"""执行缓存预热"""
try:
logger.info("Starting beatmap cache warmup...")
fetcher = await get_fetcher()
redis = get_redis()
await fetcher.warmup_homepage_cache(redis)
logger.info("Beatmap cache warmup completed successfully")
except Exception as e:
logger.error(f"Beatmap cache warmup failed: {e}")
async def refresh_ranking_cache() -> None:
"""刷新排行榜缓存"""
try:
logger.info("Starting ranking cache refresh...")
redis = get_redis()
from app.dependencies.database import with_db
async with with_db() as session:
await schedule_ranking_refresh_task(session, redis)
logger.info("Ranking cache refresh completed successfully")
except Exception as e:
logger.error(f"Ranking cache refresh failed: {e}")
async def schedule_user_cache_preload_task() -> None:
"""定时用户缓存预加载任务"""
if not settings.enable_user_cache_preload:
return
try:
logger.info("Starting user cache preload task...")
redis = get_redis()
cache_service = get_user_cache_service(redis)
from app.dependencies.database import with_db
async with with_db() as session:
recent_time = utcnow() - timedelta(hours=24)
score_count = func.count().label("score_count")
active_user_ids = (
await session.exec(
select(Score.user_id, score_count)
.where(col(Score.ended_at) >= recent_time)
.group_by(col(Score.user_id))
.order_by(score_count.desc())
.limit(settings.user_cache_max_preload_users)
)
).all()
if active_user_ids:
user_ids = [row[0] for row in active_user_ids]
await cache_service.preload_user_cache(session, user_ids)
logger.info(f"Preloaded cache for {len(user_ids)} active users")
else:
logger.info("No active users found for cache preload")
logger.info("User cache preload task completed successfully")
except Exception as e:
logger.error(f"User cache preload task failed: {e}")
async def schedule_user_cache_warmup_task() -> None:
"""定时用户缓存预热任务 - 预加载排行榜前100用户"""
try:
logger.info("Starting user cache warmup task...")
redis = get_redis()
cache_service = get_user_cache_service(redis)
async with with_db() as session:
from app.database.statistics import UserStatistics
from app.models.score import GameMode
for mode in GameMode:
try:
top_users = (
await session.exec(
select(UserStatistics.user_id)
.where(
UserStatistics.mode == mode,
~User.is_restricted_query(col(UserStatistics.user_id)),
)
.order_by(col(UserStatistics.pp).desc())
.limit(100)
)
).all()
if top_users:
user_ids = list(top_users)
await cache_service.preload_user_cache(session, user_ids)
logger.info(f"Warmed cache for top 100 users in {mode}")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Failed to warm cache for {mode}: {e}")
continue
logger.info("User cache warmup task completed successfully")
except Exception as e:
logger.error(f"User cache warmup task failed: {e}")
async def schedule_user_cache_cleanup_task() -> None:
"""定时用户缓存清理任务"""
try:
logger.info("Starting user cache cleanup task...")
redis = get_redis()
cache_service = get_user_cache_service(redis)
stats = await cache_service.get_cache_stats()
logger.info(f"User cache stats: {stats}")
logger.info("User cache cleanup task completed successfully")
except Exception as e:
logger.error(f"User cache cleanup task failed: {e}")
async def warmup_user_cache() -> None:
"""用户缓存预热"""
try:
await schedule_user_cache_warmup_task()
except Exception as e:
logger.error(f"User cache warmup failed: {e}")
async def preload_user_cache() -> None:
"""用户缓存预加载"""
try:
await schedule_user_cache_preload_task()
except Exception as e:
logger.error(f"User cache preload failed: {e}")
async def cleanup_user_cache() -> None:
"""用户缓存清理"""
try:
await schedule_user_cache_cleanup_task()
except Exception as e:
logger.error(f"User cache cleanup failed: {e}")
def register_cache_jobs() -> None:
"""注册缓存相关 APScheduler 任务"""
scheduler = get_scheduler()
scheduler.add_job(
warmup_cache,
trigger=IntervalTrigger(minutes=30, timezone=UTC),
id=CACHE_JOB_IDS["beatmap_warmup"],
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=300,
)
scheduler.add_job(
refresh_ranking_cache,
trigger=IntervalTrigger(
minutes=settings.ranking_cache_refresh_interval_minutes,
timezone=UTC,
),
id=CACHE_JOB_IDS["ranking_refresh"],
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=300,
)
scheduler.add_job(
preload_user_cache,
trigger=IntervalTrigger(minutes=15, timezone=UTC),
id=CACHE_JOB_IDS["user_preload"],
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=300,
)
scheduler.add_job(
cleanup_user_cache,
trigger=IntervalTrigger(hours=1, timezone=UTC),
id=CACHE_JOB_IDS["user_cleanup"],
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=300,
)
logger.info("Registered cache APScheduler jobs")
async def start_cache_tasks() -> None:
"""注册 APScheduler 任务并执行启动时任务"""
register_cache_jobs()
logger.info("Cache APScheduler jobs registered; running initial tasks")
async def stop_cache_tasks() -> None:
"""移除 APScheduler 任务"""
scheduler = get_scheduler()
for job_id in CACHE_JOB_IDS.values():
try:
scheduler.remove_job(job_id)
except JobLookupError:
continue
logger.info("Cache APScheduler jobs removed")