Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cli/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ func Help() {
fmt.Println(" -t \"text\" post a tweet")
fmt.Println(" -v show version")
fmt.Println(" -c clear authorized account")
fmt.Println(" X_BACKEND=xquik uses XQUIK_API_KEY and XQUIK_ACCOUNT")
fmt.Println()
fmt.Println("LEARN MORE")
fmt.Println(" Cheack source code at: https://github.com/devhindo/x")
}
}
51 changes: 28 additions & 23 deletions src/cli/tweet/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ type Tweet struct {
}

func POST_tweet(t string) {
if useXquikBackend() {
if err := postTweetWithXquik(t); err != nil {
fmt.Println(err)
os.Exit(1)
}
return
}

license, err := lock.ReadLicenseKeyFromFile()

Expand All @@ -30,45 +37,43 @@ func POST_tweet(t string) {
License: license,
Tweet: t,
}

postT(url, tweet)
}

type response struct {
Message string `json:"message"`
Message string `json:"message"`
}

func postT(url string, t Tweet) {

jsonBytes, err := json.Marshal(t)
if err != nil {
panic(err)
}
jsonBytes, err := json.Marshal(t)
if err != nil {
panic(err)
}

resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBytes))
if err != nil {
fmt.Println("can't reach server to post a tweet")
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBytes))
if err != nil {
fmt.Println("can't reach server to post a tweet")
os.Exit(0)
}
}

defer resp.Body.Close()
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
//Failed to read response.
panic(err)
}

var r response
err = json.Unmarshal(body, &r)
if err != nil {
fmt.Printf("rate limit reached thanks Elon! Try again tomorrow when it resets, sorry.")
//Failed to unmarshal response.
return
}

var r response
err = json.Unmarshal(body, &r)
if err != nil {
fmt.Printf("rate limit reached thanks Elon! Try again tomorrow when it resets, sorry.")
//Failed to unmarshal response.
return
}


//Convert bytes to String and print
fmt.Println(r.Message)
}
//Convert bytes to String and print
fmt.Println(r.Message)
}
120 changes: 120 additions & 0 deletions src/cli/tweet/xquik.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package tweet

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)

const defaultXquikBaseURL = "https://xquik.com"

type xquikConfig struct {
APIKey string
Account string
BaseURL string
}

type xquikTweetRequest struct {
Account string `json:"account"`
Text string `json:"text"`
}

type xquikTweetResponse struct {
TweetID string `json:"tweetId"`
Success bool `json:"success"`
Status string `json:"status"`
WriteActionID string `json:"writeActionId"`
Message string `json:"message"`
Error string `json:"error"`
}

func useXquikBackend() bool {
backend := os.Getenv("X_BACKEND")
if backend == "" {
backend = os.Getenv("TWITTER_BACKEND")
}
return strings.EqualFold(backend, "xquik")
}

func xquikConfigFromEnv() (xquikConfig, error) {
cfg := xquikConfig{
APIKey: strings.TrimSpace(os.Getenv("XQUIK_API_KEY")),
Account: strings.TrimSpace(os.Getenv("XQUIK_ACCOUNT")),
BaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("XQUIK_API_BASE_URL")), "/"),
}

if cfg.APIKey == "" {
return cfg, fmt.Errorf("XQUIK_API_KEY is required when X_BACKEND=xquik")
}
if cfg.Account == "" {
return cfg, fmt.Errorf("XQUIK_ACCOUNT is required when X_BACKEND=xquik")
}
if cfg.BaseURL == "" {
cfg.BaseURL = defaultXquikBaseURL
}
return cfg, nil
}

func postTweetWithXquik(text string) error {
cfg, err := xquikConfigFromEnv()
if err != nil {
return err
}
return postTweetWithXquikClient(http.DefaultClient, cfg, text, os.Stdout)
}

func postTweetWithXquikClient(client *http.Client, cfg xquikConfig, text string, out io.Writer) error {
payload := xquikTweetRequest{
Account: cfg.Account,
Text: text,
}
jsonBytes, err := json.Marshal(payload)
if err != nil {
return err
}

req, err := http.NewRequest("POST", cfg.BaseURL+"/api/v1/x/tweets", bytes.NewBuffer(jsonBytes))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", cfg.APIKey)

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("can't reach Xquik to post a tweet")
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("can't read Xquik response")
}

var result xquikTweetResponse
if len(body) > 0 {
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("can't parse Xquik response")
}
}

