from enum import StrEnum from typing import Annotated, Self from httpx import HTTPStatusError from fastapi import APIRouter, Query, Response from pydantic import BaseModel, Field, model_validator from nyekiapi.lib import settings from nyekiapi.api.v2.errors import NotFoundError, BadGatewayError, InternalServerError from nyekiapi.api.v2.schemas import ErrorSchema from nyekiapi.api.v2.profiles.schemas import ProfileSchema from nyekiapi.lib.integrations import discord, github, Profile router = APIRouter(tags=["Profiles"]) sources = [] if settings.DISCORD_TOKEN: sources.append(("DISCORD", "discord")) if settings.GITHUB_TOKEN: sources.append(("GITHUB", "github")) ProfileSource = StrEnum("ProfileSource", sources) class ProfileQuerySchema(BaseModel): source: ProfileSource = Field(..., description="The source of the profile.") source_id: str | None = Field( None, description='The ID of the profile in the source.\nSupported by: `"discord"` `"github"`', ) source_name: str | None = Field( None, description='The name of the profile in the source.\nSupported by: `"github"`', ) @model_validator(mode="after") def validate_source(self) -> Self: if self.source_id and self.source_name: raise ValueError( "Both source_id and source_name cannot be provided at the same time." ) if self.source.value == "discord" and not self.source_id: raise ValueError("source_id is required for Discord profiles.") if self.source.value == "github" and not (self.source_name or self.source_id): raise ValueError( "Either source_name or source_id is required for GitHub profiles." ) return self ProfileQuery = Annotated[ProfileQuerySchema, Query()] async def _get_profile(profile: ProfileQuerySchema) -> Profile: try: if profile.source.value == "discord": return await discord.get_profile_by_id(profile.source_id) elif profile.source.value == "github": if profile.source_id: return await github.get_profile_by_id(profile.source_id) else: return await github.get_profile_by_name(profile.source_name) except HTTPStatusError as e: if e.response.status_code == 404: raise NotFoundError("profile") from e elif e.response.status_code >= 500: raise BadGatewayError() from e else: raise InternalServerError() from e @router.get( "/profile", responses={ 404: {"model": ErrorSchema}, 422: {"model": ErrorSchema}, 500: {"model": ErrorSchema}, 502: {"model": ErrorSchema}, }, ) async def get_profile(profile: ProfileQuery) -> ProfileSchema: """Returns basic information about a profile from a given source.""" profile = await _get_profile(profile) return ProfileSchema.from_integration(profile) @router.get( "/profile/avatar", responses={ 404: {"model": ErrorSchema}, 422: {"model": ErrorSchema}, 500: {"model": ErrorSchema}, 502: {"model": ErrorSchema}, }, status_code=302, ) async def get_profile_avatar(response: Response, profile: ProfileQuery) -> None: """Redirects to the avatar URL of a profile from a given source.""" profile = await _get_profile(profile) response.headers["Location"] = profile.avatar_url @router.get( "/profile/banner", responses={ 404: {"model": ErrorSchema}, 422: {"model": ErrorSchema}, 500: {"model": ErrorSchema}, 502: {"model": ErrorSchema}, }, status_code=302, ) async def get_profile_banner(response: Response, profile: ProfileQuery) -> None: """Redirects to the banner URL of a profile from a given source.""" profile = await _get_profile(profile) if not profile.banner_url: raise NotFoundError("profile banner") response.headers["Location"] = profile.banner_url @router.get( "/profile/url", responses={ 404: {"model": ErrorSchema}, 422: {"model": ErrorSchema}, 500: {"model": ErrorSchema}, 502: {"model": ErrorSchema}, }, status_code=302, ) async def get_profile_url(response: Response, profile: ProfileQuery) -> None: """Redirects to the profile URL of a profile from a given source.""" profile = await _get_profile(profile) response.headers["Location"] = profile.url