/* Copyright Ā© 2025 MAX THE WEB */ package cmd import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "time" "github.com/BurntSushi/toml" "github.com/spf13/cobra" ) // Config structure type Config struct { BearerToken string `toml:"bearer_token"` API struct { BaseURL string `toml:"base_url"` Timeout int `toml:"timeout"` } `toml:"api"` Limits struct { MonthlyReads int `toml:"monthly_reads"` MonthlyPosts int `toml:"monthly_posts"` } `toml:"limits"` } // IntOrString handles JSON fields that can be either int or string type IntOrString int func (i *IntOrString) UnmarshalJSON(data []byte) error { // Try to unmarshal as int first var intVal int if err := json.Unmarshal(data, &intVal); err == nil { *i = IntOrString(intVal) return nil } // If that fails, try string var strVal string if err := json.Unmarshal(data, &strVal); err != nil { return err } // Convert string to int parsed, err := strconv.Atoi(strVal) if err != nil { return err } *i = IntOrString(parsed) return nil } // UsageResponse from X API type UsageResponse struct { Data struct { ProjectCap IntOrString `json:"project_cap"` // Total monthly quota ProjectUsage IntOrString `json:"project_usage"` // Already consumed CapResetDay int `json:"cap_reset_day"` // Days until reset ProjectID string `json:"project_id"` // Project identifier } `json:"data"` } var statusDays int // statusCmd represents the status command var statusCmd = &cobra.Command{ Use: "status", Short: "Check X API quota and rate limits", Long: `Shows your current X API usage status including: - Monthly quota usage (total API operations) - Time until quota reset - Current rate limit window status`, Run: func(cmd *cobra.Command, args []string) { runStatus() }, } func init() { rootCmd.AddCommand(statusCmd) statusCmd.Flags().IntVarP(&statusDays, "days", "d", 7, "Number of days of usage history (1-90)") } func runStatus() { // Load configuration config, err := loadConfig() if err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) fmt.Println("Run 'xapi login' to configure your credentials") os.Exit(1) } // Check if token is configured if config.BearerToken == "" || config.BearerToken == "YOUR_BEARER_TOKEN_HERE" { fmt.Fprintf(os.Stderr, "Bearer token not configured\n") fmt.Println("Run 'xapi login' to configure your credentials") os.Exit(1) } // Create HTTP client client := &http.Client{ Timeout: time.Duration(config.API.Timeout) * time.Second, } // Validate days parameter if statusDays < 1 || statusDays > 90 { fmt.Fprintf(os.Stderr, "Error: --days must be between 1 and 90\n") os.Exit(1) } // Build API URL with days parameter apiURL := fmt.Sprintf("%s/usage/tweets?days=%d", config.API.BaseURL, statusDays) // Make API request to usage endpoint req, err := http.NewRequest("GET", apiURL, nil) if err != nil { fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err) os.Exit(1) } req.Header.Set("Authorization", "Bearer "+config.BearerToken) // Execute request resp, err := client.Do(req) if err != nil { fmt.Fprintf(os.Stderr, "Error making API request: %v\n", err) os.Exit(1) } defer resp.Body.Close() // Check status code if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Fprintf(os.Stderr, "API error (status %d): %s\n", resp.StatusCode, string(body)) os.Exit(1) } // Parse response var usage UsageResponse if err := json.NewDecoder(resp.Body).Decode(&usage); err != nil { fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err) os.Exit(1) } // Display status displayStatus(usage, resp.Header, config) } func loadConfig() (*Config, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, err } configFile := filepath.Join(homeDir, ".config", "xapi", "config.toml") var config Config if _, err := toml.DecodeFile(configFile, &config); err != nil { return nil, err } // Set defaults if not specified if config.API.BaseURL == "" { config.API.BaseURL = "https://api.x.com/2" } if config.API.Timeout == 0 { config.API.Timeout = 30 } if config.Limits.MonthlyReads == 0 { config.Limits.MonthlyReads = 100 } if config.Limits.MonthlyPosts == 0 { config.Limits.MonthlyPosts = 500 } return &config, nil } func displayStatus(usage UsageResponse, headers http.Header, config *Config) { fmt.Println("\nšŸ“Š MONTHLY QUOTA STATUS") fmt.Println("═══════════════════════") // Calculate usage (convert IntOrString to int) cap := int(usage.Data.ProjectCap) used := int(usage.Data.ProjectUsage) remaining := cap - used percentage := float64(used) / float64(cap) * 100 // Display quota (clarify these are total API operations) fmt.Printf("ā”œā”€ Type: %s Tier (%d total operations/month)\n", getAccessTier(cap), cap) fmt.Printf("ā”œā”€ Used: %d / %d operations (%.1f%%)\n", used, cap, percentage) fmt.Printf("ā”œā”€ Remaining: %d operations\n", remaining) fmt.Printf("└─ Resets in: %d days\n", usage.Data.CapResetDay) // Display rate limit headers fmt.Println("\nā±ļø 15-MIN RATE WINDOW") fmt.Println("═════════════════════") limit := headers.Get("x-rate-limit-limit") remainingRate := headers.Get("x-rate-limit-remaining") reset := headers.Get("x-rate-limit-reset") if limit != "" { fmt.Printf("ā”œā”€ Requests: %s / %s available\n", remainingRate, limit) } if reset != "" { resetTime, _ := strconv.ParseInt(reset, 10, 64) resetAt := time.Unix(resetTime, 0) timeUntil := time.Until(resetAt) fmt.Printf("└─ Resets in: %s\n", formatDuration(timeUntil)) } // Display recommendations fmt.Println("\nšŸ’” RECOMMENDATIONS") fmt.Println("═════════════════") if remaining > 0 { daysLeft := usage.Data.CapResetDay if daysLeft > 0 { dailyBudget := float64(remaining) / float64(daysLeft) fmt.Printf("ā”œā”€ You have %d operations left for %d days\n", remaining, daysLeft) fmt.Printf("ā”œā”€ That's ~%.1f operations per day\n", dailyBudget) fmt.Printf("└─ Each search query will consume 1 operation\n") } } else { fmt.Println("└─ āš ļø No operations remaining! Consider upgrading your plan.") } fmt.Println() } // getAccessTier determines the access tier based on monthly cap func getAccessTier(cap int) string { switch { case cap <= 100: return "Free" case cap <= 10000: return "Basic" case cap <= 1000000: return "Pro" default: return "Enterprise" } } func formatDuration(d time.Duration) string { if d < 0 { return "0 seconds" } minutes := int(d.Minutes()) seconds := int(d.Seconds()) % 60 if minutes > 0 { return fmt.Sprintf("%d minutes %d seconds", minutes, seconds) } return fmt.Sprintf("%d seconds", seconds) }