From 68c8800c62e04ea8e7ffcebea319b3ae04f10a7b Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 23 Jul 2021 00:54:11 +0200 Subject: [PATCH] Improve module concept and config Signed-off-by: Knut Ahlers --- .gitignore | 1 + attributeStore.go | 72 +++++++++++++++++++++++++++++++++++ config.go | 38 +++++++++++++++++++ go.mod | 3 +- helpers.go | 4 ++ main.go | 34 +++++++++++------ mod_presence.go | 31 +++++++++++---- mod_streamSchedule.go | 87 ++++++++++++++++++++++++++----------------- modules.go | 45 ++++++++++++++++++++++ 9 files changed, 261 insertions(+), 54 deletions(-) create mode 100644 attributeStore.go create mode 100644 config.go create mode 100644 helpers.go create mode 100644 modules.go diff --git a/.gitignore b/.gitignore index 4c49bd7..eaaf257 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +discord-community .env diff --git a/attributeStore.go b/attributeStore.go new file mode 100644 index 0000000..f8279f1 --- /dev/null +++ b/attributeStore.go @@ -0,0 +1,72 @@ +package main + +import ( + "errors" + "fmt" +) + +var ( + errValueNotSet = errors.New("specified value not found") + errValueMismatch = errors.New("specified value has different format") +) + +type moduleAttributeStore map[string]interface{} + +func (m moduleAttributeStore) MustInt64(name string, defVal *int64) int64 { + v, err := m.Int64(name) + if err != nil { + if defVal != nil { + return *defVal + } + panic(err) + } + return v +} + +func (m moduleAttributeStore) MustString(name string, defVal *string) string { + v, err := m.String(name) + if err != nil { + if defVal != nil { + return *defVal + } + panic(err) + } + return v +} + +func (m moduleAttributeStore) Int64(name string) (int64, error) { + v, ok := m[name] + if !ok { + return 0, errValueNotSet + } + + switch v.(type) { + case int: + return int64(v.(int)), nil + case int16: + return int64(v.(int16)), nil + case int32: + return int64(v.(int32)), nil + case int64: + return v.(int64), nil + } + + return 0, errValueMismatch +} + +func (m moduleAttributeStore) String(name string) (string, error) { + v, ok := m[name] + if !ok { + return "", errValueNotSet + } + + if sv, ok := v.(string); ok { + return sv, nil + } + + if iv, ok := v.(fmt.Stringer); ok { + return iv.String(), nil + } + + return "", errValueMismatch +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..4e479af --- /dev/null +++ b/config.go @@ -0,0 +1,38 @@ +package main + +import ( + "os" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type ( + configFile struct { + BotToken string `yaml:"bot_token"` + GuildID string `yaml:"guild_id"` + + ModuleConfigs []moduleConfig `yaml:"module_configs"` + } + + moduleConfig struct { + Type string `yaml:"type"` + Attributes moduleAttributeStore `yaml:"attributes"` + } +) + +func newConfigFromFile(filename string) (*configFile, error) { + f, err := os.Open(filename) + if err != nil { + return nil, errors.Wrap(err, "opening config file") + } + defer f.Close() + + var ( + decoder = yaml.NewDecoder(f) + tmp configFile + ) + + decoder.SetStrict(true) + return &tmp, errors.Wrap(decoder.Decode(&tmp), "decoding config") +} diff --git a/go.mod b/go.mod index 6a21156..8277c2c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Luzifer/tezrian-discord +module github.com/Luzifer/discord-community go 1.16 @@ -9,4 +9,5 @@ require ( github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..d3dbc2c --- /dev/null +++ b/helpers.go @@ -0,0 +1,4 @@ +package main + +func ptrInt64(v int64) *int64 { return &v } +func ptrString(v string) *string { return &v } diff --git a/main.go b/main.go index 5027df9..82c561c 100644 --- a/main.go +++ b/main.go @@ -16,17 +16,13 @@ import ( var ( cfg = struct { - BotToken string `flag:"bot-token" description:"Token from the App Bot User section"` - GuildID string `flag:"guild-id" description:"ID of the Discord server (guild)"` + Config string `flag:"config,c" default:"config.yaml" description:"Path to config file"` Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` }{} - crontab = cron.New() - discord *discordgo.Session - - discordHandlers []interface{} + config *configFile version = "dev" ) @@ -38,7 +34,7 @@ func init() { } if cfg.VersionAndExit { - fmt.Printf("tezrian-discord %s\n", version) + fmt.Printf("discord-community %s\n", version) os.Exit(0) } @@ -50,17 +46,33 @@ func init() { } func main() { - var err error + var ( + crontab = cron.New() + discord *discordgo.Session + err error + ) + + if config, err = newConfigFromFile(cfg.Config); err != nil { + log.WithError(err).Fatal("Unable to load config file") + } // Connect to Discord - if discord, err = discordgo.New(strings.Join([]string{"Bot", cfg.BotToken}, " ")); err != nil { + if discord, err = discordgo.New(strings.Join([]string{"Bot", config.BotToken}, " ")); err != nil { log.WithError(err).Fatal("Unable to create discord client") } discord.Identify.Intents = discordgo.IntentsAll - for _, hdl := range discordHandlers { - discord.AddHandler(hdl) + for _, mc := range config.ModuleConfigs { + logger := log.WithField("module", mc.Type) + mod := GetModuleByName(mc.Type) + if mod == nil { + logger.Fatal("Found configuration for unsupported module") + } + + if err = mod.Initialize(crontab, discord, mc.Attributes); err != nil { + logger.WithError(err).Fatal("Unable to initialize module") + } } if err = discord.Open(); err != nil { diff --git a/mod_presence.go b/mod_presence.go index 86ca4de..17a81cd 100644 --- a/mod_presence.go +++ b/mod_presence.go @@ -6,33 +6,50 @@ import ( "strings" "time" + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" + "github.com/robfig/cron/v3" log "github.com/sirupsen/logrus" ) func init() { - if _, err := crontab.AddFunc("* * * * *", cronUpdatePresence); err != nil { - log.WithError(err).Fatal("Unable to add cronUpdatePresence function") - } + RegisterModule("presence", func() module { return &modPresence{} }) } -func cronUpdatePresence() { +type modPresence struct { + attrs moduleAttributeStore + discord *discordgo.Session +} + +func (m *modPresence) Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error { + m.attrs = attrs + m.discord = discord + + if _, err := crontab.AddFunc(attrs.MustString("cron", ptrString("* * * * *")), m.cronUpdatePresence); err != nil { + return errors.Wrap(err, "adding cron function") + } + + return nil +} + +func (m modPresence) cronUpdatePresence() { var nextStream *time.Time = nil // FIXME: Get next stream status status := "mit Seelen" if nextStream != nil { - status = fmt.Sprintf("in: %s", durationToHumanReadable(time.Since(*nextStream))) + status = fmt.Sprintf("in: %s", m.durationToHumanReadable(time.Since(*nextStream))) } - if err := discord.UpdateGameStatus(0, status); err != nil { + if err := m.discord.UpdateGameStatus(0, status); err != nil { log.WithError(err).Error("Unable to update status") } log.Debug("Updated presence") } -func durationToHumanReadable(d time.Duration) string { +func (m modPresence) durationToHumanReadable(d time.Duration) string { var elements []string d = time.Duration(math.Abs(float64(d))) diff --git a/mod_streamSchedule.go b/mod_streamSchedule.go index 11e58da..7e934a2 100644 --- a/mod_streamSchedule.go +++ b/mod_streamSchedule.go @@ -12,6 +12,7 @@ import ( "github.com/Luzifer/go_helpers/v2/backoff" "github.com/bwmarrin/discordgo" "github.com/pkg/errors" + "github.com/robfig/cron/v3" log "github.com/sirupsen/logrus" ) @@ -22,40 +23,56 @@ const ( streamSchedulePastTime = 15 * time.Minute ) -type twitchStreamScheduleResponse struct { - Data struct { - Segments []struct { - ID string `json:"id"` - StartTime *time.Time `json:"start_time"` - EndTime *time.Time `json:"end_time"` - Title string `json:"title"` - CanceledUntil *time.Time `json:"canceled_until"` - Category *struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"category"` - IsRecurring bool `json:"is_recurring"` - } `json:"segments"` - BroadcasterID string `json:"broadcaster_id"` - BroadcasterName string `json:"broadcaster_name"` - BroadcasterLogin string `json:"broadcaster_login"` - Vacation *struct { - StartTime *time.Time `json:"start_time"` - EndTime *time.Time `json:"end_time"` - } `json:"vacation"` - } `json:"data"` - Pagination struct { - Cursor string `json:"cursor"` - } `json:"pagination"` +func init() { + RegisterModule("schedule", func() module { return &modStreamSchedule{} }) } -func init() { - if _, err := crontab.AddFunc("*/10 * * * *", cronUpdateSchedule); err != nil { +type ( + modStreamSchedule struct { + attrs moduleAttributeStore + discord *discordgo.Session + } + + twitchStreamScheduleResponse struct { + Data struct { + Segments []struct { + ID string `json:"id"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + Title string `json:"title"` + CanceledUntil *time.Time `json:"canceled_until"` + Category *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"category"` + IsRecurring bool `json:"is_recurring"` + } `json:"segments"` + BroadcasterID string `json:"broadcaster_id"` + BroadcasterName string `json:"broadcaster_name"` + BroadcasterLogin string `json:"broadcaster_login"` + Vacation *struct { + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + } `json:"vacation"` + } `json:"data"` + Pagination struct { + Cursor string `json:"cursor"` + } `json:"pagination"` + } +) + +func (m *modStreamSchedule) Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error { + m.attrs = attrs + m.discord = discord + + if _, err := crontab.AddFunc(attrs.MustString("cron", ptrString("*/10 * * * *")), m.cronUpdateSchedule); err != nil { log.WithError(err).Fatal("Unable to add cronUpdatePresence function") } + + return nil } -func cronUpdateSchedule() { +func (m modStreamSchedule) cronUpdateSchedule() { var data twitchStreamScheduleResponse if err := backoff.NewBackoff().WithMaxIterations(twitchAPIRequestLimit).Retry(func() error { ctx, cancel := context.WithTimeout(context.Background(), twitchAPIRequestTimeout) @@ -111,7 +128,7 @@ func cronUpdateSchedule() { } msgEmbed.Fields = append(msgEmbed.Fields, &discordgo.MessageEmbedField{ - Name: formatGermanShort(*seg.StartTime), + Name: m.formatGermanShort(*seg.StartTime), Value: title, Inline: false, }) @@ -121,7 +138,7 @@ func cronUpdateSchedule() { } } - msgs, err := discord.ChannelMessages(discordAnnouncementChannel, 100, "", "", "") + msgs, err := m.discord.ChannelMessages(discordAnnouncementChannel, 100, "", "", "") if err != nil { log.WithError(err).Error("Unable to fetch announcement channel messages") return @@ -139,14 +156,14 @@ func cronUpdateSchedule() { if managedMsg != nil { oldEmbed := managedMsg.Embeds[0] - if !embedNeedsUpdate(oldEmbed, msgEmbed) { + if !m.embedNeedsUpdate(oldEmbed, msgEmbed) { log.Debug("Stream Schedule is up-to-date") return } - _, err = discord.ChannelMessageEditEmbed(discordAnnouncementChannel, managedMsg.ID, msgEmbed) + _, err = m.discord.ChannelMessageEditEmbed(discordAnnouncementChannel, managedMsg.ID, msgEmbed) } else { - _, err = discord.ChannelMessageSendEmbed(discordAnnouncementChannel, msgEmbed) + _, err = m.discord.ChannelMessageSendEmbed(discordAnnouncementChannel, msgEmbed) } if err != nil { log.WithError(err).Error("Unable to announce streamplan") @@ -156,7 +173,7 @@ func cronUpdateSchedule() { log.Info("Updated Stream Schedule") } -func formatGermanShort(t time.Time) string { +func (m modStreamSchedule) formatGermanShort(t time.Time) string { wd := map[time.Weekday]string{ time.Monday: "Mo.", time.Tuesday: "Di.", @@ -175,7 +192,7 @@ func formatGermanShort(t time.Time) string { return strings.Join([]string{wd, t.In(tz).Format("02.01. 15:04"), "Uhr"}, " ") } -func embedNeedsUpdate(o, n *discordgo.MessageEmbed) bool { +func (m modStreamSchedule) embedNeedsUpdate(o, n *discordgo.MessageEmbed) bool { if o.Title != n.Title { return true } diff --git a/modules.go b/modules.go new file mode 100644 index 0000000..846277b --- /dev/null +++ b/modules.go @@ -0,0 +1,45 @@ +package main + +import ( + "sync" + + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" + "github.com/robfig/cron/v3" +) + +var ( + moduleRegister = map[string]moduleInitFn{} + moduleRegisterLock sync.RWMutex +) + +type ( + module interface { + Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error + } + + moduleInitFn func() module +) + +func GetModuleByName(name string) module { + moduleRegisterLock.RLock() + defer moduleRegisterLock.RUnlock() + + mif, ok := moduleRegister[name] + if !ok { + return nil + } + + return mif() +} + +func RegisterModule(name string, modInit moduleInitFn) { + moduleRegisterLock.Lock() + defer moduleRegisterLock.Unlock() + + if _, ok := moduleRegister[name]; ok { + panic(errors.Errorf("duplicate module register %q", name)) + } + + moduleRegister[name] = modInit +}