package client import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils" ) // DeviceCode represents the device authorization response type DeviceCode struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` VerificationURL string `json:"verification_url"` ExpiresIn int `json:"expires_in"` Interval int `json:"interval"` } // OAuthToken represents an OAuth token response type OAuthToken struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } // DeviceAuth performs OAuth Device Flow authentication func (r *Request) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) { // Step 1: Request device code deviceCode, err := r.requestDeviceCode(ctx) if err != nil { return nil, err } // Call callback with device code if onCode != nil { onCode(deviceCode) } // Step 2: Poll for token return r.pollForToken(ctx, deviceCode) } // requestDeviceCode requests a device code from Yandex OAuth func (r *Request) requestDeviceCode(ctx context.Context) (*DeviceCode, error) { // Use Yandex OAuth endpoint for device auth data := url.Values{} data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d") data.Set("scope", "login:info login:email yandex_music") req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/device/code", strings.NewReader(data.Encode())) if err != nil { return nil, utils.NewYandexMusicError("failed to create device code request", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", UserAgent) resp, err := r.client.Do(req) if err != nil { return nil, utils.NewYandexMusicError("failed to request device code", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, utils.NewYandexMusicError("failed to read device code response", err) } if resp.StatusCode != http.StatusOK { return nil, utils.NewYandexMusicError(fmt.Sprintf("device code request failed: %s", string(body)), utils.ErrBadRequest) } var deviceCode DeviceCode if err := json.Unmarshal(body, &deviceCode); err != nil { return nil, utils.NewYandexMusicError("failed to parse device code", err) } return &deviceCode, nil } // pollForToken polls for the OAuth token func (r *Request) pollForToken(ctx context.Context, deviceCode *DeviceCode) (*OAuthToken, error) { interval := deviceCode.Interval if interval == 0 { interval = 5 } deadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second) ticker := time.NewTicker(time.Duration(interval) * time.Second) defer ticker.Stop() data := url.Values{} data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d") data.Set("device_code", deviceCode.DeviceCode) for { select { case <-ctx.Done(): return nil, ctx.Err() case <-ticker.C: if time.Now().After(deadline) { return nil, utils.NewYandexMusicError("device authorization timed out", utils.ErrTimeout) } req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode())) if err != nil { return nil, utils.NewYandexMusicError("failed to create token request", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", UserAgent) resp, err := r.client.Do(req) if err != nil { continue // Retry on network error } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode == http.StatusOK { var token OAuthToken if err := json.Unmarshal(body, &token); err != nil { return nil, utils.NewYandexMusicError("failed to parse token", err) } return &token, nil } // Check for specific errors var errResp map[string]string json.Unmarshal(body, &errResp) if errResp["error"] == "authorization_pending" { continue // Keep polling } if errResp["error"] == "slow_down" { interval *= 2 ticker.Reset(time.Duration(interval) * time.Second) continue } if errResp["error"] == "expired_token" { return nil, utils.NewYandexMusicError("device code expired", utils.ErrTimeout) } if errResp["error"] == "access_denied" { return nil, utils.NewYandexMusicError("access denied by user", utils.ErrUnauthorized) } } } } // RefreshToken refreshes an access token using a refresh token func (r *Request) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) { data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d") data.Set("refresh_token", refreshToken) req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode())) if err != nil { return nil, utils.NewYandexMusicError("failed to create refresh request", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", UserAgent) resp, err := r.client.Do(req) if err != nil { return nil, utils.NewYandexMusicError("failed to refresh token", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, utils.NewYandexMusicError("failed to read refresh response", err) } if resp.StatusCode != http.StatusOK { return nil, utils.NewYandexMusicError(fmt.Sprintf("token refresh failed: %s", string(body)), utils.ErrUnauthorized) } var token OAuthToken if err := json.Unmarshal(body, &token); err != nil { return nil, utils.NewYandexMusicError("failed to parse refreshed token", err) } return &token, nil } // Missing imports - need to add at top of file // "io" // "net/http"