gemini_oauth_client.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package repository
  2. import (
  3. "context"
  4. "fmt"
  5. "net/url"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/imroc/req/v3"
  11. )
  12. type geminiOAuthClient struct {
  13. tokenURL string
  14. cfg *config.Config
  15. }
  16. func NewGeminiOAuthClient(cfg *config.Config) service.GeminiOAuthClient {
  17. return &geminiOAuthClient{
  18. tokenURL: geminicli.TokenURL,
  19. cfg: cfg,
  20. }
  21. }
  22. func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, codeVerifier, redirectURI, proxyURL string) (*geminicli.TokenResponse, error) {
  23. client, err := createGeminiReqClient(proxyURL)
  24. if err != nil {
  25. return nil, fmt.Errorf("create HTTP client: %w", err)
  26. }
  27. // Use different OAuth clients based on oauthType:
  28. // - code_assist: always use built-in Gemini CLI OAuth client (public)
  29. // - google_one: always use built-in Gemini CLI OAuth client (public)
  30. // - ai_studio: requires a user-provided OAuth client
  31. oauthCfgInput := geminicli.OAuthConfig{
  32. ClientID: c.cfg.Gemini.OAuth.ClientID,
  33. ClientSecret: c.cfg.Gemini.OAuth.ClientSecret,
  34. Scopes: c.cfg.Gemini.OAuth.Scopes,
  35. }
  36. if oauthType == "code_assist" || oauthType == "google_one" {
  37. // Force use of built-in Gemini CLI OAuth client
  38. oauthCfgInput.ClientID = ""
  39. oauthCfgInput.ClientSecret = ""
  40. }
  41. oauthCfg, err := geminicli.EffectiveOAuthConfig(oauthCfgInput, oauthType)
  42. if err != nil {
  43. return nil, err
  44. }
  45. formData := url.Values{}
  46. formData.Set("grant_type", "authorization_code")
  47. formData.Set("client_id", oauthCfg.ClientID)
  48. formData.Set("client_secret", oauthCfg.ClientSecret)
  49. formData.Set("code", code)
  50. formData.Set("code_verifier", codeVerifier)
  51. formData.Set("redirect_uri", redirectURI)
  52. var tokenResp geminicli.TokenResponse
  53. resp, err := client.R().
  54. SetContext(ctx).
  55. SetFormDataFromValues(formData).
  56. SetSuccessResult(&tokenResp).
  57. Post(c.tokenURL)
  58. if err != nil {
  59. return nil, fmt.Errorf("request failed: %w", err)
  60. }
  61. if !resp.IsSuccessState() {
  62. return nil, fmt.Errorf("token exchange failed: status %d, body: %s", resp.StatusCode, geminicli.SanitizeBodyForLogs(resp.String()))
  63. }
  64. return &tokenResp, nil
  65. }
  66. func (c *geminiOAuthClient) RefreshToken(ctx context.Context, oauthType, refreshToken, proxyURL string) (*geminicli.TokenResponse, error) {
  67. client, err := createGeminiReqClient(proxyURL)
  68. if err != nil {
  69. return nil, fmt.Errorf("create HTTP client: %w", err)
  70. }
  71. oauthCfgInput := geminicli.OAuthConfig{
  72. ClientID: c.cfg.Gemini.OAuth.ClientID,
  73. ClientSecret: c.cfg.Gemini.OAuth.ClientSecret,
  74. Scopes: c.cfg.Gemini.OAuth.Scopes,
  75. }
  76. if oauthType == "code_assist" || oauthType == "google_one" {
  77. // Force use of built-in Gemini CLI OAuth client
  78. oauthCfgInput.ClientID = ""
  79. oauthCfgInput.ClientSecret = ""
  80. }
  81. oauthCfg, err := geminicli.EffectiveOAuthConfig(oauthCfgInput, oauthType)
  82. if err != nil {
  83. return nil, err
  84. }
  85. formData := url.Values{}
  86. formData.Set("grant_type", "refresh_token")
  87. formData.Set("refresh_token", refreshToken)
  88. formData.Set("client_id", oauthCfg.ClientID)
  89. formData.Set("client_secret", oauthCfg.ClientSecret)
  90. var tokenResp geminicli.TokenResponse
  91. resp, err := client.R().
  92. SetContext(ctx).
  93. SetFormDataFromValues(formData).
  94. SetSuccessResult(&tokenResp).
  95. Post(c.tokenURL)
  96. if err != nil {
  97. return nil, fmt.Errorf("request failed: %w", err)
  98. }
  99. if !resp.IsSuccessState() {
  100. return nil, fmt.Errorf("token refresh failed: status %d, body: %s", resp.StatusCode, geminicli.SanitizeBodyForLogs(resp.String()))
  101. }
  102. return &tokenResp, nil
  103. }
  104. func createGeminiReqClient(proxyURL string) (*req.Client, error) {
  105. return getSharedReqClient(reqClientOptions{
  106. ProxyURL: proxyURL,
  107. Timeout: 60 * time.Second,
  108. })
  109. }