overhaul config, refactor twitch labels, init satellite

This commit is contained in:
ari melody 2026-07-24 17:24:07 +01:00
parent b2485eda79
commit 097a36ac69
Signed by: ari
GPG key ID: CF99829C92678188
6 changed files with 161 additions and 152 deletions

View file

@ -1,77 +0,0 @@
package oauth
import (
"context"
"crypto/rand"
"fmt"
"log"
"net/http"
"sync"
"golang.org/x/oauth2"
)
func GenerateToken(
ctx *context.Context,
oauth2Config *oauth2.Config,
) (*oauth2.Token, error) {
verifier := oauth2.GenerateVerifier()
state := rand.Text()
var token *oauth2.Token
wg := sync.WaitGroup{}
var server http.Server
server.Addr = "127.0.0.1"
server.Handler = http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
scope := r.URL.Query().Get("scope")
resState := r.URL.Query().Get("state")
if len(code) == 0 || len(scope) == 0 || resState != state {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
t, err := oauth2Config.Exchange(*ctx, code, oauth2.VerifierOption(verifier))
if err != nil {
log.Fatalf("Could not exchange OAuth2 code: %v", err)
http.Error( w,
fmt.Sprintf("Could not exchange OAuth2 code: %v", err),
http.StatusBadRequest,
)
return
}
token = t
http.Error(
w,
"Authentication successful! You may now close this tab.",
http.StatusOK,
)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
wg.Done()
server.Close()
},
)
url := oauth2Config.AuthCodeURL(
state,
oauth2.AccessTypeOffline,
oauth2.S256ChallengeOption(verifier),
)
log.Printf("Log in with Twitch: %s\n\n", url)
wg.Add(1)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
return nil, fmt.Errorf("http: %v", err)
}
wg.Wait()
return token, nil
}

View file

