"""Yandex OAuth client with Disk API support. Scopes requested: - login:email — email address - login:info — login, first+last name - login:avatar — avatar URL - cloud_api:disk.write — write anywhere on Disk - cloud_api:disk.app_folder — access app folder on Disk - cloud_api:disk.read — read entire Disk - cloud_api:disk.info — Disk info (quota, etc.) """ from dataclasses import dataclass import httpx from app.core.config import get_settings YANDEX_OAUTH_URL = "https://oauth.yandex.ru" YANDEX_LOGIN_URL = "https://login.yandex.ru" YANDEX_DISK_URL = "https://cloud-api.yandex.net/v1/disk" SCOPES = " ".join([ "login:email", "login:info", "login:avatar", "cloud_api:disk.write", "cloud_api:disk.app_folder", "cloud_api:disk.read", "cloud_api:disk.info", ]) VOIDEA_DISK_FOLDER = "VoIdeaAI" @dataclass class YandexUserInfo: id: str login: str email: str display_name: str avatar_url: str | None @dataclass class YandexTokenResult: access_token: str refresh_token: str | None expires_in: int async def get_authorize_url() -> str: """Generate Yandex OAuth authorization URL.""" settings = get_settings() params = { "response_type": "code", "client_id": settings.oauth_yandex_id, "redirect_uri": settings.oauth_yandex_redirect_uri, "scope": SCOPES, } query = "&".join(f"{k}={v}" for k, v in params.items()) return f"{YANDEX_OAUTH_URL}/authorize?{query}" async def exchange_code(code: str) -> YandexTokenResult | None: """Exchange authorization code for access token. Args: code: Authorization code from OAuth callback Returns: Token result or None if exchange failed """ settings = get_settings() if not settings.oauth_yandex_id or not settings.oauth_yandex_secret: return None async with httpx.AsyncClient() as client: resp = await client.post( f"{YANDEX_OAUTH_URL}/token", data={ "grant_type": "authorization_code", "code": code, "client_id": settings.oauth_yandex_id, "client_secret": settings.oauth_yandex_secret, }, ) if resp.status_code != 200: return None data = resp.json() return YandexTokenResult( access_token=data["access_token"], refresh_token=data.get("refresh_token"), expires_in=data.get("expires_in", 0), ) async def get_user_info(access_token: str) -> YandexUserInfo | None: """Get user info from Yandex Login API. Args: access_token: Valid OAuth access token Returns: User info or None if request failed """ async with httpx.AsyncClient() as client: resp = await client.get( f"{YANDEX_LOGIN_URL}/info", params={"format": "json"}, headers={"Authorization": f"OAuth {access_token}"}, ) if resp.status_code != 200: return None data = resp.json() avatar_url: str | None = None if data.get("default_avatar_id"): avatar_url = ( f"https://avatars.yandex.net/get-yapic/{data['default_avatar_id']}/islands-200" ) display_name = data.get("real_name") or data.get("display_name") or data.get("login", "") email = data.get("default_email") or "" return YandexUserInfo( id=str(data["id"]), login=data.get("login", ""), email=email, display_name=display_name, avatar_url=avatar_url, ) async def ensure_app_folder(access_token: str) -> bool: """Create VoIdeaAI folder on Yandex.Disk if it doesn't exist. Args: access_token: Valid OAuth access token with disk.write scope Returns: True if folder exists (created or already present) """ async with httpx.AsyncClient() as client: resp = await client.put( f"{YANDEX_DISK_URL}/resources", params={"path": VOIDEA_DISK_FOLDER}, headers={"Authorization": f"OAuth {access_token}"}, ) return resp.status_code in (201, 409) async def get_disk_info(access_token: str) -> dict | None: """Get Yandex.Disk info (quota, usage). Args: access_token: Valid OAuth access token Returns: Disk info dict or None """ async with httpx.AsyncClient() as client: resp = await client.get( f"{YANDEX_DISK_URL}", headers={"Authorization": f"OAuth {access_token}"}, ) return resp.json() if resp.status_code == 200 else None async def upload_file(access_token: str, local_path: str, remote_name: str) -> bool: """Upload a file to VoIdeaAI folder on Yandex.Disk. Args: access_token: Valid OAuth access token local_path: Path to local file remote_name: Name for the file on Disk Returns: True if upload succeeded """ import os if not os.path.exists(local_path): return False remote_path = f"{VOIDEA_DISK_FOLDER}/{remote_name}" async with httpx.AsyncClient() as client: # 1. Get upload URL resp = await client.get( f"{YANDEX_DISK_URL}/resources/upload", params={"path": remote_path, "overwrite": "true"}, headers={"Authorization": f"OAuth {access_token}"}, ) if resp.status_code != 200: return False upload_url = resp.json().get("href") if not upload_url: return False # 2. Upload file with open(local_path, "rb") as f: upload_resp = await client.put(upload_url, content=f) return upload_resp.status_code in (201, 202)