diff --git a/internal/irc/irc.go b/internal/irc/irc.go index 8a5965c..0d36738 100644 --- a/internal/irc/irc.go +++ b/internal/irc/irc.go @@ -2,8 +2,8 @@ package irc import ( "bufio" - "encoding/json" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -16,8 +16,8 @@ import ( "time" rgb "github.com/foresthoffman/rgblog" - scribble "github.com/nanobox-io/golang-scribble" uuid "github.com/google/uuid" + scribble "github.com/nanobox-io/golang-scribble" ) const PSTFormat = "Jan 2 15:04:05 PST" @@ -54,16 +54,17 @@ type KardBot struct { Server string startTime time.Time Prompts []string - Database scribble.Driver + Database scribble.Driver ChannelData map[string]ChannelData } type ChannelData struct { - Name string `json:"name"` - AdminKey string `json:"value,omitempty"` - Command string `json:"customcommand,omitempty"` - ExtraStrings string `json:"extrastrings,omitempty"` - JoinTime time.Time `json:"jointime"` + Name string `json:"name"` + AdminKey string `json:"value,omitempty"` + Command string `json:"customcommand,omitempty"` + ExtraStrings string `json:"extrastrings,omitempty"` + JoinTime time.Time `json:"jointime"` + ControlChannel bool } // Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it @@ -185,7 +186,6 @@ func (bb *KardBot) Login() { bb.conn.Write([]byte("NICK " + bb.Name + "\r\n")) } - func (bb *KardBot) LeaveChannel(channels ...string) { for _, channel := range channels { rgb.YPrintf("[%s] Leaving #%s...\n", TimeStamp(), channel) @@ -263,7 +263,7 @@ func (bb *KardBot) Start() { fmt.Println("Aborting!") return } - + err = bb.readChannelData() if nil != err { fmt.Println(err) @@ -275,7 +275,7 @@ func (bb *KardBot) Start() { bb.Connect() bb.Login() if len(bb.ChannelData) > 0 { - for channelName := range(bb.ChannelData) { + for channelName := range bb.ChannelData { bb.JoinChannel(channelName) } } else { @@ -304,20 +304,20 @@ func (bb *KardBot) readChannelData() error { return err } bb.ChannelData = make(map[string]ChannelData) - bb.ChannelData[bb.Channel] = record; + bb.ChannelData[bb.Channel] = record } else { bb.ChannelData = make(map[string]ChannelData) } for _, data := range records { record := ChannelData{} - err := json.Unmarshal([]byte(data), &record); + err := json.Unmarshal([]byte(data), &record) if err != nil { return err } if record.Name != "" { if record.Command == "" { record.Command = "card" - + rgb.YPrintf("[%s] Rewriting data for #%s...\n", TimeStamp(), bb.Channel) if err := bb.Database.Write("channelData", record.Name, record); err != nil { return err @@ -346,7 +346,7 @@ func (bb *KardBot) readOrCreateChannelKey(channel string) string { var record ChannelData if record, ok := bb.ChannelData[channel]; !ok { rgb.YPrintf("[%s] No channel data for #%s exists, creating\n", TimeStamp(), channel) - err = bb.Database.Read("channelData", channel, &record); + err = bb.Database.Read("channelData", channel, &record) if err == nil { bb.ChannelData[channel] = record } @@ -378,4 +378,4 @@ func TimeStamp() string { func TimeStampFmt(format string) string { return time.Now().Format(format) -} \ No newline at end of file +} diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index afa09d5..8dc2f18 100755 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -91,40 +91,40 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { UnauthorizedHandler(response, request) return - } + } type TemplateData struct { - Channel string - Command string - ExtraStrings string - SinceTime time.Time - Leaving bool + Channel string + Command string + ExtraStrings string + SinceTime time.Time + Leaving bool } channelData := ircBot.ChannelData[vars["channel"]] var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, false} if request.Method == "POST" { request.ParseForm() - if strings.Join(request.PostForm["leave"],",") == "Leave twitch channel" { + if strings.Join(request.PostForm["leave"], ",") == "Leave twitch channel" { td.Leaving = true - } else if strings.Join(request.PostForm["reallyleave"],",") == "Really leave twitch channel" { - delete(ircBot.ChannelData,vars["channel"]) + } else if strings.Join(request.PostForm["reallyleave"], ",") == "Really leave twitch channel" { + delete(ircBot.ChannelData, vars["channel"]) ircBot.Database.Delete("channelData", vars["channel"]) ircBot.LeaveChannel(vars["channel"]) LeaveHandler(response, request) return } sourceData := ircBot.ChannelData[vars["channel"]] - if strings.Join(request.PostForm["Command"],",") != "" { - sourceData.Command = strings.Join(request.PostForm["Command"],",") + if strings.Join(request.PostForm["Command"], ",") != "" { + sourceData.Command = strings.Join(request.PostForm["Command"], ",") td.Command = sourceData.Command ircBot.ChannelData[vars["channel"]] = sourceData } - if strings.Join(request.PostForm["ExtraStrings"],",") != sourceData.ExtraStrings { - sourceData.ExtraStrings = strings.Join(request.PostForm["ExtraStrings"],",") + if strings.Join(request.PostForm["ExtraStrings"], ",") != sourceData.ExtraStrings { + sourceData.ExtraStrings = strings.Join(request.PostForm["ExtraStrings"], ",") td.ExtraStrings = sourceData.ExtraStrings ircBot.ChannelData[vars["channel"]] = sourceData } - ircBot.Database.Write("channelData", vars["channel"], sourceData); + ircBot.Database.Write("channelData", vars["channel"], sourceData) } tmpl := template.Must(template.ParseFiles("web/admin.html")) tmpl.Execute(response, td) diff --git a/main.go b/main.go index 1996938..d173136 100755 --- a/main.go +++ b/main.go @@ -1,214 +1,214 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "math/rand" - "os" - "path/filepath" - "time" - - builtins "git.martyn.berlin/martyn/karaokards/internal/builtins" - irc "git.martyn.berlin/martyn/karaokards/internal/irc" - webserver "git.martyn.berlin/martyn/karaokards/internal/webserver" - rgb "github.com/foresthoffman/rgblog" - scribble "github.com/nanobox-io/golang-scribble" -) - -type configStruct struct { - InitialChannels []string `json:"channels"` - OAuthPath string `json:"oauthpath,omitempty"` - StringPath string `json:"authpath,omitempty"` - DataPath string `json:"datapath,omitempty"` -} - -type customStringsStruct struct { - Strings []string `json:"strings,omitempty"` -} - -var selectablePrompts []string - -var customStrings customStringsStruct - -var config configStruct - -func readConfig() { - var data []byte - var err error - configFile := "" - if os.Getenv("KARAOKARDS_CONFIGFILE") != "" { - if _, err := os.Stat(os.Getenv("KARAOKARDS_CONFIGFILE")); os.IsNotExist(err) { - rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_CONFIGFILE")) - os.Exit(1) - } - configFile = os.Getenv("KARAOKARDS_CONFIGFILE") - } else { - ex, err := os.Executable() - if err != nil { - rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and cannot find executable!\n", irc.TimeStamp()) - } - exPath := filepath.Dir(ex) - if _, err := os.Stat(exPath + "/config.json"); os.IsNotExist(err) { - rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and `config.json` not alongside executable!\n", irc.TimeStamp()) - if _, err := os.Stat("/etc/karaokards/config.json"); os.IsNotExist(err) { - rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var unset and neither '%s' nor '%s' exist!\n", irc.TimeStamp(), exPath + "/config.json", "/etc/karaokards/config.json") - os.Exit(1) - } else { - configFile = "/etc/karaokards/config.json" - } - } else { - configFile = exPath + "/config.json" - } - } - data, err = ioutil.ReadFile(configFile) - if err != nil { - rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err) - os.Exit(1) - } - err = json.Unmarshal(data, &customStrings) - if err != nil { - rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err) - os.Exit(1) - } - rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile) - return -} - -//openDatabase "database" in this sense being a scribble db -func openDatabase() *scribble.Driver { - dataPath := "" - if config.DataPath == "" { - if os.Getenv("KARAOKARDS_DATA_FOLDER") != "" { - if _, err := os.Stat(os.Getenv("KARAOKARDS_DATA_FOLDER")); os.IsNotExist(err) { - rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER")) - os.Exit(1) - } - dataPath = os.Getenv("KARAOKARDS_DATA_FOLDER") - } else { - ex, err := os.Executable() - if err != nil { - rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var unset and cannot find executable!\n", irc.TimeStamp()) - os.Exit(1) - } - exPath := filepath.Dir(ex) - if _, err := os.Stat(exPath + "/data"); os.IsNotExist(err) { - rgb.YPrintf("[%s] Warning %s doesn't exist, trying to create it.\n", irc.TimeStamp(), exPath + "/data") - err = os.Mkdir(exPath + "/data", 0770) - if err != nil { - rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath + "/data", err) - os.Exit(1) - } - } - dataPath = exPath + "/data" - } - } else { - if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) { - rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER")) - os.Exit(1) - } - dataPath = config.DataPath - } - db, err := scribble.New(dataPath, nil) - if err != nil { - rgb.RPrintf("[%s] Error opening database in '%s' : %s\n", irc.TimeStamp(), dataPath, err) - os.Exit(1) - } - return db -} - -func readBonusStrings() []string { - var data []byte - var err error - if config.StringPath == "" { - ex, err := os.Executable() - if err != nil { - rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) - return []string{} - } - exPath := filepath.Dir(ex) - data, err = ioutil.ReadFile(exPath + "/strings.json") - if err != nil { - rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) - return []string{} - } - } else { - data, err = ioutil.ReadFile(config.StringPath) - if err != nil { - rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) - return []string{} - } - } - err = json.Unmarshal(data, &customStrings) - if err != nil { - rgb.YPrintf("[%s] Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error: %s\n", irc.TimeStamp(), err) - return []string{} - } - rgb.YPrintf("[%s] Read %d prompts from `strings.json`\n", irc.TimeStamp(), len(customStrings.Strings)) - return customStrings.Strings -} - -var buildDate string - -func main() { - rgb.YPrintf("[%s] starting karaokard bot build %s\n", irc.TimeStamp(), buildDate) - readConfig() - rand.Seed(time.Now().UnixNano()) - for _, val := range builtins.Karaokards { - selectablePrompts = append(selectablePrompts, val) - } - for _, val := range readBonusStrings() { - selectablePrompts = append(selectablePrompts, val) - } - persistentData := openDatabase() - var dbGlobalPrompts []string - if err := persistentData.Read("prompts", "global", &dbGlobalPrompts); err != nil { - persistentData.Write("prompts", "common", dbGlobalPrompts) - } - selectablePrompts := append(selectablePrompts, dbGlobalPrompts...) - - rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts)) - oauthPath := "" - if config.OAuthPath == "" { - if os.Getenv("TWITCH_OAUTH_JSON") != "" { - if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) { - os.Exit(1) - } - oauthPath = os.Getenv("TWITCH_OAUTH_JSON") - } else { - if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/oauth.json"); os.IsNotExist(err) { - rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json") - if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) { - rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/oauth.json") - os.Exit(1) - } - oauthPath = "/etc/twitch/oauth.json" - } else { - oauthPath = os.Getenv("HOME") + "/.twitch/oauth.json" - } - } - } else { - if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) { - rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.OAuthPath) - os.Exit(1) - } - oauthPath = config.OAuthPath - } - - // Replace the channel name, bot name, and the path to the private directory with your respective - // values. - myBot := irc.KardBot{ - Channel: "imartynontwitch", - MsgRate: time.Duration(20/30) * time.Millisecond, - Name: "Karaokards", - Port: "6667", - PrivatePath: oauthPath, - Server: "irc.chat.twitch.tv", - Prompts: selectablePrompts, - Database: *persistentData, - } - go func() { - rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353") - webserver.HandleHTTP(&myBot) - }() - myBot.Start() -} +package main + +import ( + "encoding/json" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "time" + + builtins "git.martyn.berlin/martyn/karaokards/internal/builtins" + irc "git.martyn.berlin/martyn/karaokards/internal/irc" + webserver "git.martyn.berlin/martyn/karaokards/internal/webserver" + rgb "github.com/foresthoffman/rgblog" + scribble "github.com/nanobox-io/golang-scribble" +) + +type configStruct struct { + InitialChannels []string `json:"channels"` + OAuthPath string `json:"oauthpath,omitempty"` + StringPath string `json:"authpath,omitempty"` + DataPath string `json:"datapath,omitempty"` +} + +type customStringsStruct struct { + Strings []string `json:"strings,omitempty"` +} + +var selectablePrompts []string + +var customStrings customStringsStruct + +var config configStruct + +func readConfig() { + var data []byte + var err error + configFile := "" + if os.Getenv("KARAOKARDS_CONFIGFILE") != "" { + if _, err := os.Stat(os.Getenv("KARAOKARDS_CONFIGFILE")); os.IsNotExist(err) { + rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_CONFIGFILE")) + os.Exit(1) + } + configFile = os.Getenv("KARAOKARDS_CONFIGFILE") + } else { + ex, err := os.Executable() + if err != nil { + rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and cannot find executable!\n", irc.TimeStamp()) + } + exPath := filepath.Dir(ex) + if _, err := os.Stat(exPath + "/config.json"); os.IsNotExist(err) { + rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and `config.json` not alongside executable!\n", irc.TimeStamp()) + if _, err := os.Stat("/etc/karaokards/config.json"); os.IsNotExist(err) { + rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var unset and neither '%s' nor '%s' exist!\n", irc.TimeStamp(), exPath+"/config.json", "/etc/karaokards/config.json") + os.Exit(1) + } else { + configFile = "/etc/karaokards/config.json" + } + } else { + configFile = exPath + "/config.json" + } + } + data, err = ioutil.ReadFile(configFile) + if err != nil { + rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err) + os.Exit(1) + } + err = json.Unmarshal(data, &customStrings) + if err != nil { + rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err) + os.Exit(1) + } + rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile) + return +} + +//openDatabase "database" in this sense being a scribble db +func openDatabase() *scribble.Driver { + dataPath := "" + if config.DataPath == "" { + if os.Getenv("KARAOKARDS_DATA_FOLDER") != "" { + if _, err := os.Stat(os.Getenv("KARAOKARDS_DATA_FOLDER")); os.IsNotExist(err) { + rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER")) + os.Exit(1) + } + dataPath = os.Getenv("KARAOKARDS_DATA_FOLDER") + } else { + ex, err := os.Executable() + if err != nil { + rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var unset and cannot find executable!\n", irc.TimeStamp()) + os.Exit(1) + } + exPath := filepath.Dir(ex) + if _, err := os.Stat(exPath + "/data"); os.IsNotExist(err) { + rgb.YPrintf("[%s] Warning %s doesn't exist, trying to create it.\n", irc.TimeStamp(), exPath+"/data") + err = os.Mkdir(exPath+"/data", 0770) + if err != nil { + rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath+"/data", err) + os.Exit(1) + } + } + dataPath = exPath + "/data" + } + } else { + if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) { + rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER")) + os.Exit(1) + } + dataPath = config.DataPath + } + db, err := scribble.New(dataPath, nil) + if err != nil { + rgb.RPrintf("[%s] Error opening database in '%s' : %s\n", irc.TimeStamp(), dataPath, err) + os.Exit(1) + } + return db +} + +func readBonusStrings() []string { + var data []byte + var err error + if config.StringPath == "" { + ex, err := os.Executable() + if err != nil { + rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) + return []string{} + } + exPath := filepath.Dir(ex) + data, err = ioutil.ReadFile(exPath + "/strings.json") + if err != nil { + rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) + return []string{} + } + } else { + data, err = ioutil.ReadFile(config.StringPath) + if err != nil { + rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err) + return []string{} + } + } + err = json.Unmarshal(data, &customStrings) + if err != nil { + rgb.YPrintf("[%s] Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error: %s\n", irc.TimeStamp(), err) + return []string{} + } + rgb.YPrintf("[%s] Read %d prompts from `strings.json`\n", irc.TimeStamp(), len(customStrings.Strings)) + return customStrings.Strings +} + +var buildDate string + +func main() { + rgb.YPrintf("[%s] starting karaokard bot build %s\n", irc.TimeStamp(), buildDate) + readConfig() + rand.Seed(time.Now().UnixNano()) + for _, val := range builtins.Karaokards { + selectablePrompts = append(selectablePrompts, val) + } + for _, val := range readBonusStrings() { + selectablePrompts = append(selectablePrompts, val) + } + persistentData := openDatabase() + var dbGlobalPrompts []string + if err := persistentData.Read("prompts", "global", &dbGlobalPrompts); err != nil { + persistentData.Write("prompts", "common", dbGlobalPrompts) + } + selectablePrompts := append(selectablePrompts, dbGlobalPrompts...) + + rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts)) + oauthPath := "" + if config.OAuthPath == "" { + if os.Getenv("TWITCH_OAUTH_JSON") != "" { + if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) { + os.Exit(1) + } + oauthPath = os.Getenv("TWITCH_OAUTH_JSON") + } else { + if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/oauth.json"); os.IsNotExist(err) { + rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json") + if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) { + rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/oauth.json") + os.Exit(1) + } + oauthPath = "/etc/twitch/oauth.json" + } else { + oauthPath = os.Getenv("HOME") + "/.twitch/oauth.json" + } + } + } else { + if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) { + rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.OAuthPath) + os.Exit(1) + } + oauthPath = config.OAuthPath + } + + // Replace the channel name, bot name, and the path to the private directory with your respective + // values. + myBot := irc.KardBot{ + Channel: "imartynontwitch", + MsgRate: time.Duration(20/30) * time.Millisecond, + Name: "Karaokards", + Port: "6667", + PrivatePath: oauthPath, + Server: "irc.chat.twitch.tv", + Prompts: selectablePrompts, + Database: *persistentData, + } + go func() { + rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353") + webserver.HandleHTTP(&myBot) + }() + myBot.Start() +}