if resp.StatusCode == http.StatusOK && result.TweetID != "" {
fmt.Fprintf(out, "tweet posted with Xquik: %s\n", result.TweetID)
return nil
}
if resp.StatusCode == http.StatusAccepted && result.WriteActionID != "" {
fmt.Fprintf(out, "tweet accepted by Xquik; confirmation pending: %s\n", result.WriteActionID)
return nil
}
if result.Message != "" {
return fmt.Errorf("Xquik request failed: %s", result.Message)
}
if result.Error != "" {
return fmt.Errorf("Xquik request failed: %s", result.Error)
}
return fmt.Errorf("Xquik request failed with status %d", resp.StatusCode)
}
126 changes: 126 additions & 0 deletions src/cli/tweet/xquik_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package tweet

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestUseXquikBackend(t *testing.T) {
t.Setenv("X_BACKEND", "xquik")
t.Setenv("TWITTER_BACKEND", "")

if !useXquikBackend() {
t.Fatal("expected X_BACKEND=xquik to enable the Xquik backend")
}
}

func TestXquikConfigFromEnvRequiresCredentials(t *testing.T) {
t.Setenv("XQUIK_API_KEY", "")
t.Setenv("XQUIK_ACCOUNT", "")
t.Setenv("XQUIK_API_BASE_URL", "")

_, err := xquikConfigFromEnv()
if err == nil || !strings.Contains(err.Error(), "XQUIK_API_KEY") {
t.Fatalf("expected missing API key error, got %v", err)
}
}

func TestPostTweetWithXquikClientPostsTweet(t *testing.T) {
var gotRequest xquikTweetRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Fatalf("method = %s, want POST", r.Method)
}
if r.URL.Path != "/api/v1/x/tweets" {
t.Fatalf("path = %s, want /api/v1/x/tweets", r.URL.Path)
}
if r.Header.Get("X-API-Key") != "test-key" {
t.Fatalf("X-API-Key header = %q", r.Header.Get("X-API-Key"))
}
if err := json.NewDecoder(r.Body).Decode(&gotRequest); err != nil {
t.Fatalf("decode request: %v", err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"success":true,"tweetId":"123"}`))
}))
defer server.Close()

var out bytes.Buffer
err := postTweetWithXquikClient(server.Client(), xquikConfig{
APIKey: "test-key",
Account: "@example",
BaseURL: server.URL,
}, "hello", &out)
if err != nil {
t.Fatalf("postTweetWithXquikClient returned error: %v", err)
}
if gotRequest.Account != "@example" || gotRequest.Text != "hello" {
t.Fatalf("request = %+v", gotRequest)
}
if out.String() != "tweet posted with Xquik: 123\n" {
t.Fatalf("output = %q", out.String())
}
}

func TestPostTweetWithXquikClientAcceptsPendingConfirmation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte(`{"status":"pending_confirmation","writeActionId":"42"}`))
}))
defer server.Close()

var out bytes.Buffer
err := postTweetWithXquikClient(server.Client(), xquikConfig{
APIKey: "test-key",
Account: "@example",
BaseURL: server.URL,
}, "hello", &out)
if err != nil {
t.Fatalf("postTweetWithXquikClient returned error: %v", err)
}
if out.String() != "tweet accepted by Xquik; confirmation pending: 42\n" {
t.Fatalf("output = %q", out.String())
}
}

func TestPostTweetWithXquikClientReturnsAPIError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"message":"invalid API key"}`))
}))
defer server.Close()

var out bytes.Buffer
err := postTweetWithXquikClient(server.Client(), xquikConfig{
APIKey: "test-key",
Account: "@example",
BaseURL: server.URL,
}, "hello", &out)
if err == nil || !strings.Contains(err.Error(), "invalid API key") {
t.Fatalf("expected API error, got %v", err)
}
if out.String() != "" {
t.Fatalf("output = %q", out.String())
}
}

func TestPostTweetWithXquikUsesDefaultBaseURL(t *testing.T) {
t.Setenv("XQUIK_API_KEY", "test-key")
t.Setenv("XQUIK_ACCOUNT", "@example")
t.Setenv("XQUIK_API_BASE_URL", "")

cfg, err := xquikConfigFromEnv()
if err != nil {
t.Fatalf("xquikConfigFromEnv returned error: %v", err)
}
if cfg.BaseURL != defaultXquikBaseURL {
t.Fatalf("BaseURL = %q", cfg.BaseURL)
}
}