diff --git a/cmd/bot/main.go b/cmd/bot/main.go index c9a3a66..7de8821 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -1,4 +1,4 @@ -package main +package main import ( "log" @@ -20,16 +20,21 @@ func main() { } }() +_ = godotenv.Load() + log.Println("OAuth server started on port 8080") - botUsername := os.Getenv("nz_chatterbot") - botOAuth := os.Getenv("BOT_OAUTH") - channel := os Getenv("brycefromnz101") + botUsername := os.Getenv("BOT_USERNAME") + botAccess := os.Getenv("BOT_ACCESS_TOKEN") + botRefresh := os.Getenv("BOT_REFRESH_TOKEN") + channel := os Getenv("CHANNEL") clientID := os.Getenv("TWITCH_CLIENT_ID") - clientSecret := os.Getenv("TWITCH_CLIENT_SECRET") + clientSecret := os.Getenv("TWITCH_CLIENT_SECRET") + channelAccess := os.Getenv("STREAMER_ACCESS_TOKEN") + channelRefresh := os.Getenv("STREAMER_REFRESH_TOKEN") botChannel := botUsername - if botUsername == "" || botOAuth == "" || channel == "" || clientID == "" || clientSecret == "" { + if botUsername == "" || botAccess == "" || channel == "" || clientID == "" || clientSecret == "" { log.Fatal("Please set Bot_Username, Bot_OAuth, Channel, Twitch_Client_ID, and Twitch_Client_Secret env vars") } diff --git a/go.mod b/go.mod index 70b41f8..f513f8c 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,7 @@ require ( github.com/nicklaw5/helix v1.25.0 ) -require github.com/golang-jwt/jwt v3.2.2+incompatible // indirect +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/joho/godotenv v1.5.1 // indirect +) diff --git a/go.sum b/go.sum index 61991e0..f1a45af 100644 --- a/go.sum +++ b/go.sum @@ -3,5 +3,7 @@ github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/j github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M= github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw= diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index 91151d3..4edcfac 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -39,6 +39,7 @@ func StartOAuthServer(addr string) error { http.HandleFunc("/", handleIndex) http.HandleFunc("/callback", handleCallback) + http.HandleFunc("/refresh", handleRefresh) log.Printf("OAuth server listening on %s", addr) return http.ListenAndServe(addr, nil) @@ -46,7 +47,7 @@ func StartOAuthServer(addr string) error { func handleIndex(w http.ResponseWriter, r *http.Request) { authURL := fmt.Sprintf( - "https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s", + "https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&force_verify=true", url.QueryEscape(clientID), url.QueryEscape(redirectURI), url.QueryEscape(strings.Join(scopes, " ")), @@ -77,10 +78,26 @@ func handleCallback(w http.ResponseWriter, r *http.Request) { } jsonData, _ := json.MarshalIndent(token, "", " ") - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/ijson") w.Write(jsonData) } +func handleRefresh(w http.ResponseWriter, r *http.Request) { + refresh := r.URL.Query().Get("refresh_token") + if refresh == "" { + http.Error(w, "Missing refresh_token parameter", http.StatusBadRequest) + return + } + tr, err := ExchangeRefreshToken(refresh) + if err != nil { + http.Error(w, "Refresh failed: "+err.Error(), http.StatusInternalServerError) + return + } + data, _ := json.MarshalIndent(tr, "", " ") + w.Header().Set("Content-Type", "application/json") + w.Write(data) +} + func exchangeCodeForToken(code string) (*TokenResponse, error) { data := url.Values{} data.Set("client_id", clientID) @@ -110,3 +127,31 @@ func exchangeCodeForToken(code string) (*TokenResponse, error) { return &tokenResp, nil } + +// refresh tokens programtically +func ExchangeRefreshToken(refreshToken string) (*TokenResponse, error) { + form := url.Values{} + form.Set("client_id", clientID) + form.Set("client_secret", clientSecret) + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", refreshToken) + + resp, err := http.Post( + "https://id.twitch.tv/oauth2/token", + "application/x-www-form-urlencoded", + strings.NewReader(form.Encode()), + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("refresh failed: %s", resp.Status) + } + var tr TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { + return nil, err + } + return &tr, nil +} diff --git a/internal/twitchapi/client.go b/internal/twitchapi/client.go index db4eb40..6798f7b 100644 --- a/internal/twitchapi/client.go +++ b/internal/twitchapi/client.go @@ -5,10 +5,12 @@ import ( "time" helix "github.com/nicklaw5/helix" + oauthsrv "streambot_twitch/internal/auth" ) type Client struct { api *helix.Client + refreshToken string } func NewClient(clientID, clientSecret string) (*Client, error) { @@ -29,6 +31,29 @@ func NewClient(clientID, clientSecret string) (*Client, error) { return &Client{api: apiClient}, nil } +// set User Tokens - Oauth and Refresh +func (c *Client) SetUserTokens(accessToken, refreshToken string, expiresIn int) { + at := strings.TrimPrefix(accessToken, "oauth:") + c.api.SetUserAccessToken(at) + c.accessToken = at + c.refreshToken = refreshToken + if expiresIn > 0 { + c.expiry = time.Now().Add(time.Duration(expiresIn) * time.Second) + } +} + +// use /refresh endpoint to renew tokens +func (c *Client) RefreshIfNeeded() error { + if time.Until(c.expiry) < 5*time.Minute { + tr, err := oauthsrv.ExchangeRefreshToken(c.refreshToken) + if err 1= nil { + return err + } + c.SetUserTokens(tr.AccessToken, tr.RefreshToken, tr.ExpiresIn) + } + return nil +} + // Twitch API helpers func (c *Client) GetUserID(login string) (string, error) {