Initial sanitized code sync
This commit is contained in:
184
pkg/chatgpt/account.go
Normal file
184
pkg/chatgpt/account.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package chatgpt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"gpt-plus/pkg/httpclient"
|
||||
)
|
||||
|
||||
const accountCheckURL = "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27"
|
||||
|
||||
// AccountInfo holds parsed account data from the accounts/check endpoint.
|
||||
type AccountInfo struct {
|
||||
AccountID string
|
||||
AccountUserID string // e.g. "user-xxx__account-id"
|
||||
PlanType string // "free", "plus", "team"
|
||||
OrganizationID string
|
||||
IsTeam bool
|
||||
Structure string // "personal" or "workspace"
|
||||
// Entitlement reflects whether OpenAI has actually activated the subscription.
|
||||
HasActiveSubscription bool
|
||||
SubscriptionID string
|
||||
// EligiblePromos contains promo campaign IDs keyed by plan (e.g. "plus", "team").
|
||||
EligiblePromos map[string]string
|
||||
}
|
||||
|
||||
// accountCheckResponse mirrors the JSON response from accounts/check.
|
||||
type accountCheckResponse struct {
|
||||
Accounts map[string]accountEntry `json:"accounts"`
|
||||
}
|
||||
|
||||
type promoEntry struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type accountEntry struct {
|
||||
Account struct {
|
||||
AccountID string `json:"account_id"`
|
||||
AccountUserID string `json:"account_user_id"`
|
||||
PlanType string `json:"plan_type"`
|
||||
IsDeactivated bool `json:"is_deactivated"`
|
||||
Structure string `json:"structure"`
|
||||
} `json:"account"`
|
||||
Entitlement struct {
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
HasActiveSubscription bool `json:"has_active_subscription"`
|
||||
} `json:"entitlement"`
|
||||
Features []string `json:"features"`
|
||||
EligiblePromoCampaigns map[string]promoEntry `json:"eligible_promo_campaigns"`
|
||||
}
|
||||
|
||||
// CheckAccount queries the ChatGPT account status and returns parsed account info.
|
||||
func CheckAccount(client *httpclient.Client, accessToken, deviceID string) (*AccountInfo, error) {
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
"User-Agent": defaultUserAgent,
|
||||
"Accept": "*/*",
|
||||
"Origin": chatGPTOrigin,
|
||||
"Referer": chatGPTOrigin + "/",
|
||||
"oai-device-id": deviceID,
|
||||
"oai-language": defaultLanguage,
|
||||
}
|
||||
|
||||
resp, err := client.Get(accountCheckURL, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account check request: %w", err)
|
||||
}
|
||||
|
||||
body, err := httpclient.ReadBody(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read account check body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("account check returned %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Printf("[account] check response: %s", string(body)[:min(len(body), 500)])
|
||||
|
||||
var result accountCheckResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse account check response: %w", err)
|
||||
}
|
||||
|
||||
// Flatten all accounts, then pick the best one (personal > workspace)
|
||||
var allAccounts []*AccountInfo
|
||||
for _, entry := range result.Accounts {
|
||||
promos := make(map[string]string)
|
||||
for key, promo := range entry.EligiblePromoCampaigns {
|
||||
promos[key] = promo.ID
|
||||
}
|
||||
allAccounts = append(allAccounts, &AccountInfo{
|
||||
AccountID: entry.Account.AccountID,
|
||||
AccountUserID: entry.Account.AccountUserID,
|
||||
PlanType: entry.Account.PlanType,
|
||||
Structure: entry.Account.Structure,
|
||||
IsTeam: entry.Account.Structure == "workspace",
|
||||
HasActiveSubscription: entry.Entitlement.HasActiveSubscription,
|
||||
SubscriptionID: entry.Entitlement.SubscriptionID,
|
||||
EligiblePromos: promos,
|
||||
})
|
||||
}
|
||||
|
||||
if len(allAccounts) == 0 {
|
||||
return nil, fmt.Errorf("no accounts found in response")
|
||||
}
|
||||
|
||||
// Sort by AccountID for deterministic ordering (eliminates map iteration randomness)
|
||||
sort.Slice(allAccounts, func(i, j int) bool {
|
||||
return allAccounts[i].AccountID < allAccounts[j].AccountID
|
||||
})
|
||||
|
||||
// Prefer personal account over workspace
|
||||
for _, acct := range allAccounts {
|
||||
if acct.Structure == "personal" {
|
||||
return acct, nil
|
||||
}
|
||||
}
|
||||
return allAccounts[0], nil
|
||||
}
|
||||
|
||||
// CheckAccountFull returns all account entries (useful for finding team accounts).
|
||||
func CheckAccountFull(client *httpclient.Client, accessToken, deviceID string) ([]*AccountInfo, error) {
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
"User-Agent": defaultUserAgent,
|
||||
"Accept": "*/*",
|
||||
"Origin": chatGPTOrigin,
|
||||
"Referer": chatGPTOrigin + "/",
|
||||
"oai-device-id": deviceID,
|
||||
"oai-language": defaultLanguage,
|
||||
}
|
||||
|
||||
resp, err := client.Get(accountCheckURL, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account check request: %w", err)
|
||||
}
|
||||
|
||||
body, err := httpclient.ReadBody(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read account check body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("account check returned %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result accountCheckResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse account check response: %w", err)
|
||||
}
|
||||
|
||||
var accounts []*AccountInfo
|
||||
for _, entry := range result.Accounts {
|
||||
promos := make(map[string]string)
|
||||
for key, promo := range entry.EligiblePromoCampaigns {
|
||||
promos[key] = promo.ID
|
||||
}
|
||||
info := &AccountInfo{
|
||||
AccountID: entry.Account.AccountID,
|
||||
AccountUserID: entry.Account.AccountUserID,
|
||||
PlanType: entry.Account.PlanType,
|
||||
Structure: entry.Account.Structure,
|
||||
IsTeam: entry.Account.Structure == "workspace",
|
||||
HasActiveSubscription: entry.Entitlement.HasActiveSubscription,
|
||||
SubscriptionID: entry.Entitlement.SubscriptionID,
|
||||
EligiblePromos: promos,
|
||||
}
|
||||
accounts = append(accounts, info)
|
||||
}
|
||||
|
||||
if len(accounts) == 0 {
|
||||
return nil, fmt.Errorf("no accounts found in response")
|
||||
}
|
||||
|
||||
sort.Slice(accounts, func(i, j int) bool {
|
||||
return accounts[i].AccountID < accounts[j].AccountID
|
||||
})
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user