Add configuration handling and parsing. * Configuration initialization: `paprika init [ -c xyz ]` * Default directory path for DB and plugin state. * Usage / Help text.
Anthony DeDominic adedomin@gmail.com
Sat, 20 Nov 2021 07:16:16 -0500
8 files changed,
331 insertions(+),
11 deletions(-)
A
config/config.go
@@ -0,0 +1,109 @@
+package config + +import ( + "log" + "os" + "strings" + + "github.com/adedomin/indenttext" +) + +var ( + Nick = "paprika" + Pass = "" + Host = "irc.rizon.net:6667" + Sasl = "" + Tls = false + ChanJoinStr = "JOIN #taigobot-test" + DbPath = "" + ApiKeys = make(map[string]string) +) + +func init() { + configPath := "" + inC := false + initMode := false + for _, v := range os.Args { + switch v { + case "-c", "--config": + inC = true + case "-h", "--help": + usage() + default: + if inC { + configPath = v + } else if v == "init" { + initMode = true + } else if v == "help" { + usage() + } + } + } + + if initMode { + createConfig(configPath) + } + + var file *os.File + var err error + if configPath == "" { + file = findConfig() + } else { + file, err = os.Open(configPath) + if err != nil { + log.Printf("Error: Could not open config path.") + log.Fatalf("- %s", err) + } + } + defer file.Close() + + firstChannel := true + var chanList strings.Builder + err = indenttext.Parse(file, func (parents []string, item string, typeof indenttext.ItemType) bool { + if len(parents) == 1 && typeof == indenttext.Value { + switch parents[0] { + case "nick": + Nick = item + case "pass": + Pass = item + case "host": + Host = item + case "sasl": + Sasl = item + case "tls": + if item == "true" { + Tls = true + } else { + Tls = false + } + case "channels": + if firstChannel { + chanList.WriteString(item) + firstChannel = false + } else { + chanList.WriteByte(',') + chanList.WriteString(item) + } + case "db-path": + DbPath = item + } + } else if len(parents) == 2 && typeof == indenttext.Value { + if parents[0] == "api-keys" { + ApiKeys[parents[1]] = item + } + } + return false + }) + if err != nil { + log.Fatal(err) + } + + if chanList.Len() > 0 { + ChanJoinStr = splitChannelList(chanList.String()) + } + + if DbPath == "" { + DbPath = getDbPath() + } +} +
A
config/confpath.go
@@ -0,0 +1,90 @@
+package config + +import ( + _ "embed" + "log" + "os" + "path" +) + +//go:embed example.conf +var exampleConfig []byte + +func configPaths() []string { + var configChoices []string + // from most desirable to least desirable config path + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + configChoices = append(configChoices, path.Join(xdgConfigHome, "paprika.conf")) + } + if home := os.Getenv("HOME"); home != "" { + configChoices = append(configChoices, path.Join(home, ".config", "paprika.conf")) + } + systemdConfigDir := os.Getenv("CONFIGURATION_DIRECTORY") + if systemdConfigDir != "" { + configChoices = append(configChoices, path.Join(systemdConfigDir, "paprika.conf")) + } else { + configChoices = append(configChoices, path.Join("/etc", "paprika.conf")) + } + return configChoices +} + +func createConfig(userPath string) { + var file *os.File + fpath := userPath + var err error + var errs []error + + if userPath != "" { + file, err = os.Create(userPath) + if err != nil { + log.Fatalf("Error: Failed to create config: %s", err) + } + } else { + configChoices := configPaths() + for _, v := range configChoices { + file, err = os.Create(v) + if err == nil { + fpath = v + break + } else { + errs = append(errs, err) + } + } + if file == nil { + log.Print("Error: Failed to create config file.") + for i, _ := range configChoices { + log.Printf("- reason: %s", errs[i]) + } + os.Exit(1) + } + } + defer file.Close() + + if _, err = file.Write(exampleConfig); err != nil { + log.Fatalf("Error: Failed to create config: %s", err) + } else { + log.Printf("- %s", fpath) + os.Exit(0) + } +} + +func findConfig() *os.File { + configChoices := configPaths() + + var errs []error + for _, v := range configChoices { + file, err := os.Open(v) + if err == nil { + return file + } else { + errs = append(errs, err) + } + } + + log.Print("Error: Could not open any config paths.") + for i, _ := range configChoices { + log.Printf("- %s", errs[i]) + } + os.Exit(1) + return nil +}
A
config/example.conf
@@ -0,0 +1,37 @@
+# IRCd Parameters +nick: paprika1234 +# make sure to add the port at the end with a colon +host: irc.rizon.net:6667 +# use the word true or false for this field +tls: false + +# Sever password, sent as "PASS :%s", pass +# NOTE (xyz:\n:) is declaring a key with no values. +pass: + # put your pass here or make it a name: value pair +: + +# SASL password (cert support tbd) for nickserv identification +sasl: + # put your pass here or make it a name: value pair +: + +# database, and other state, directory +# defaults to XDG_DATA_HOME or +# STATE_DIRECTORY if using StateDirectory= in a systemd service unit file +db-path: + # e.g. /var/lib/mybot/state +: + +# a newline delimited list of channels +channels: + # the (#) is a comment, use content start marker (') to escape it. + # for syntax-highlighting reasons, you can terminate content with a (') as well. + '#taigobot-test' +: + +# : by itself terminates a list +# API Keys are stored in a map by IDENT: value +api-keys: + test: fdsafdasfdsafdsa +:
A
config/helpers.go
@@ -0,0 +1,76 @@
+package config + +import ( + "io/ioutil" + "log" + "os" + "path" + "strings" +) + +func splitChannelList(channels string) string { + if len(channels) /* len() on string is bytelen */ + 5 /* for "JOIN " */ <= 510 { + return "JOIN " + channels + "\r\n" + } + + // channel lenghts longer than 510bytes have to be split + chans := strings.Split(channels, ",") + lineSize := 0 + first := true + + var ret strings.Builder + + // Splits configured list of channels into a safe set of commands + for _, channel := range chans { + if lineSize + len(channel) > 510 { + lineSize = 0 + first = true + ret.WriteByte('\r') + ret.WriteByte('\n') + } + + if !first { + ret.WriteByte(',') + lineSize += 1 + } else { + ret.WriteString("JOIN ") + lineSize += 5 + first = false + } + ret.WriteString(channel) + lineSize += len(channel) + } + ret.WriteByte('\r') + ret.WriteByte('\n') + + return ret.String() +} + +func getDbPath() string { + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RuntimeDirectory= + systemdStateDir := os.Getenv("STATE_DIRECTORY") + xdgDataDir := os.Getenv("XDG_DATA_HOME") + if systemdStateDir == "" && xdgDataDir == "" { + log.Print("Warning: Nowhere to store state!") + log.Print("- Please add StateDirectory= to your systemd unit or add db-path to your config.") + + // TODO: change this to os.MkdirTemp when 1.17 is more common + dir, err := ioutil.TempDir("", "paprika-bot") + if err != nil { + log.Fatalf("Failed making temporary area: %s", err) + } + log.Printf("Warning: Generated a temporary path: %s", dir) + return dir + } else if xdgDataDir != "" { + return path.Join(xdgDataDir, "paprika") + } else { + return systemdStateDir + } +} + +func usage() { + println("usage: paprika [init] [-c config]\n") + println(" init Initialize configuration for the bot.") + println(" -c config Use config given on the command line.\n") + os.Exit(1) +}
M
database/db.go
→
database/db.go
@@ -1,6 +1,9 @@
package database import ( + "path" + + "git.icyphox.sh/paprika/config" "github.com/dgraph-io/badger/v3" )@@ -12,7 +15,9 @@ *badger.DB
} func Open() (*badger.DB, error) { - db, err := badger.Open(badger.DefaultOptions("./badger")) + db, err := badger.Open( + badger.DefaultOptions(path.Join(config.DbPath, "badger")), + ) if err != nil { return nil, err }
M
go.sum
→
go.sum
@@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/adedomin/indenttext v0.0.0-20211119223124-29d4292dd20b h1:8LTaSMTmm2VPw860DZtPSRHokEna95eEOFIfdvsKwi8= +github.com/adedomin/indenttext v0.0.0-20211119223124-29d4292dd20b/go.mod h1:0W4Kq5zZA0pHRMal7C+rcxF5bQeFQl75XwlV6SBJXeU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
M
main.go
→
main.go
@@ -6,6 +6,7 @@ "log"
"net" "strings" + "git.icyphox.sh/paprika/config" "git.icyphox.sh/paprika/database" "git.icyphox.sh/paprika/plugins" "gopkg.in/irc.v3"@@ -101,8 +102,7 @@
func ircHandler(c *irc.Client, m *irc.Message) { switch m.Command { case "001": - // TODO: load this from config - c.Write("JOIN #taigobot-test") + c.Write(config.ChanJoinStr) case "PRIVMSG": if isCTCPmessage(m) { handleCTCPMessage(c, m)@@ -113,17 +113,17 @@ }
} func main() { - // TODO: load this from config - conn, err := net.Dial("tcp", "irc.rizon.net:6667") + conn, err := net.Dial("tcp", config.Host) if err != nil { log.Fatal(err) } + defer conn.Close() - config := irc.ClientConfig{ - Nick: "paprika112", - Pass: "", - User: "paprika112", - Name: "paprika", + ircConfig := irc.ClientConfig{ + Nick: config.Nick, + Pass: config.Pass, + User: "paprikabot", + Name: config.Nick, Handler: irc.HandlerFunc(ircHandler), }@@ -133,7 +133,7 @@ log.Fatal(err)
} defer database.DB.Close() - client := irc.NewClient(conn, config) + client := irc.NewClient(conn, ircConfig) err = client.Run() if err != nil { log.Fatal(err)