summary refs log tree commit diff
diff options
context:
space:
mode:
authorvenomade <venomade@venomade.com>2024-12-16 20:24:06 +0000
committervenomade <venomade@venomade.com>2024-12-16 20:24:06 +0000
commit21874c403c7b04da9cf408a42dfd7aab6ae3177d (patch)
treef70ec15514b0a5b0ffdf92cb3b2eccba88cc982e
Initial Commit
-rw-r--r--.gitignore1
-rw-r--r--README.org4
-rw-r--r--authconf.go82
-rw-r--r--go.mod10
-rw-r--r--go.sum8
-rw-r--r--main.go87
-rw-r--r--main_test.go26
-rw-r--r--oauth.go57
-rw-r--r--secrets.go78
-rw-r--r--token.go90
10 files changed, 443 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f332936
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+lifesigns
\ No newline at end of file
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..7a8faf3
--- /dev/null
+++ b/README.org
@@ -0,0 +1,4 @@
+#+title: lifesigns: A Twitch Who's Live CLI
+
+* Lifesigns
+A CLI for status applications that shows live followed streamers
diff --git a/authconf.go b/authconf.go
new file mode 100644
index 0000000..2e9b81b
--- /dev/null
+++ b/authconf.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strconv"
+)
+
+type UTRSimple struct {
+	AccessToken  string   `json:"access_token"`
+	ExpiresIn    int      `json:"expires_in"`
+	RefreshToken string   `json:"refresh_token"`
+}
+
+func Auth(authFile string) UTRSimple {
+	_ , ferr := os.Stat(authFile)
+	if ferr != nil {
+		WriteAuth(authFile)
+	}
+	return ReadAuth(authFile)
+}
+
+func WriteAuth(authFile string) {
+	authToken, err := GetOAuth(clientID)
+	if err != nil {
+		panic(err)
+	}
+	
+	uT := GetUserToken(authToken)
+	
+	err = os.MkdirAll("/home/venomade/.local/share/lifesigns", 0755)
+	// TODO: unhardcode
+	
+	if err != nil {
+		panic(err)
+	}
+
+	file, err := os.Create(authFile)
+	if err != nil {
+		panic(err)
+	}
+	defer file.Close()
+
+	confString := fmt.Sprintf("%s\n%s\n%s", uT.AccessToken, uT.ExpiresIn, uT.RefreshToken)
+	_, err = file.WriteString(confString)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func ReadAuth(authFile string) UTRSimple{
+	file, err := os.Open(authFile)
+	if err != nil {
+		fmt.Println("Error opening file:", err)
+		panic(err)
+	}
+	defer file.Close()
+
+	// Create a slice to hold the lines of the file
+	var lines []string
+
+	// Read all lines from the file into the slice
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		lines = append(lines, scanner.Text())
+	}
+
+	var utrs UTRSimple
+	
+	// Process each line using the appropriate function
+	if len(lines) >= 2 { // Change to 3
+		utrs.AccessToken = lines[0]
+		exp, _ := strconv.Atoi(lines[1])
+		utrs.ExpiresIn = exp
+		utrs.RefreshToken = lines[2]
+	} else {
+		fmt.Println("File does not contain enough lines.")
+	}
+
+	return utrs
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..7476d14
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module venomade.com/lifesigns
+
+go 1.23.4
+
+require (
+	github.com/nicklaw5/helix/v2 v2.31.0
+	golang.org/x/oauth2 v0.24.0
+)
+
+require github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4b3fc02
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,8 @@
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/nicklaw5/helix/v2 v2.31.0 h1:/8E5H20D/f3PGmSWT5NWtjwt+M8/GeCjnK/AkoLIFQA=
+github.com/nicklaw5/helix/v2 v2.31.0/go.mod h1:e1GsZq4NDk9sQlPJ0Nr3+14R9cizqg09VAk7/IonpOU=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..462cd96
--- /dev/null
+++ b/main.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/nicklaw5/helix/v2"
+
+	"golang.org/x/oauth2/clientcredentials"
+)
+
+var (
+	clientSecrets = Secrets("/home/venomade/.local/share/lifesigns/secrets")
+	clientID = clientSecrets.ClientID
+	clientSecret = clientSecrets.ClientSecret // Refactor
+	authFile = "/home/venomade/.local/share/lifesigns/auth"
+	secretsFile = "/home/venomade/.local/share/lifesigns/secrets"
+	// Fix Hardcodes
+	oauth2Config *clientcredentials.Config
+)
+
+func main() {
+
+	userTokens := Auth(authFile);
+	
+	client, err := helix.NewClient(&helix.Options{
+		ClientID: clientID,
+		ClientSecret: clientSecret,
+		AppAccessToken: GetAppToken(),
+		UserAccessToken: userTokens.AccessToken,
+		RefreshToken: userTokens.RefreshToken,
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	// resp, err := client.GetUsers(&helix.UsersParams{
+	// 	IDs:    []string{"26301881", "18074328"},
+	// 	Logins: []string{"summit1g", "lirik"},
+	// })
+	
+	// resp, err := client.GetFollowedStream(&helix.FollowedStreamsParams{
+	// 	UserID: "venomade98",
+	// })
+	// resp, err := client.GetFollowedChannels(&helix.GetFollowedChannelParams{
+	// 	UserID: GetUserID(client),
+	// })
+
+	resp, err := client.GetFollowedStream(&helix.FollowedStreamsParams{
+		UserID: GetUserID(client),
+	})
+
+	// for _, channel := range resp.Data.FollowedChannels {
+	// 	fmt.Println("Channel:", channel.BroadcasterName)
+	// }
+	var output strings.Builder
+	
+	for _, channel := range resp.Data.Streams {
+		output.WriteString(fmt.Sprintf("%s ", channel.UserName))
+	}
+
+	fmt.Println(fmt.Sprintf("  %s", strings.TrimSpace(output.String())))
+}
+
+func GetUserID(client *helix.Client) string{
+	resp, err := client.GetUsers(&helix.UsersParams {
+		Logins: []string{"venomade98"},
+	})
+		
+	if err != nil {
+		panic(err)
+	}
+
+	// fmt.Printf("Status code: %d\n", resp.StatusCode)
+	// fmt.Printf("Error: %s\n", resp.ErrorMessage)
+	// fmt.Printf("Rate limit: %d\n", resp.GetRateLimit())
+	// fmt.Printf("Rate limit remaining: %d\n", resp.GetRateLimitRemaining())
+	// fmt.Printf("Rate limit reset: %d\n\n", resp.GetRateLimitReset())
+
+	var uid string;
+	
+	for _, user := range resp.Data.Users {
+		uid = string(user.ID)
+	}
+
+	return uid
+}
diff --git a/main_test.go b/main_test.go
new file mode 100644
index 0000000..a9386d0
--- /dev/null
+++ b/main_test.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"testing"
+)
+
+func TestOAuth(t *testing.T) {
+	result := GetOAuth(clientID, "/home/venomade/.local/share/lifesigns/auth")
+	if result != "" {
+		fmt.Println("OAuth: ", result)
+	} else {
+		t.Errorf("OAuth should be a code, got %s", result)
+	}
+}
+
+func TestOAuthNoFile(t *testing.T) {
+	os.Remove("/home/venomade/.local/share/lifesigns/auth")
+	result := GetOAuth(clientID, "/home/venomade/.local/share/lifesigns/auth")
+	if result != "" {
+		fmt.Println("OAuth: ", result)
+	} else {
+		t.Errorf("OAuth should be a code, got %s", result)
+	}
+}
diff --git a/oauth.go b/oauth.go
new file mode 100644
index 0000000..f41dde3
--- /dev/null
+++ b/oauth.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"os/exec"
+)
+
+func openBrowser(url string) error {
+	cmd := exec.Command("xdg-open", url)
+	return cmd.Start()
+}
+
+func startServer(codeChannel chan string) {
+	// Step 5: Start a web server that listens on localhost:3000
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		// Extract the code parameter from the query string
+		code := r.URL.Query().Get("code")
+		if code != "" {
+			// Send the authorization code to the channel
+			codeChannel <- code
+
+			// Respond to the browser
+			fmt.Fprintf(w, "Authorization successful! You can now close this window.")
+		} else {
+			// If there's no code, display an error
+			http.Error(w, "Authorization failed", http.StatusBadRequest)
+		}
+	})
+
+	// Start the server
+	err := http.ListenAndServe(":3000", nil)
+	if err != nil {
+		fmt.Printf("Error starting server: %v\n", err)
+	}
+}
+
+func GetOAuth(clientID string) (string, error) {
+	// Step 1: Construct the URL with the provided client_id
+	url := fmt.Sprintf("https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=http://localhost:3000&scope=user%%3Aread%%3Afollows&state=xsscheckbasic101", clientID)
+
+	// Step 2: Open the URL in the default web browser using xdg-open
+	err := openBrowser(url)
+	if err != nil {
+		return "", fmt.Errorf("failed to open browser: %v", err)
+	}
+
+	// Step 3: Start the HTTP server to capture the response
+	codeChannel := make(chan string)
+	go startServer(codeChannel)
+
+	// Step 4: Wait for the authorization code to be received
+	code := <-codeChannel
+
+	// Return the authorization code
+	return code, nil
+}
diff --git a/secrets.go b/secrets.go
new file mode 100644
index 0000000..e8f3762
--- /dev/null
+++ b/secrets.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+type SecretsConf struct {
+	ClientID     string
+	ClientSecret string
+}
+
+func Secrets(secretsFile string) SecretsConf {
+	_, ferr := os.Stat(secretsFile)
+	if ferr != nil {
+		// WriteSecrets(secretsFile)
+	}
+	return ReadSecrets(secretsFile)
+}
+
+// func WriteSecrets(secretsFile string) {
+// 	secretsToken, err := GetOAuth(clientID)
+// 	if err != nil {
+// 		panic(err)
+// 	}
+
+// 	uT := GetUserToken(secretsToken)
+
+// 	err = os.MkdirAll("/home/venomade/.local/share/lifesigns", 0755)
+// 	// TODO: unhardcode
+
+// 	if err != nil {
+// 		panic(err)
+// 	}
+
+// 	file, err := os.Create(secretsFile)
+// 	if err != nil {
+// 		panic(err)
+// 	}
+// 	defer file.Close()
+
+// 	confString := fmt.Sprintf("%s\n%s\n%s", uT.AccessToken, uT.ExpiresIn, uT.RefreshToken)
+// 	_, err = file.WriteString(confString)
+// 	if err != nil {
+// 		panic(err)
+// 	}
+// }
+
+func ReadSecrets(secretsFile string) SecretsConf {
+	file, err := os.Open(secretsFile)
+	if err != nil {
+		fmt.Println("Error opening file:", err)
+		panic(err)
+	}
+	defer file.Close()
+
+	// Create a slice to hold the lines of the file
+	var lines []string
+
+	// Read all lines from the file into the slice
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		lines = append(lines, scanner.Text())
+	}
+
+	var secrets SecretsConf
+
+	// Process each line using the appropriate function
+	if len(lines) >= 2 {
+		secrets.ClientID = lines[0]
+		secrets.ClientSecret = lines[1]
+	} else {
+		fmt.Println("File does not contain enough lines.")
+	}
+
+	return secrets
+}
diff --git a/token.go b/token.go
new file mode 100644
index 0000000..8ccbdb2
--- /dev/null
+++ b/token.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+
+	"golang.org/x/oauth2/clientcredentials"
+	"golang.org/x/oauth2/twitch"
+)
+
+type UserTokenResponse struct {
+	AccessToken  string   `json:"access_token"`
+	ExpiresIn    int      `json:"expires_in"`
+	RefreshToken string   `json:"refresh_token"`
+	Scope        []string `json:"scope"`
+	TokenType    string   `json:"token_type"`
+}
+
+func GetUserToken(authCode string) UserTokenResponse{
+
+	// Define the URL and form data
+	apiURL := "https://id.twitch.tv/oauth2/token" // Replace with the actual URL to send the POST request to
+
+	data := url.Values{}
+	data.Set("client_id", clientID)
+	data.Set("client_secret", clientSecret)
+	data.Set("code", authCode)
+	data.Set("grant_type", "authorization_code")
+	data.Set("redirect_uri", "http://localhost:3000")
+
+	// Create the POST request
+	req, err := http.NewRequest("POST", apiURL, bytes.NewBufferString(data.Encode()))
+	if err != nil {
+		fmt.Println("Error creating request:", err)
+		panic(err)
+	}
+
+	// Set the appropriate header for x-www-form-urlencoded
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	// Send the request
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("Error sending request:", err)
+		panic(err)
+	}
+	defer resp.Body.Close()
+
+	// Print the response status
+	// fmt.Println("Response Status:", resp.Status)
+
+	// You can read and print the body here if needed
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("Error reading response body:", err)
+		panic(err)
+	}
+	
+	var userTokenResponse UserTokenResponse
+
+	err = json.Unmarshal([]byte(body), &userTokenResponse)
+	if err != nil {
+		fmt.Println("Error unmarshaling JSON:", err)
+		panic(err)
+	}
+			
+	return userTokenResponse
+}
+
+func GetAppToken() string{
+	oauth2Config = &clientcredentials.Config{
+		ClientID:     clientID,
+		ClientSecret: clientSecret,
+		TokenURL:     twitch.Endpoint.TokenURL,
+	}
+
+	token, err := oauth2Config.Token(context.Background())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return token.AccessToken
+}