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
7 changes: 4 additions & 3 deletions .github/workflows/go-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '1.21.0'
go-version-file: 'src/cli/go.mod'
cache-dependency-path: 'src/cli/go.sum'

- name: Build
run: cd src/cli && go build -v ./...
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Windows](#windows)
- [Mac](#mac)
- [Authentication](#authentication)
- [Xquik Backend](#xquik-backend)
- [Docs](#docs)

## Usage
Expand Down Expand Up @@ -121,6 +122,24 @@ Note: running `x auth -v` windows might flag the tool as a threat this is becaus
- if anything went wrong run `x -c` to clear any settings and start over
- if the problem persists, kindly open an issue [here](https://github.com/devhindo/x/issues/new) and describe the problem and I'll be happy to help!

## Xquik Backend

Use the optional Xquik backend without changing the default authentication flow:

```bash
export X_BACKEND=xquik
export XQUIK_API_KEY="your-key"
export XQUIK_ACCOUNT="your-connected-account"
x tweet "Hello from Xquik"
```

`TWITTER_BACKEND=xquik` is an alternative selector. Set
`XQUIK_API_BASE_URL` only to override the default `https://xquik.com` origin.
Remote overrides must use HTTPS. Plain HTTP is accepted only for loopback
development addresses.

Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.


## Posting tweets

Expand Down
7 changes: 6 additions & 1 deletion src/cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import (
var rootCmd = &cobra.Command{
Use: "x",
Short: "x CLI - Post tweets from your terminal",
Long: `x is a CLI tool that allows you to post tweets to X (formerly Twitter) directly from your terminal.`,
Long: `x is a CLI tool that allows you to post tweets to X (formerly Twitter) directly from your terminal.

To use the optional Xquik backend, set X_BACKEND=xquik, XQUIK_API_KEY,
and XQUIK_ACCOUNT. TWITTER_BACKEND can replace X_BACKEND. Set
XQUIK_API_BASE_URL only when you need to override the default API URL.`,
Example: ` X_BACKEND=xquik XQUIK_API_KEY=your-key XQUIK_ACCOUNT=your-account x tweet "Hello"`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
// Join args just in case, but usually it's one arg "msg"
Expand Down
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")
}
}
42 changes: 20 additions & 22 deletions src/cli/lock/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,32 @@ import (
"io"
"os"
"path/filepath"

"github.com/google/uuid"
)

func GenerateLicenseKey() (string, error) {
uuid := uuid.New()
uuidBytes := uuid[:]
licenseKeyBytes := append(uuidBytes)
licenseKey := base64.StdEncoding.EncodeToString(licenseKeyBytes)
id := uuid.New()
licenseKey := base64.StdEncoding.EncodeToString(id[:])
return licenseKey, nil
}

func WriteLicenseKeyToFile(licenseKey string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("Error getting user home directory: %v", err)
return fmt.Errorf("Error getting user home directory: %v", err)
}

licenseFilePath := filepath.Join(homeDir, ".tempxcli")
licenseFile, err := os.Create(licenseFilePath)
if err != nil {
return fmt.Errorf("Error creating license file: %v", err)
return fmt.Errorf("Error creating license file: %v", err)
}
defer licenseFile.Close()

_, err = licenseFile.WriteString(licenseKey)
if err != nil {
return fmt.Errorf("Error writing license key to file: %v", err)
return fmt.Errorf("Error writing license key to file: %v", err)
}

return nil
Expand All @@ -42,36 +40,36 @@ func WriteLicenseKeyToFile(licenseKey string) error {
func ReadLicenseKeyFromFile() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("Error getting user home directory: %v", err)
return "", fmt.Errorf("Error getting user home directory: %v", err)
}

licenseFilePath := filepath.Join(homeDir, ".tempxcli")
licenseFile, err := os.Open(licenseFilePath)
if err != nil {
return "", fmt.Errorf("Error opening license file: %v", err)
return "", fmt.Errorf("Error opening license file: %v", err)
}
defer licenseFile.Close()

licenseFileBytes, err := io.ReadAll(licenseFile)
if err != nil {
return "", fmt.Errorf("Error reading license file: %v", err)
return "", fmt.Errorf("Error reading license file: %v", err)
}

licenseKey := string(licenseFileBytes)
return licenseKey, nil
}

func ClearLicenseFile() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("Error getting user home directory: %v", err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("Error getting user home directory: %v", err)
}

licenseFilePath := filepath.Join(homeDir, ".tempxcli")
err = os.Remove(licenseFilePath)
if err != nil {
return fmt.Errorf("Error deleting license file: %v", err)
}
licenseFilePath := filepath.Join(homeDir, ".tempxcli")
err = os.Remove(licenseFilePath)
if err != nil {
return fmt.Errorf("Error deleting license file: %v", err)
}

return nil
}
return nil
}
21 changes: 21 additions & 0 deletions src/cli/lock/key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lock

import (
"encoding/base64"
"testing"
)

func TestGenerateLicenseKey(t *testing.T) {
licenseKey, err := GenerateLicenseKey()
if err != nil {
t.Fatalf("GenerateLicenseKey() error = %v", err)
}

decoded, err := base64.StdEncoding.DecodeString(licenseKey)
if err != nil {
t.Fatalf("DecodeString() error = %v", err)
}
if len(decoded) != 16 {
t.Fatalf("decoded license key length = %d, want 16", len(decoded))
}
}
64 changes: 31 additions & 33 deletions src/cli/tweet/future.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@ package tweet
// x t "hi" 5h6m7s

import (
"fmt"
"log"
"strconv"
"strings"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"

"github.com/devhindo/x/src/cli/lock"
)

type FutureTweet struct {
License string `json:"license"`
Tweet string `json:"tweet"`
Hours int `json:"hours"`
Minutes int `json:"minutes"`
Hours int `json:"hours"`
Minutes int `json:"minutes"`
}

func PostFutureTweet(c []string) {

url := "http://localhost:3000/api/tweets/future"

// x t "hi" 5h6m7s
Expand All @@ -40,18 +40,18 @@ func PostFutureTweet(c []string) {
log.SetFlags(0)
log.Fatal(err)
}

license, err := lock.ReadLicenseKeyFromFile()

if err != nil {
fmt.Println("you are not authenticated | try 'x auth'")
return
}

tweet := FutureTweet{
License: license,
Tweet: tweetText,
Hours: hrs,
Tweet: tweetText,
Hours: hrs,
Minutes: mins,
}

Expand All @@ -60,36 +60,34 @@ func PostFutureTweet(c []string) {
if err != nil {
log.SetFlags(0)
log.Fatal(err)
}
}
}


func postFutureTweetToServer(url string, t FutureTweet) error {
fmt.Println("unmarchalling")
jsonBytes, err := json.Marshal(t)
if err != nil {
panic(err)
}
if err != nil {
panic(err)
}

resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBytes))
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBytes))
fmt.Println("posting")
if err != nil {
return fmt.Errorf("can't reach server to post a tweet")
}
if err != nil {
return fmt.Errorf("can't reach server to post a tweet")
}
fmt.Println("before defer")
defer resp.Body.Close()
defer resp.Body.Close()
fmt.Println("after defer")
_, err = io.ReadAll(resp.Body)
if err != nil {
//Failed to read response.
return fmt.Errorf("can't read server response")
}

