1
0
Fork 0
streamtitle/client.go

190 lines
5.8 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"errors"
"github.com/nicklaw5/helix/v2"
)
// Client is the data structure that groups all the program state.
type Client struct {
client *helix.Client // The HTTP client used to interact with Twitch.
config appConfig // The static config information in the environment.
state appState // The state (credentials and such)
broadcastId string // The broadcast ID for the current token
streamInfo streamInfo // The stream information object.
}
// NewContext builds a new context and returns the outcome.
func NewContext() (*Client, error) {
var ctx Client
if err := ctx.config.read(); err != nil {
return nil, err
}
if err := ctx.state.read(); err != nil {
return nil, err
}
client, err := helix.NewClient(&helix.Options{
ClientID: ctx.config.ClientId,
ClientSecret: ctx.config.ClientSecret,
RedirectURI: "http://localhost:9300/st-callback",
})
if err != nil {
return nil, err
}
ctx.client = client
return &ctx, nil
}
// refreshToken tries to use the given refresh token to get a new pair of
// access and refresh token. If the token cannot be refreshed because the
// token is not valid, a separate flag will be raised and the strings will
// be empty. If there is an issue during the token refresh, it will return
// the corresponding error.
func (ctx *Client) refreshToken(token string) (bool, string, string, error) {
logger.Print("Using the refresh token...")
if resp, err := ctx.client.RefreshUserAccessToken(token); err != nil {
return false, "", "", err
} else {
if resp.Error == "" {
return true, resp.Data.AccessToken, resp.Data.RefreshToken, nil
} else {
return false, "", "", nil
}
}
}
// Tries to finalize the login by checking the validity of the token. If the token
// is valid, it will also set the owner of the token as the broadcaster ID field
// in the local client structure.
func (ctx *Client) finishLogin(token string) (bool, error) {
logger.Print("Validating access token...")
if valid, response, err := ctx.client.ValidateToken(token); err != nil {
return false, err
} else if valid {
logger.Print("Token is valid and belongs to ", response.Data.Login)
ctx.broadcastId = response.Data.UserID
ctx.client.SetUserAccessToken(token)
return true, nil
} else {
logger.Print("Access token is not valid (maybe expired?")
return false, nil
}
}
// Login will initialise the client state so that there exists a valid access
// token in the client context which can be used by further API calls made
// within the client.
func (ctx *Client) Login() error {
logger.Print("Prepare for login")
// Check if the token in the app state is still valid
if ctx.state.AccessToken != "" {
if valid, err := ctx.finishLogin(ctx.state.AccessToken); err != nil {
return err
} else if valid {
return nil
}
// So the access token is not valid. Is the refresh token valid?
if ctx.state.RefreshToken != "" {
if valid, access, refresh, err := ctx.refreshToken(ctx.state.RefreshToken); err != nil {
return err
} else if valid {
logger.Print("A new token has been generated")
ctx.state.AccessToken = access
ctx.state.RefreshToken = refresh
if err := ctx.state.write(); err != nil {
return err
}
if valid, err := ctx.finishLogin(access); err != nil {
return err
} else if valid {
return nil
}
} else {
logger.Print("Refresh token is not valid")
}
}
} else {
logger.Print("No access token found in the application state")
}
// The user has to log in
callback := make(chan loginTokens)
go spawnAuthorizationServer(ctx, callback)
tokens := <-callback
ctx.state.AccessToken = tokens.accessToken
ctx.state.RefreshToken = tokens.refreshToken
if err := ctx.state.write(); err != nil {
return err
}
if valid, err := ctx.finishLogin(ctx.state.AccessToken); err != nil {
return err
} else if valid {
return nil
} else {
// I am out of ideas, man!
return errors.New("cannot issue a valid access token")
}
}
// AuthorizationURL will build the URL that the user has to visit in order
// to authorize the bot when the refresh token does not exist or it is
// either invalid or expired.
func (ctx *Client) AuthorizationURL(state string) string {
return ctx.client.GetAuthorizationURL(&helix.AuthorizationURLParams{
ResponseType: "code",
Scopes: []string{"channel:manage:broadcast"},
State: state,
ForceVerify: false,
})
}
// FetchStreamInfo will download the current stream information via the API
// and it will store the information in the stream information part of the
// client context.
func (ctx *Client) FetchStreamInfo() error {
logger.Print("Fetching stream information...")
resp, err := ctx.client.GetChannelInformation(&helix.GetChannelInformationParams{
BroadcasterIDs: []string{ctx.broadcastId},
})
if err == nil {
ctx.streamInfo.load(&resp.Data.Channels[0])
}
return err
}
// SendStreamInfo generates an API call that will submit the currently stored
// stream information in the context. If this was previously set by calls
// to the setters, it will update the stream information.
func (ctx *Client) SendStreamInfo(dryrun bool) error {
logger.Print("Updating stream information...")
logger.Print("New title: ", ctx.streamInfo.title)
logger.Print("New game ID: ", ctx.streamInfo.game)
logger.Print("New language: ", ctx.streamInfo.language)
logger.Print("Tag list: ", ctx.streamInfo.tags)
if dryrun {
logger.Print("Skipping update because we are running in dry-run mode")
return nil
}
_, err := ctx.client.EditChannelInformation(&helix.EditChannelInformationParams{
BroadcasterID: ctx.broadcastId,
Title: ctx.streamInfo.title,
GameID: ctx.streamInfo.game,
BroadcasterLanguage: ctx.streamInfo.language,
Tags: ctx.streamInfo.tags,
})
return err
}