275 lines
6.8 KiB
Go
275 lines
6.8 KiB
Go
package invite
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
|
||
"codex-pool/internal/client"
|
||
)
|
||
|
||
// DefaultProxy 默认代理
|
||
const DefaultProxy = "http://127.0.0.1:7890"
|
||
|
||
// TeamInviter Team 邀请器
|
||
type TeamInviter struct {
|
||
client *client.TLSClient
|
||
accessToken string
|
||
accountID string
|
||
}
|
||
|
||
// InviteRequest 邀请请求
|
||
type InviteRequest struct {
|
||
EmailAddresses []string `json:"email_addresses"`
|
||
Role string `json:"role"`
|
||
ResendEmails bool `json:"resend_emails"`
|
||
}
|
||
|
||
// AccountCheckResponse 账号检查响应
|
||
type AccountCheckResponse struct {
|
||
Accounts map[string]struct {
|
||
Account struct {
|
||
PlanType string `json:"plan_type"`
|
||
} `json:"account"`
|
||
} `json:"accounts"`
|
||
}
|
||
|
||
// AccountStatus 账户状态检查结果
|
||
type AccountStatus struct {
|
||
Status string // active, banned, token_expired, error
|
||
PlanType string // team, plus, free 等
|
||
AccountID string // 账户 ID
|
||
Error string // 错误信息
|
||
}
|
||
|
||
// New 创建邀请器 (使用默认代理)
|
||
func New(accessToken string) *TeamInviter {
|
||
c, _ := client.New(DefaultProxy)
|
||
return &TeamInviter{
|
||
client: c,
|
||
accessToken: accessToken,
|
||
}
|
||
}
|
||
|
||
// NewWithProxy 创建邀请器 (指定代理)
|
||
func NewWithProxy(accessToken, proxy string) *TeamInviter {
|
||
c, _ := client.New(proxy)
|
||
return &TeamInviter{
|
||
client: c,
|
||
accessToken: accessToken,
|
||
}
|
||
}
|
||
|
||
// SetAccountID 手动设置 account_id(当已有存储值时使用)
|
||
func (t *TeamInviter) SetAccountID(accountID string) {
|
||
t.accountID = accountID
|
||
}
|
||
|
||
// CheckAccountStatus 检查账户状态(封禁检测)
|
||
// 基于 check_ban.py 的逻辑:
|
||
// - HTTP 200 + 有账户数据 → active
|
||
// - HTTP 200 + 无账户数据 → banned
|
||
// - HTTP 401 → token_expired
|
||
// - HTTP 403 → banned
|
||
func (t *TeamInviter) CheckAccountStatus() AccountStatus {
|
||
result := AccountStatus{Status: "error"}
|
||
|
||
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
|
||
req.Header.Set("Authorization", "Bearer "+t.accessToken)
|
||
req.Header.Set("Accept", "application/json")
|
||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
|
||
|
||
resp, err := t.client.Do(req)
|
||
if err != nil {
|
||
result.Error = fmt.Sprintf("请求失败: %v", err)
|
||
return result
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, _ := io.ReadAll(resp.Body)
|
||
|
||
switch resp.StatusCode {
|
||
case 200:
|
||
var checkResp AccountCheckResponse
|
||
if err := json.Unmarshal(body, &checkResp); err != nil {
|
||
result.Error = fmt.Sprintf("解析失败: %v", err)
|
||
return result
|
||
}
|
||
|
||
// 检查是否有账户数据
|
||
if len(checkResp.Accounts) == 0 {
|
||
result.Status = "banned"
|
||
result.Error = "无账户数据"
|
||
return result
|
||
}
|
||
|
||
// 查找非 default 的账户
|
||
for accountID, info := range checkResp.Accounts {
|
||
if accountID == "default" {
|
||
continue
|
||
}
|
||
result.Status = "active"
|
||
result.AccountID = accountID
|
||
result.PlanType = info.Account.PlanType
|
||
t.accountID = accountID
|
||
return result
|
||
}
|
||
|
||
// 只有 default 账户,也视为正常
|
||
result.Status = "active"
|
||
result.PlanType = "default"
|
||
return result
|
||
|
||
case 401:
|
||
result.Status = "token_expired"
|
||
result.Error = "Token 已过期"
|
||
return result
|
||
|
||
case 403:
|
||
result.Status = "banned"
|
||
result.Error = "账户被封禁 (403)"
|
||
return result
|
||
|
||
default:
|
||
result.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// GetAccountID 获取 Team 的 account_id (workspace_id)
|
||
func (t *TeamInviter) GetAccountID() (string, error) {
|
||
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
|
||
req.Header.Set("Authorization", "Bearer "+t.accessToken)
|
||
req.Header.Set("Accept", "application/json")
|
||
|
||
resp, err := t.client.Do(req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, _ := io.ReadAll(resp.Body)
|
||
|
||
if resp.StatusCode != 200 {
|
||
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(200, len(body))])
|
||
}
|
||
|
||
var result AccountCheckResponse
|
||
if err := json.Unmarshal(body, &result); err != nil {
|
||
return "", fmt.Errorf("解析失败: %v", err)
|
||
}
|
||
|
||
// 查找 team plan 的 account_id
|
||
for accountID, info := range result.Accounts {
|
||
if accountID != "default" && info.Account.PlanType == "team" {
|
||
t.accountID = accountID
|
||
return accountID, nil
|
||
}
|
||
}
|
||
|
||
// 如果没找到 team,返回第一个非 default 的
|
||
for accountID := range result.Accounts {
|
||
if accountID != "default" {
|
||
t.accountID = accountID
|
||
return accountID, nil
|
||
}
|
||
}
|
||
|
||
return "", fmt.Errorf("未找到 account_id")
|
||
}
|
||
|
||
// SendInvites 发送邀请
|
||
func (t *TeamInviter) SendInvites(emails []string) error {
|
||
if t.accountID == "" {
|
||
return fmt.Errorf("未设置 account_id,请先调用 GetAccountID()")
|
||
}
|
||
|
||
url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites", t.accountID)
|
||
|
||
payload := InviteRequest{
|
||
EmailAddresses: emails,
|
||
Role: "standard-user",
|
||
ResendEmails: true,
|
||
}
|
||
body, _ := json.Marshal(payload)
|
||
|
||
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
|
||
req.Header.Set("Authorization", "Bearer "+t.accessToken)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Chatgpt-Account-Id", t.accountID)
|
||
|
||
resp, err := t.client.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
if resp.StatusCode != 200 && resp.StatusCode != 201 {
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))])
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetPendingInvites 获取待处理的邀请列表
|
||
func (t *TeamInviter) GetPendingInvites() ([]string, error) {
|
||
if t.accountID == "" {
|
||
return nil, fmt.Errorf("未设置 account_id")
|
||
}
|
||
|
||
url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites?offset=0&limit=100&query=", t.accountID)
|
||
|
||
req, _ := http.NewRequest("GET", url, nil)
|
||
req.Header.Set("Authorization", "Bearer "+t.accessToken)
|
||
|
||
resp, err := t.client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, _ := io.ReadAll(resp.Body)
|
||
|
||
var result struct {
|
||
Invites []struct {
|
||
Email string `json:"email"`
|
||
} `json:"invites"`
|
||
}
|
||
|
||
if err := json.Unmarshal(body, &result); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var emails []string
|
||
for _, inv := range result.Invites {
|
||
emails = append(emails, inv.Email)
|
||
}
|
||
|
||
return emails, nil
|
||
}
|
||
|
||
// AcceptInvite 接受邀请 (使用被邀请账号的 token)
|
||
func AcceptInvite(inviteLink string, accessToken string) error {
|
||
c, _ := client.New(DefaultProxy)
|
||
|
||
req, _ := http.NewRequest("GET", inviteLink, nil)
|
||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||
|
||
resp, err := c.Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 && resp.StatusCode != 302 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(100, len(body))])
|
||
}
|
||
|
||
return nil
|
||
}
|