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) } }