diff --git a/.gitignore b/.gitignore index f489ae7..fa95e54 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ public *.log secret.json config.json +actions.json diff --git a/main.go b/main.go index 4130984..cf72f98 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,23 @@ package main import ( + "encoding/json" + "errors" + "flag" + "fmt" "log" + "net/http" + "net/url" "os" + "regexp" "strings" - "encoding/json" "time" - "net/http" - "net/url" - "fmt" - "flag" "github.com/gempir/go-twitch-irc/v4" ) +var COMMAND_RE = regexp.MustCompile(`^(\$|!)([a-z_]+) ?(.*)?$`) + type Secret struct { ClientID string ClientSecret string @@ -34,6 +38,26 @@ type Config struct { Periodic []Periodic } +type Poll struct { + Active bool + Text string + Yes int + No int +} + +type Bot struct { + Secrets Secret + Config Config + Client *twitch.Client + CurrentPoll Poll +} + +type Command struct { + Type string + Command string + Text string +} + type TwitchTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` @@ -60,13 +84,13 @@ func RunPeriodic(message Periodic, channel string, client *twitch.Client) { } } -func RefreshToken(secrets* Secret) { +func (bot *Bot) RefreshToken() { 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("client_id", bot.Secrets.ClientID) + data.Set("client_secret", bot.Secrets.ClientSecret) data.Set("grant_type", "refresh_token") - data.Set("refresh_token", secrets.RefreshToken) + data.Set("refresh_token", bot.Secrets.RefreshToken) encoded := strings.NewReader(data.Encode()) resp, err := http.Post(api_url,"application/x-www-form-urlencoded", encoded) @@ -83,10 +107,10 @@ func RefreshToken(secrets* Secret) { err = json.NewDecoder(resp.Body).Decode(&token_resp) if err != nil { log.Fatal(err) } - secrets.AccessToken = fmt.Sprintf("oauth:%s", token_resp.AccessToken); + bot.Secrets.AccessToken = fmt.Sprintf("oauth:%s", token_resp.AccessToken); } -func LoadConfigs() (Secret, Config) { +func (bot *Bot) LoadConfigs() { var secrets_file string var config_file string var action_file string @@ -100,71 +124,172 @@ func LoadConfigs() (Secret, Config) { 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) + bot.Secrets = LoadJSON[Secret](secrets_file) + bot.Config = LoadJSON[Config](config_file) + bot.Config.Actions = LoadJSON[map[string]string](action_file) +} + +func ParseCommand(message string) (Command, error) { + matches := COMMAND_RE.FindStringSubmatch(message) + + if matches == nil { + return Command{}, errors.New("Invalid command format.") + } + + result := Command{ + matches[1], + matches[2], + matches[3], + } - return secrets, config + return result, nil } -func main() { - SECRETS, CONFIG := LoadConfigs() +func (bot *Bot) StreamerAction(message twitch.PrivateMessage) { + if !message.User.IsBroadcaster { + log.Println("! A StreamerAction message was sent by someone not marked as Broadcaster") + return + } - RefreshToken(&SECRETS) + cmd, err := ParseCommand(message.Message) + if err != nil { + fmt.Println(err) + return + } - // or client := twitch.NewAnonymousClient() for an anonymous user (no write capabilities) - client := twitch.NewClient(CONFIG.ClientNick, SECRETS.AccessToken) + switch cmd.Command { + case "poll_start": + if cmd.Text == "" { return } - client.OnPrivateMessage(func(message twitch.PrivateMessage) { - // not sure if this is good enough for auth + bot.CurrentPoll.Active = true + bot.CurrentPoll.Text = cmd.Text + bot.CurrentPoll.Yes = 0 + bot.CurrentPoll.No = 0 - cmd, found := strings.CutPrefix(message.Message, "!") - if !found { return } + announce := fmt.Sprintf("New Poll: %s. Use 1, yes or true; 0, no or false to reply.", bot.CurrentPoll.Text) - // see if it's a valid action - action, is_action := CONFIG.Actions[strings.ToLower(cmd)] + bot.Send(message, announce) + case "poll_end": + bot.CurrentPoll.Active = false + bot.CurrentPoll.Text = fmt.Sprintf( + "Poll Has Ended: \"%s\" Yes=%d, No=%d", + bot.CurrentPoll.Text, bot.CurrentPoll.Yes, bot.CurrentPoll.No) + bot.Send(message, bot.CurrentPoll.Text) + } +} - if is_action && message.User.IsBroadcaster { - log.Println("Hitting URL", action) - return; - } +func (bot *Bot) TryPoll(message twitch.PrivateMessage) bool { + switch message.Message { + case "yes", "1", "true": + bot.CurrentPoll.Yes += 1 + return true + case "no", "0", "false": + bot.CurrentPoll.No += 1 + return true + } + + return false +} + +func (bot *Bot) Send(message twitch.PrivateMessage, text string) { + bot.Client.Say(message.Channel, text) +} + +func (bot *Bot) Reply(message twitch.PrivateMessage, text string) { + bot.Client.Reply(message.Channel, message.ID, text) +} + +func (bot *Bot) SendHelp(message twitch.PrivateMessage) { + bot.Reply(message, "Invalid command. Use !help to see what's available.") +} + +func (bot *Bot) ViewerAction(message twitch.PrivateMessage) { + cmd, err := ParseCommand(message.Message) + if err != nil || cmd.Type != "!" { + bot.SendHelp(message) + return + } + + switch cmd.Command { + case "poll": + if bot.CurrentPoll.Active { + bot.Reply(message, + fmt.Sprintf("Current Poll: %s, Yes=%d, No=%d", + bot.CurrentPoll.Text, bot.CurrentPoll.Yes, bot.CurrentPoll.No)) + } else { + bot.Reply(message, "No poll currently active.") + } + default: // see if it's a normal command - reply, valid_cmd := CONFIG.Commands[strings.ToLower(cmd)] + reply, valid_cmd := bot.Config.Commands[strings.ToLower(cmd.Command)] if valid_cmd { - client.Reply(message.Channel, message.ID, reply) + bot.Reply(message, reply) } else if !valid_cmd { - client.Reply(message.Channel, message.ID, "Invalid command. Use !help to see what's available.") + bot.SendHelp(message) } - }) + } +} - client.OnSelfJoinMessage(func (message twitch.UserJoinMessage) { - log.Println("Join", message.Channel, "as", message.User, "successful") - }) +func (bot *Bot) HandleMessage(message twitch.PrivateMessage) { + // not sure if this is good enough for auth + if strings.HasPrefix(message.Message, "$") && message.User.IsBroadcaster { + bot.StreamerAction(message) + } else if bot.CurrentPoll.Active && bot.TryPoll(message) { + return + } else if strings.HasPrefix(message.Message, "!") { + bot.ViewerAction(message) + } +} + +func (bot *Bot) CreateClient() { + // or client := twitch.NewAnonymousClient() for an anonymous user (no write capabilities) + bot.Client = twitch.NewClient(bot.Config.ClientNick, bot.Secrets.AccessToken) - client.OnConnect(func () { - log.Println("Connected. Joining", CONFIG.Channel) - client.Join(CONFIG.Channel) + bot.Client.OnPrivateMessage(func (message twitch.PrivateMessage) { + bot.HandleMessage(message) }) - client.OnPingMessage(func (message twitch.PingMessage) { - log.Println("PING!", message.Message) + bot.Client.OnSelfJoinMessage(func (message twitch.UserJoinMessage) { + log.Println("Join", message.Channel, "as", message.User, "successful") }) - client.OnPongMessage(func (message twitch.PongMessage) { - log.Println("PONG!", message.Message) + bot.Client.OnConnect(func () { + log.Println("Connected. Joining", bot.Config.Channel) + bot.Client.Join(bot.Config.Channel) }) - client.OnNoticeMessage(func (message twitch.NoticeMessage) { + bot.Client.OnNoticeMessage(func (message twitch.NoticeMessage) { log.Println("NOTICE", message.Channel, message.Message) }) +} - for _, perodic := range(CONFIG.Periodic) { - go RunPeriodic(perodic, CONFIG.Channel, client) +func (bot *Bot) RunPeriodic() { + for _, perodic := range(bot.Config.Periodic) { + go RunPeriodic(perodic, bot.Config.Channel, bot.Client) } +} +func (bot *Bot) Connect() { log.Println("Connecting to Twitch...") - err := client.Connect() + err := bot.Client.Connect() if err != nil { log.Fatal(err) } } + +func CreateBot() *Bot { + var bot Bot + + bot.CurrentPoll.Active = false + bot.LoadConfigs() + bot.RefreshToken() + bot.CreateClient() + + return &bot +} + +func main() { + bot := CreateBot() + bot.RunPeriodic() + bot.Connect() +}