Skip to content

Commit

Permalink
Move Active Matches to new API
Browse files Browse the repository at this point in the history
  • Loading branch information
raimannma committed Feb 26, 2025
1 parent 5ab2913 commit 49b8925
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 239 deletions.
2 changes: 0 additions & 2 deletions deadlock_data_api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ class AppConfig:
hook0: HOOK0Config
steam_proxy: SteamProxyConfig | None
steam_api_key: str
discord_webhook_url: str | None
emergency_mode: bool
enforce_rate_limits: bool
demo_retention_days: int
Expand All @@ -152,7 +151,6 @@ def from_env(cls) -> "AppConfig":
hook0=HOOK0Config.from_env(),
steam_proxy=SteamProxyConfig.from_env(),
steam_api_key=os.environ.get("STEAM_API_KEY"),
discord_webhook_url=os.environ.get("DISCORD_WEBHOOK_URL"),
emergency_mode=os.environ.get("EMERGENCY_MODE") == "true",
enforce_rate_limits=os.environ.get("ENFORCE_RATE_LIMITS") == "true",
demo_retention_days=int(os.environ.get("DEMO_RETENTION_DAYS", 21)),
Expand Down
136 changes: 68 additions & 68 deletions deadlock_data_api/models/active_match.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
from pydantic import BaseModel, ConfigDict
from valveprotos_py.citadel_gcmessages_client_pb2 import CMsgDevMatchInfo


class ActiveMatchPlayer(BaseModel):
model_config = ConfigDict(populate_by_name=True)
account_id: int
team: int
abandoned: bool
hero_id: int


class ActiveMatch(BaseModel):
model_config = ConfigDict(populate_by_name=True)

start_time: int
winning_team: int
match_id: int
players: list[ActiveMatchPlayer]
lobby_id: int
net_worth_team_0: int
net_worth_team_1: int
game_mode_version: int
duration_s: int
spectators: int
open_spectator_slots: int
objectives_mask_team0: int
objectives_mask_team1: int
match_mode: int
game_mode: int
match_score: int
region_mode: int

@classmethod
def from_msg(cls, msg: CMsgDevMatchInfo) -> "ActiveMatch":
return cls(
start_time=msg.start_time,
winning_team=msg.winning_team,
match_id=msg.match_id,
players=[
ActiveMatchPlayer(
account_id=player.account_id,
team=player.team,
abandoned=player.abandoned,
hero_id=player.hero_id,
)
for player in msg.players
],
lobby_id=msg.lobby_id,
net_worth_team_0=msg.net_worth_team_0,
net_worth_team_1=msg.net_worth_team_1,
game_mode_version=msg.game_mode_version,
duration_s=msg.duration_s,
spectators=msg.spectators,
open_spectator_slots=msg.open_spectator_slots,
objectives_mask_team0=msg.objectives_mask_team0,
objectives_mask_team1=msg.objectives_mask_team1,
match_mode=msg.match_mode,
game_mode=msg.game_mode,
match_score=msg.match_score,
region_mode=msg.region_mode,
)


class APIActiveMatch(BaseModel):
model_config = ConfigDict(populate_by_name=True)

active_matches: list[ActiveMatch]
# from pydantic import BaseModel, ConfigDict
# from valveprotos_py.citadel_gcmessages_client_pb2 import CMsgDevMatchInfo
#
#
# class ActiveMatchPlayer(BaseModel):
# model_config = ConfigDict(populate_by_name=True)
# account_id: int
# team: int
# abandoned: bool
# hero_id: int
#
#
# class ActiveMatch(BaseModel):
# model_config = ConfigDict(populate_by_name=True)
#
# start_time: int
# winning_team: int
# match_id: int
# players: list[ActiveMatchPlayer]
# lobby_id: int
# net_worth_team_0: int
# net_worth_team_1: int
# game_mode_version: int
# duration_s: int
# spectators: int
# open_spectator_slots: int
# objectives_mask_team0: int
# objectives_mask_team1: int
# match_mode: int
# game_mode: int
# match_score: int
# region_mode: int
#
# @classmethod
# def from_msg(cls, msg: CMsgDevMatchInfo) -> "ActiveMatch":
# return cls(
# start_time=msg.start_time,
# winning_team=msg.winning_team,
# match_id=msg.match_id,
# players=[
# ActiveMatchPlayer(
# account_id=player.account_id,
# team=player.team,
# abandoned=player.abandoned,
# hero_id=player.hero_id,
# )
# for player in msg.players
# ],
# lobby_id=msg.lobby_id,
# net_worth_team_0=msg.net_worth_team_0,
# net_worth_team_1=msg.net_worth_team_1,
# game_mode_version=msg.game_mode_version,
# duration_s=msg.duration_s,
# spectators=msg.spectators,
# open_spectator_slots=msg.open_spectator_slots,
# objectives_mask_team0=msg.objectives_mask_team0,
# objectives_mask_team1=msg.objectives_mask_team1,
# match_mode=msg.match_mode,
# game_mode=msg.game_mode,
# match_score=msg.match_score,
# region_mode=msg.region_mode,
# )
#
#
# class APIActiveMatch(BaseModel):
# model_config = ConfigDict(populate_by_name=True)
#
# active_matches: list[ActiveMatch]
3 changes: 1 addition & 2 deletions deadlock_data_api/routers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response

