package main
import (
"log"
"os"
"strings"
"encoding/json"
"time"
"net/http"
"net/url"
"fmt"
"flag"
"github.com/gempir/go-twitch-irc/v4"
)
type Secret struct {
ClientID string
ClientSecret string
AccessToken string
RefreshToken string
}
type Periodic struct {
Seconds int
Message string
}
type Config struct {
ClientNick string
Channel string
Commands map [ string ] string
Actions map [ string ] string
Periodic [ ] Periodic
}
type TwitchTokenResponse struct {
AccessToken string ` json:"access_token" `
RefreshToken string ` json:"refresh_token" `
ExpiresIn int ` json:"expires_in" `
Scope [ ] string ` json:"scope" `
TokenType string ` json:"token_type" `
}
func LoadJSON [ T any ] ( file string ) T {
data , err := os . ReadFile ( file )
if err != nil { log . Fatal ( err ) }
var config T
err = json . Unmarshal ( data , & config )
if err != nil { log . Fatal ( err ) }
return config
}
func RunPeriodic ( message Periodic , channel string , client * twitch . Client ) {
for {
time . Sleep ( time . Duration ( message . Seconds ) * time . Second )
client . Say ( channel , message . Message )
}
}
func RefreshToken ( secrets * Secret ) {
api_url := "https://id.twitch.tv/oauth2/token"
data := url . Values { }
data . Set ( "client_id" , secrets . ClientID )
data . Set ( "client_secret" , secrets . ClientSecret )
data . Set ( "grant_type" , "refresh_token" )
data . Set ( "refresh_token" , secrets . RefreshToken )
encoded := strings . NewReader ( data . Encode ( ) )
resp , err := http . Post ( api_url , "application/x-www-form-urlencoded" , encoded )
if err != nil { log . Fatal ( err ) }
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
log . Fatal ( fmt . Sprintf ( "Invalid status code: %v." , resp . StatusCode ) )
}
var token_resp TwitchTokenResponse
err = json . NewDecoder ( resp . Body ) . Decode ( & token_resp )
if err != nil { log . Fatal ( err ) }
secrets . AccessToken = fmt . Sprintf ( "oauth:%s" , token_resp . AccessToken ) ;
}
func LoadConfigs ( ) ( Secret , Config ) {
var secrets_file string
var config_file string
var action_file string
flag . StringVar ( & secrets_file , "secrets" , "secret.json" , "Your secrets file. KEEP PRIVATE!" )
flag . StringVar ( & config_file , "config" , "config.json" , "Your config.json. Can be public." )
flag . StringVar ( & action_file , "actions" , "actions.json" , "Your URL actions. KEEP PRIVATE!" )
flag . Parse ( )
log . Println ( "Loading secrets from" , secrets_file )
log . Println ( "Loading config from" , config_file )
log . Println ( "Loading actions from" , action_file )
secrets := LoadJSON [ Secret ] ( secrets_file )
config := LoadJSON [ Config ] ( config_file )
config . Actions = LoadJSON [ map [ string ] string ] ( action_file )
return secrets , config
}
func main ( ) {
SECRETS , CONFIG := LoadConfigs ( )
RefreshToken ( & SECRETS )
// or client := twitch.NewAnonymousClient() for an anonymous user (no write capabilities)
client := twitch . NewClient ( CONFIG . ClientNick , SECRETS . AccessToken )
client . OnPrivateMessage ( func ( message twitch . PrivateMessage ) {
// not sure if this is good enough for auth
cmd , found := strings . CutPrefix ( message . Message , "!" )
if ! found { return }
// see if it's a valid action
action , is_action := CONFIG . Actions [ strings . ToLower ( cmd ) ]
if is_action && message . User . IsBroadcaster {
log . Println ( "Hitting URL" , action )
return ;
}
// see if it's a normal command
reply , valid_cmd := CONFIG . Commands [ strings . ToLower ( cmd ) ]
if valid_cmd {
client . Reply ( message . Channel , message . ID , reply )
} else if ! valid_cmd {
client . Reply ( message . Channel , message . ID , "Invalid command. Use !help to see what's available." )
}
} )
client . OnSelfJoinMessage ( func ( message twitch . UserJoinMessage ) {
log . Println ( "Join" , message . Channel , "as" , message . User , "successful" )
} )
client . OnConnect ( func ( ) {
log . Println ( "Connected. Joining" , CONFIG . Channel )
client . Join ( CONFIG . Channel )
} )
client . OnPingMessage ( func ( message twitch . PingMessage ) {
log . Println ( "PING!" , message . Message )
} )
client . OnPongMessage ( func ( message twitch . PongMessage ) {
log . Println ( "PONG!" , message . Message )
} )
client . OnNoticeMessage ( func ( message twitch . NoticeMessage ) {
log . Println ( "NOTICE" , message . Channel , message . Message )
} )
for _ , perodic := range ( CONFIG . Periodic ) {
go RunPeriodic ( perodic , CONFIG . Channel , client )
}
log . Println ( "Connecting to Twitch..." )
err := client . Connect ( )
if err != nil { log . Fatal ( err ) }
}