var r response
if err != nil {
//Failed to read response.
return fmt.Errorf("can't read server response")
}

var r response

//Convert bytes to String and print
fmt.Println(r.Message)
//Convert bytes to String and print
fmt.Println(r.Message)

return nil
}
Expand All @@ -98,6 +96,9 @@ func handleFutureTweetArgs(c []string) (string, string, error) {
if len(c) < 3 {
return "", "", fmt.Errorf("no tweet given | try 'x f --help'")
}
if len(c) < 4 {
return "", "", fmt.Errorf("no schedule time given | try 'x f --help'")
}

if c[2] == "-h" || c[2] == "--help" {
fmt.Println("post future tweets")
Expand All @@ -109,9 +110,6 @@ func handleFutureTweetArgs(c []string) (string, string, error) {
return c[2], c[3], nil
}

if len(c) < 4 {
fmt.Println("No schedule time is given | try 'x f --help'")
}
return c[2], c[3], nil
}

Expand Down Expand Up @@ -167,4 +165,4 @@ func handleTweetTime(t string) (int, int, error) {
}

return hrs, mins, nil
}
}
15 changes: 14 additions & 1 deletion src/cli/tweet/future_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"testing"
)


func TestHandleTweetTime(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -103,6 +102,20 @@ func TestHandleFutureTweetArgs(t *testing.T) {
wantTime: "",
wantErr: true,
},
{
name: "missing schedule time",
args: []string{"x", "f", "hello world"},
wantTweet: "",
wantTime: "",
wantErr: true,
},
{
name: "help without schedule time",
args: []string{"x", "f", "--help"},
wantTweet: "",
wantTime: "",
wantErr: true,
},
}

for _, tt := range tests {
Expand Down
Loading