205 lines
5.7 KiB
Python
205 lines
5.7 KiB
Python
"""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)
|