You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
7.0 KiB
295 lines
7.0 KiB
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gempir/go-twitch-irc/v4"
|
|
)
|
|
|
|
var COMMAND_RE = regexp.MustCompile(`^(\$|!)([a-z_]+) ?(.*)?$`)
|
|
|
|
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 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"`
|
|
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 (bot *Bot) RefreshToken() {
|
|
api_url := "https://id.twitch.tv/oauth2/token"
|
|
data := url.Values{}
|
|
data.Set("client_id", bot.Secrets.ClientID)
|
|
data.Set("client_secret", bot.Secrets.ClientSecret)
|
|
data.Set("grant_type", "refresh_token")
|
|
data.Set("refresh_token", bot.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) }
|
|
|
|
bot.Secrets.AccessToken = fmt.Sprintf("oauth:%s", token_resp.AccessToken);
|
|
}
|
|
|
|
func (bot *Bot) LoadConfigs() {
|
|
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)
|
|
|
|
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 result, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
cmd, err := ParseCommand(message.Message)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
switch cmd.Command {
|
|
case "poll_start":
|
|
if cmd.Text == "" { return }
|
|
|
|
bot.CurrentPoll.Active = true
|
|
bot.CurrentPoll.Text = cmd.Text
|
|
bot.CurrentPoll.Yes = 0
|
|
bot.CurrentPoll.No = 0
|
|
|
|
announce := fmt.Sprintf("New Poll: %s. Use 1, yes or true; 0, no or false to reply.", bot.CurrentPoll.Text)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
|
|
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 := bot.Config.Commands[strings.ToLower(cmd.Command)]
|
|
|
|
if valid_cmd {
|
|
bot.Reply(message, reply)
|
|
} else if !valid_cmd {
|
|
bot.SendHelp(message)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
bot.Client.OnPrivateMessage(func (message twitch.PrivateMessage) {
|
|
bot.HandleMessage(message)
|
|
})
|
|
|
|
bot.Client.OnSelfJoinMessage(func (message twitch.UserJoinMessage) {
|
|
log.Println("Join", message.Channel, "as", message.User, "successful")
|
|
})
|
|
|
|
bot.Client.OnConnect(func () {
|
|
log.Println("Connected. Joining", bot.Config.Channel)
|
|
bot.Client.Join(bot.Config.Channel)
|
|
})
|
|
|
|
bot.Client.OnNoticeMessage(func (message twitch.NoticeMessage) {
|
|
log.Println("NOTICE", message.Channel, message.Message)
|
|
})
|
|
}
|
|
|
|
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 := 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()
|
|
}
|
|
|