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