Complete CLI tool with 4 core commands: - xapi login: Configure OAuth credentials via editor - xapi status: Test authentication - xapi search: Search tweets with preview/execute modes - xapi create: Post tweets with preview/execute modes Features: - OAuth 1.0a authentication with HMAC-SHA1 signing - OAuth 2.0 Client ID/Secret support (for future features) - TOML-based configuration - Editor integration for config management - Helpful error messages for permission issues - Quota-aware design (no caching to avoid complexity) Built for developers on Free/Basic X API tiers.
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
/*
|
|
Copyright © 2025 maxtheweb
|
|
*/
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Config structure
|
|
type Config struct {
|
|
OAuth struct {
|
|
ConsumerKey string `toml:"consumer_key"`
|
|
ConsumerSecret string `toml:"consumer_secret"`
|
|
AccessToken string `toml:"access_token"`
|
|
AccessTokenSecret string `toml:"access_token_secret"`
|
|
} `toml:"oauth"`
|
|
OAuth2 struct {
|
|
ClientID string `toml:"client_id"`
|
|
ClientSecret string `toml:"client_secret"`
|
|
} `toml:"oauth2"`
|
|
}
|
|
|
|
// UserMeResponse from X API /2/users/me
|
|
type UserMeResponse struct {
|
|
Data struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Username string `json:"username"`
|
|
} `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
|
|
// statusCmd represents the status command
|
|
var statusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "Test OAuth authentication",
|
|
Long: `Test your OAuth 1.0a credentials by authenticating with X API.
|
|
|
|
This command verifies that your credentials are valid by making an
|
|
authenticated request to /2/users/me to fetch your user information.
|
|
|
|
Run this after 'xapi login' to confirm your setup is correct.`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
runStatus()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(statusCmd)
|
|
}
|
|
|
|
func runStatus() {
|
|
fmt.Println("\nTESTING AUTHENTICATION")
|
|
fmt.Println("======================")
|
|
|
|
// Load OAuth config
|
|
config, err := loadConfig()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
|
fmt.Println("\nRun 'xapi login' to configure your credentials")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check OAuth credentials
|
|
if config.OAuth.ConsumerKey == "" {
|
|
fmt.Fprintf(os.Stderr, "OAuth credentials not configured\n")
|
|
fmt.Println("Run 'xapi login' to configure your credentials")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("\nAuthenticating with X API...")
|
|
|
|
// Make authenticated request to /2/users/me
|
|
apiURL := "https://api.x.com/2/users/me"
|
|
|
|
// Create HTTP client
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", apiURL, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Generate OAuth 1.0a Authorization header
|
|
oauthClient := NewOAuthClient(config)
|
|
authHeader := oauthClient.GetAuthorizationHeader("GET", apiURL, map[string]string{})
|
|
req.Header.Set("Authorization", authHeader)
|
|
|
|
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()
|
|
|
|
// Read response body
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check status
|
|
if resp.StatusCode != http.StatusOK {
|
|
fmt.Fprintf(os.Stderr, "\nAUTHENTICATION FAILED\n")
|
|
fmt.Fprintf(os.Stderr, "Status: %d\n", resp.StatusCode)
|
|
fmt.Fprintf(os.Stderr, "Response: %s\n", string(body))
|
|
fmt.Println("\nPlease check your OAuth credentials:")
|
|
fmt.Println(" - Consumer Key (API Key)")
|
|
fmt.Println(" - Consumer Secret (API Secret)")
|
|
fmt.Println(" - Access Token")
|
|
fmt.Println(" - Access Token Secret")
|
|
fmt.Println("\nRun 'xapi login' to reconfigure")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Parse response
|
|
var userResp UserMeResponse
|
|
if err := json.Unmarshal(body, &userResp); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check for API errors
|
|
if len(userResp.Errors) > 0 {
|
|
fmt.Println("\nAUTHENTICATION FAILED")
|
|
for _, err := range userResp.Errors {
|
|
fmt.Printf(" - %s\n", err.Message)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Success!
|
|
fmt.Println("\nAUTHENTICATION SUCCESSFUL")
|
|
fmt.Println("=========================")
|
|
fmt.Printf("\nAuthenticated as:\n")
|
|
fmt.Printf(" Name: %s\n", userResp.Data.Name)
|
|
fmt.Printf(" Username: @%s\n", userResp.Data.Username)
|
|
fmt.Printf(" User ID: %s\n", userResp.Data.ID)
|
|
fmt.Println("\nYour OAuth credentials are working correctly!")
|
|
fmt.Println("You can now use 'xapi search' and 'xapi create'")
|
|
fmt.Println()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|