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.
116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
/*
|
|
Copyright © 2025 maxtheweb
|
|
*/
|
|
package cmd
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// OAuthClient handles OAuth 1.0a authentication
|
|
type OAuthClient struct {
|
|
ConsumerKey string
|
|
ConsumerSecret string
|
|
AccessToken string
|
|
AccessTokenSecret string
|
|
}
|
|
|
|
// NewOAuthClient creates a new OAuth client from config
|
|
func NewOAuthClient(config *Config) *OAuthClient {
|
|
return &OAuthClient{
|
|
ConsumerKey: config.OAuth.ConsumerKey,
|
|
ConsumerSecret: config.OAuth.ConsumerSecret,
|
|
AccessToken: config.OAuth.AccessToken,
|
|
AccessTokenSecret: config.OAuth.AccessTokenSecret,
|
|
}
|
|
}
|
|
|
|
// GetAuthorizationHeader generates the OAuth 1.0a Authorization header
|
|
func (o *OAuthClient) GetAuthorizationHeader(method, requestURL string, params map[string]string) string {
|
|
// OAuth parameters
|
|
oauthParams := map[string]string{
|
|
"oauth_consumer_key": o.ConsumerKey,
|
|
"oauth_token": o.AccessToken,
|
|
"oauth_signature_method": "HMAC-SHA1",
|
|
"oauth_timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
|
"oauth_nonce": generateNonce(),
|
|
"oauth_version": "1.0",
|
|
}
|
|
|
|
// Combine OAuth params and request params for signature
|
|
allParams := make(map[string]string)
|
|
for k, v := range oauthParams {
|
|
allParams[k] = v
|
|
}
|
|
for k, v := range params {
|
|
allParams[k] = v
|
|
}
|
|
|
|
// Generate signature
|
|
signature := o.generateSignature(method, requestURL, allParams)
|
|
oauthParams["oauth_signature"] = signature
|
|
|
|
// Build Authorization header
|
|
var headerParts []string
|
|
for k, v := range oauthParams {
|
|
headerParts = append(headerParts, fmt.Sprintf(`%s="%s"`, percentEncode(k), percentEncode(v)))
|
|
}
|
|
sort.Strings(headerParts)
|
|
|
|
return "OAuth " + strings.Join(headerParts, ", ")
|
|
}
|
|
|
|
// generateSignature creates the OAuth signature
|
|
func (o *OAuthClient) generateSignature(method, requestURL string, params map[string]string) string {
|
|
// 1. Create parameter string
|
|
var paramPairs []string
|
|
for k, v := range params {
|
|
paramPairs = append(paramPairs, percentEncode(k)+"="+percentEncode(v))
|
|
}
|
|
sort.Strings(paramPairs)
|
|
paramString := strings.Join(paramPairs, "&")
|
|
|
|
// 2. Create signature base string
|
|
signatureBase := strings.Join([]string{
|
|
method,
|
|
percentEncode(requestURL),
|
|
percentEncode(paramString),
|
|
}, "&")
|
|
|
|
// 3. Create signing key
|
|
signingKey := percentEncode(o.ConsumerSecret) + "&" + percentEncode(o.AccessTokenSecret)
|
|
|
|
// 4. Calculate HMAC-SHA1 signature
|
|
mac := hmac.New(sha1.New, []byte(signingKey))
|
|
mac.Write([]byte(signatureBase))
|
|
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
|
|
return signature
|
|
}
|
|
|
|
// percentEncode encodes a string according to RFC 3986
|
|
func percentEncode(s string) string {
|
|
encoded := url.QueryEscape(s)
|
|
// url.QueryEscape uses + for spaces, but OAuth needs %20
|
|
encoded = strings.ReplaceAll(encoded, "+", "%20")
|
|
return encoded
|
|
}
|
|
|
|
// generateNonce generates a random nonce for OAuth
|
|
func generateNonce() string {
|
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
result := make([]byte, 32)
|
|
for i := range result {
|
|
result[i] = chars[rand.Intn(len(chars))]
|
|
}
|
|
return string(result)
|
|
}
|