核心功能: - 实现基于 Telegram Inline Button 交互的后台面板与用户端 - 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号 - 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请 - 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码 - 随机邀请功能:成功拉人后自动核销兑换码 - 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误) 系统与配置: - 使用 PostgreSQL 数据库管理账号、邀请和兑换记录 - 支持在端内动态添加、移除管理员 - 完善 Docker 部署配置与 .gitignore 规则
110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
package chatgpt
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"encoding/json"
|
|
)
|
|
|
|
// TokenResult holds the result of a token refresh.
|
|
type TokenResult struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
IDToken string
|
|
ExpiresIn int
|
|
}
|
|
|
|
// GetEmail extracts the user's email from the ID token or Access token.
|
|
func (tr *TokenResult) GetEmail() string {
|
|
if tr.IDToken != "" {
|
|
claims, err := decodeJWTPayload(tr.IDToken)
|
|
if err == nil {
|
|
if email, ok := claims["email"].(string); ok && email != "" {
|
|
return email
|
|
}
|
|
}
|
|
}
|
|
if tr.AccessToken != "" {
|
|
claims, err := decodeJWTPayload(tr.AccessToken)
|
|
if err == nil {
|
|
if email, ok := claims["email"].(string); ok && email != "" {
|
|
return email
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// RefreshAccessToken exchanges a refresh token for a new access token.
|
|
func (c *Client) RefreshAccessToken(refreshToken string) (*TokenResult, error) {
|
|
rt := strings.TrimSpace(refreshToken)
|
|
if rt == "" {
|
|
return nil, fmt.Errorf("refresh token 为空")
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("grant_type", "refresh_token")
|
|
form.Set("client_id", openaiClientID)
|
|
form.Set("refresh_token", rt)
|
|
form.Set("scope", "openid profile email")
|
|
|
|
req, err := http.NewRequest("POST", "https://auth.openai.com/oauth/token", strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("刷新 token 网络错误: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if resp.StatusCode != 200 {
|
|
var errData struct {
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description"`
|
|
}
|
|
_ = json.Unmarshal(body, &errData)
|
|
msg := errData.ErrorDescription
|
|
if msg == "" {
|
|
msg = errData.Error
|
|
}
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return nil, fmt.Errorf("刷新 token 失败: %s", msg)
|
|
}
|
|
|
|
var result struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
IDToken string `json:"id_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("解析刷新结果失败: %w", err)
|
|
}
|
|
if result.AccessToken == "" {
|
|
return nil, fmt.Errorf("刷新 token 失败: 未返回有效凭证")
|
|
}
|
|
|
|
newRT := result.RefreshToken
|
|
if newRT == "" {
|
|
newRT = rt
|
|
}
|
|
|
|
return &TokenResult{
|
|
AccessToken: result.AccessToken,
|
|
RefreshToken: newRT,
|
|
IDToken: result.IDToken,
|
|
ExpiresIn: result.ExpiresIn,
|
|
}, nil
|
|
}
|