@ -1,6 +1,6 @@
const labelEl = document.getElementById("label");
const selectedLabel = new URLSearchParams(location.search).get("l");
const label = location.pathname.split("/twitch/labels/")[1];
const labelLatestFollower = "latest-follower"
const labelLatestSubscriber = "latest-subscriber"
const labelLatestCheer = "latest-cheer"
@ -11,14 +11,11 @@ const allowedLabels = [
]
/**
* @param {string} selectedLabel
* @param {string} label
*/
async function sync() {
if (selectedLabel == null) {
const exampleUrl =
location.pathname +
"?l=" +
labelLatestFollower;
if (label == null) {
const exampleUrl = `${location.protocol}//${location.host}/twitch/labels/${labelLatestFollower}`;
labelEl.innerHTML =
"Needs label to listen to, " +
@ -29,7 +26,7 @@ async function sync() {
let eventSource;
try {
eventSource = new EventSource("/twitch/sse?l=" + selectedLabel);
eventSource = new EventSource("/twitch/sse/" + label);
} catch (err) {
console.error("Failed to connect to event source", err);
return;
@ -46,7 +43,7 @@ async function sync() {
});
eventSource.addEventListener("update", event => {
console.log(event.data);
console.log(`[${label}] ${event.data}`);
labelEl.innerText = event.data;
});

View file

@ -40,16 +40,16 @@ type (
}
ServiceOptions struct {
Host string
Port int16
}
serviceConfig struct {
ChannelName string `json:"channel_name"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
ChannelName string
ClientID string
ClientSecret string
}
Service struct {
host string
port int16
channelName string
@ -85,30 +85,9 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
if err := os.MkdirAll(DATA_PATH, 0750); err != nil { panic(err) }
if err := os.MkdirAll(path.Join(DATA_PATH, "state"), 0750); err != nil { panic(err) }
config := serviceConfig{}
if configFile, err := os.OpenFile(CONFIG_FILEPATH, os.O_CREATE | os.O_RDWR, 0600); err != nil {
return nil, fmt.Errorf("open %s: %v", CONFIG_FILEPATH, err)
} else {
defer configFile.Close()
stat, err := configFile.Stat()
if err != nil { return nil, fmt.Errorf("stat %s: %v", CONFIG_FILEPATH, err) }
if stat.Size() == 0 {
enc := json.NewEncoder(configFile)
enc.SetIndent("", "\t")
if err := enc.Encode(&config); err != nil {
return nil, fmt.Errorf("write %s: %v", CONFIG_FILEPATH, err)
}
return nil, fmt.Errorf("Config file is empty: %s", CONFIG_FILEPATH)
} else if err := json.NewDecoder(configFile).Decode(&config); err != nil {
return nil, fmt.Errorf("read %s: %v", CONFIG_FILEPATH, err)
}
}
if len(config.ChannelName) == 0 { return nil, errors.New("config: channel_name cannot be empty") }
if len(config.ClientID) == 0 { return nil, errors.New("config: client_id cannot be empty") }
if len(config.ClientSecret) == 0 { return nil, errors.New("config: client_secret cannot be empty") }
if len(opts.ChannelName) == 0 { return nil, errors.New("config: channel_name cannot be empty") }
if len(opts.ClientID) == 0 { return nil, errors.New("config: client_id cannot be empty") }
if len(opts.ClientSecret) == 0 { return nil, errors.New("config: client_secret cannot be empty") }
latestFollowerC := make(chan string)
latestFollowerBroadcast := broadcast.NewBroadcastChannel(
@ -121,6 +100,7 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
ctx, latestCheerC)
srv := &Service{
host: opts.Host,
port: opts.Port,
labels: &twitchLabels{
LatestFollower: &twitchLabel{
@ -136,12 +116,12 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
Broadcast: latestCheerBroadcast,
},
},
channelName: config.ChannelName,
clientID: config.ClientID,
clientSecret: config.ClientSecret,
channelName: opts.ChannelName,
clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthConfig: &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
Endpoint: twitch.Endpoint,
Scopes: []string{
"moderator:read:followers",
@ -156,7 +136,7 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
"channel:read:hype_train",
"moderator:read:shoutouts",
},
RedirectURL: fmt.Sprintf("http://localhost:%d/twitch/auth", opts.Port),
RedirectURL: fmt.Sprintf("http://%s:%d/twitch/auth", opts.Host, opts.Port),
},
}
@ -223,13 +203,13 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
go srv.start(ctx)
})
group.GET("/sse", func(ctx *gin.Context) {
group.GET("/sse/:label", func(ctx *gin.Context) {
ctx.Header("connection", "keep-alive")
listeningTo := ctx.Query("l")
labelParam := ctx.Param("label")
var label *twitchLabel
switch listeningTo {
switch labelParam {
case LABEL_LATEST_FOLLOWER:
label = srv.labels.LatestFollower
case LABEL_LATEST_SUBSCRIBER:
@ -237,7 +217,7 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
case LABEL_LATEST_CHEER:
label = srv.labels.LatestCheer
default:
ctx.String(http.StatusBadRequest, "Unknown label %s", listeningTo)
ctx.String(http.StatusBadRequest, "Unknown label %s", labelParam)
return
}
@ -257,14 +237,24 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
})
})
group.GET("/label", func(ctx *gin.Context) {
group.GET("/labels/:label", func(ctx *gin.Context) {
labelParam := ctx.Param("label")
switch labelParam {
case LABEL_LATEST_FOLLOWER:
case LABEL_LATEST_SUBSCRIBER:
case LABEL_LATEST_CHEER:
default:
http.NotFound(ctx.Writer, ctx.Request)
return
}
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/labels.html")
})
}
func (srv *Service) Run(ctx context.Context) {
if srv.oauthToken == nil || !srv.oauthToken.Valid() {
log.Printf("Log in with Twitch: http://localhost:%d/twitch/login", srv.port)
log.Printf("Log in with Twitch: http://%s:%d/twitch/login", srv.host, srv.port)
} else {
if err := srv.start(ctx); err != nil {
log.Printf("Failed to start Twitch service: %v", err)