from deadlock_data_api.models.active_match import ActiveMatch
from deadlock_data_api.routers import v1

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -55,5 +54,5 @@ def get_builds_by_hero_id(req: Request, hero_id: int) -> RedirectResponse:


@router.get("/active-matches", response_model_exclude_none=True)
def get_active_matches(req: Request, res: Response) -> list[ActiveMatch]:
def get_active_matches(req: Request, res: Response) -> RedirectResponse:
return v1.get_active_matches(req, res)
63 changes: 23 additions & 40 deletions deadlock_data_api/routers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
from starlette.requests import Request
from starlette.responses import JSONResponse, RedirectResponse, Response
from starlette.status import HTTP_301_MOVED_PERMANENTLY
from valveprotos_py.citadel_gcmessages_client_pb2 import (
CMsgClientToGCGetActiveMatchesResponse,
)
from valveprotos_py.citadel_gcmessages_common_pb2 import (
CMsgMatchMetaData,
CMsgMatchMetaDataContents,
Expand All @@ -23,7 +20,6 @@
from deadlock_data_api import utils
from deadlock_data_api.conf import CONFIG
from deadlock_data_api.globs import s3_main_conn
from deadlock_data_api.models.active_match import ActiveMatch
from deadlock_data_api.models.leaderboard import Leaderboard
from deadlock_data_api.models.player_card import PlayerCard
from deadlock_data_api.models.player_match_history import (
Expand All @@ -33,7 +29,6 @@
from deadlock_data_api.rate_limiter import limiter
from deadlock_data_api.rate_limiter.models import RateLimit
from deadlock_data_api.routers.v1_utils import (
fetch_active_matches_raw,
fetch_metadata,
get_leaderboard,
get_match_salts_from_db,
Expand All @@ -44,7 +39,7 @@
)
from deadlock_data_api.utils import cache_file, get_cached_file, send_webhook_event

CACHE_AGE_ACTIVE_MATCHES = 20
# CACHE_AGE_ACTIVE_MATCHES = 20
# CACHE_AGE_BUILDS = 5 * 60

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -198,47 +193,35 @@ def get_builds_by_author_id(

@router.get(
"/raw-active-matches",
response_model_exclude_none=True,
summary="Updates every 20s | Rate Limit 100req/s, Shared Rate Limit with /active-matches",
summary="Moved to new API: http://api.deadlock-api.com/",
description="""
# Endpoint moved to new API
- New API Docs: http://api.deadlock-api.com/docs
- New API Endpoint: http://api.deadlock-api.com/v1/matches/active/raw
""",
deprecated=True,
)
def get_active_matches_raw(
req: Request, res: Response, account_groups: str | None = None
) -> Response:
limiter.apply_limits(req, res, "/v1/active-matches", [RateLimit(limit=100, period=1)])
account_groups = utils.validate_account_groups(
account_groups, req.headers.get("X-API-Key", req.query_params.get("api_key"))
)
return Response(
content=fetch_active_matches_raw(account_groups),
media_type="application/octet-stream",
headers={"Cache-Control": f"public, max-age={CACHE_AGE_ACTIVE_MATCHES}"},
def get_active_matches_raw() -> RedirectResponse:
return RedirectResponse(
"https://api.deadlock-api.com/v1/matches/active/raw", HTTP_301_MOVED_PERMANENTLY
)


@router.get(
"/active-matches",
response_model_exclude_none=True,
summary="Updates every 20s | Rate Limit 100req/s, Shared Rate Limit with /raw-active-matches",
summary="Moved to new API: http://api.deadlock-api.com/",
description="""
# Endpoint moved to new API
- New API Docs: http://api.deadlock-api.com/docs
- New API Endpoint: http://api.deadlock-api.com/v1/matches/active
""",
deprecated=True,
)
def get_active_matches(
req: Request, res: Response, account_id: int | None = None, account_groups: str | None = None
) -> list[ActiveMatch]:
limiter.apply_limits(req, res, "/v1/active-matches", [RateLimit(limit=100, period=1)])
res.headers["Cache-Control"] = f"public, max-age={CACHE_AGE_ACTIVE_MATCHES}"

account_id = utils.validate_steam_id_optional(account_id)
account_groups = utils.validate_account_groups(
account_groups, req.headers.get("X-API-Key", req.query_params.get("api_key"))
)

raw_active_matches = fetch_active_matches_raw(account_groups)
msg = CMsgClientToGCGetActiveMatchesResponse.FromString(raw_active_matches)

return [
ActiveMatch.from_msg(am)
for am in msg.active_matches
if account_id is None or any(p.account_id == account_id for p in am.players)
]
def get_active_matches(account_id: int | None = None) -> RedirectResponse:
url = URL("https://api.deadlock-api.com/v1/matches/active")
if account_id:
url.include_query_params(account_id=account_id)
return RedirectResponse(url, HTTP_301_MOVED_PERMANENTLY)


@router.get(
Expand Down
55 changes: 25 additions & 30 deletions deadlock_data_api/routers/v1_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@
from typing import Literal

import requests
import snappy
from cachetools.func import ttl_cache
from fastapi import HTTPException
from starlette.status import HTTP_404_NOT_FOUND, HTTP_503_SERVICE_UNAVAILABLE
from valveprotos_py.citadel_gcmessages_client_pb2 import (
CMsgCitadelProfileCard,
CMsgClientToGCGetActiveMatches,
CMsgClientToGCGetLeaderboard,
CMsgClientToGCGetLeaderboardResponse,
CMsgClientToGCGetMatchHistory,
CMsgClientToGCGetMatchHistoryResponse,
CMsgClientToGCGetMatchMetaData,
CMsgClientToGCGetMatchMetaDataResponse,
CMsgClientToGCGetProfileCard,
k_EMsgClientToGCGetActiveMatches,
k_EMsgClientToGCGetLeaderboard,
k_EMsgClientToGCGetMatchHistory,
k_EMsgClientToGCGetMatchMetaData,
Expand All @@ -41,13 +38,11 @@
)
from deadlock_data_api.utils import (
call_steam_proxy,
call_steam_proxy_raw,
send_webhook_message,
)

CACHE_AGE_ACTIVE_MATCHES = 20
# CACHE_AGE_ACTIVE_MATCHES = 20
# CACHE_AGE_BUILDS = 5 * 60
LOAD_FILE_RETRIES = 5
# LOAD_FILE_RETRIES = 5

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -340,29 +335,29 @@ def fetch_metadata(match_id: int, salts: CMsgClientToGCGetMatchMetaDataResponse)
# return Build.model_validate(result[0])


@ttl_cache(ttl=CACHE_AGE_ACTIVE_MATCHES)
def fetch_active_matches_raw(account_groups: str | None = None, retries: int = 3) -> bytes:
try:
attempts = 0
while True:
attempts += 1
try:
msg = call_steam_proxy_raw(
k_EMsgClientToGCGetActiveMatches,
CMsgClientToGCGetActiveMatches(),
10,
account_groups.split(",") if account_groups else ["LowRateLimitApis"],
)
return snappy.decompress(msg[7:])
except Exception as e:
if attempts >= retries:
raise e
msg = f"Failed to fetch active matches: {e.response.status_code if isinstance(e, requests.exceptions.HTTPError) else type(e).__name__}"
LOGGER.exception(msg)
except Exception as e:
msg = f"Failed to fetch active matches: {e.response.status_code if isinstance(e, requests.exceptions.HTTPError) else type(e).__name__}"
send_webhook_message(msg)
raise HTTPException(status_code=500, detail="Failed to fetch active matches")
# @ttl_cache(ttl=CACHE_AGE_ACTIVE_MATCHES)
# def fetch_active_matches_raw(account_groups: str | None = None, retries: int = 3) -> bytes:
# try:
# attempts = 0
# while True:
# attempts += 1
# try:
# msg = call_steam_proxy_raw(
# k_EMsgClientToGCGetActiveMatches,
# CMsgClientToGCGetActiveMatches(),
# 10,
# account_groups.split(",") if account_groups else ["LowRateLimitApis"],
# )
# return snappy.decompress(msg[7:])
# except Exception as e:
# if attempts >= retries:
# raise e
# msg = f"Failed to fetch active matches: {e.response.status_code if isinstance(e, requests.exceptions.HTTPError) else type(e).__name__}"
# LOGGER.exception(msg)
# except Exception as e:
# msg = f"Failed to fetch active matches: {e.response.status_code if isinstance(e, requests.exceptions.HTTPError) else type(e).__name__}"
# send_webhook_message(msg)
# raise HTTPException(status_code=500, detail="Failed to fetch active matches")


# last_patch_notes: str = ""
Expand Down
Loading

0 comments on commit 49b8925

Please sign in to comment.