Improve module concept and config

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2021-07-23 00:54:11 +02:00
parent b6e1ea94e8
commit 68c8800c62
Signed by: luzifer
GPG key ID: 0066F03ED215AD7D
9 changed files with 261 additions and 54 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
discord-community
.env .env

72
attributeStore.go Normal file
View file

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

38
config.go Normal file
View file

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

3
go.mod
View file

@ -1,4 +1,4 @@
module github.com/Luzifer/tezrian-discord module github.com/Luzifer/discord-community
go 1.16 go 1.16
@ -9,4 +9,5 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
gopkg.in/yaml.v2 v2.4.0
) )

4
helpers.go Normal file
View file

@ -0,0 +1,4 @@
package main
func ptrInt64(v int64) *int64 { return &v }
func ptrString(v string) *string { return &v }

34
main.go
View file

@ -16,17 +16,13 @@ import (
var ( var (
cfg = struct { cfg = struct {
BotToken string `flag:"bot-token" description:"Token from the App Bot User section"` Config string `flag:"config,c" default:"config.yaml" description:"Path to config file"`
GuildID string `flag:"guild-id" description:"ID of the Discord server (guild)"`
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` 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)"` 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"` VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
}{} }{}
crontab = cron.New() config *configFile
discord *discordgo.Session
discordHandlers []interface{}
version = "dev" version = "dev"
) )
@ -38,7 +34,7 @@ func init() {
} }
if cfg.VersionAndExit { if cfg.VersionAndExit {
fmt.Printf("tezrian-discord %s\n", version) fmt.Printf("discord-community %s\n", version)
os.Exit(0) os.Exit(0)
} }
@ -50,17 +46,33 @@ func init() {
} }
func main() { 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 // 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") log.WithError(err).Fatal("Unable to create discord client")
} }
discord.Identify.Intents = discordgo.IntentsAll discord.Identify.Intents = discordgo.IntentsAll
for _, hdl := range discordHandlers { for _, mc := range config.ModuleConfigs {
discord.AddHandler(hdl) 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 { if err = discord.Open(); err != nil {

View file

@ -6,33 +6,50 @@ import (
"strings" "strings"
"time" "time"
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func init() { func init() {
if _, err := crontab.AddFunc("* * * * *", cronUpdatePresence); err != nil { RegisterModule("presence", func() module { return &modPresence{} })
log.WithError(err).Fatal("Unable to add cronUpdatePresence function")
}
} }
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 var nextStream *time.Time = nil
// FIXME: Get next stream status // FIXME: Get next stream status
status := "mit Seelen" status := "mit Seelen"
if nextStream != nil { 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.WithError(err).Error("Unable to update status")
} }
log.Debug("Updated presence") log.Debug("Updated presence")
} }
func durationToHumanReadable(d time.Duration) string { func (m modPresence) durationToHumanReadable(d time.Duration) string {
var elements []string var elements []string
d = time.Duration(math.Abs(float64(d))) d = time.Duration(math.Abs(float64(d)))

View file

@ -12,6 +12,7 @@ import (
"github.com/Luzifer/go_helpers/v2/backoff" "github.com/Luzifer/go_helpers/v2/backoff"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -22,7 +23,17 @@ const (
streamSchedulePastTime = 15 * time.Minute streamSchedulePastTime = 15 * time.Minute
) )
type twitchStreamScheduleResponse struct { func init() {
RegisterModule("schedule", func() module { return &modStreamSchedule{} })
}
type (
modStreamSchedule struct {
attrs moduleAttributeStore
discord *discordgo.Session
}
twitchStreamScheduleResponse struct {
Data struct { Data struct {
Segments []struct { Segments []struct {
ID string `json:"id"` ID string `json:"id"`
@ -48,14 +59,20 @@ type twitchStreamScheduleResponse struct {
Cursor string `json:"cursor"` Cursor string `json:"cursor"`
} `json:"pagination"` } `json:"pagination"`
} }
)
func init() { func (m *modStreamSchedule) Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error {
if _, err := crontab.AddFunc("*/10 * * * *", cronUpdateSchedule); err != nil { 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") log.WithError(err).Fatal("Unable to add cronUpdatePresence function")
} }
return nil
} }
func cronUpdateSchedule() { func (m modStreamSchedule) cronUpdateSchedule() {
var data twitchStreamScheduleResponse var data twitchStreamScheduleResponse
if err := backoff.NewBackoff().WithMaxIterations(twitchAPIRequestLimit).Retry(func() error { if err := backoff.NewBackoff().WithMaxIterations(twitchAPIRequestLimit).Retry(func() error {
ctx, cancel := context.WithTimeout(context.Background(), twitchAPIRequestTimeout) ctx, cancel := context.WithTimeout(context.Background(), twitchAPIRequestTimeout)
@ -111,7 +128,7 @@ func cronUpdateSchedule() {
} }
msgEmbed.Fields = append(msgEmbed.Fields, &discordgo.MessageEmbedField{ msgEmbed.Fields = append(msgEmbed.Fields, &discordgo.MessageEmbedField{
Name: formatGermanShort(*seg.StartTime), Name: m.formatGermanShort(*seg.StartTime),
Value: title, Value: title,
Inline: false, Inline: false,
}) })
@ -121,7 +138,7 @@ func cronUpdateSchedule() {
} }
} }
msgs, err := discord.ChannelMessages(discordAnnouncementChannel, 100, "", "", "") msgs, err := m.discord.ChannelMessages(discordAnnouncementChannel, 100, "", "", "")
if err != nil { if err != nil {
log.WithError(err).Error("Unable to fetch announcement channel messages") log.WithError(err).Error("Unable to fetch announcement channel messages")
return return
@ -139,14 +156,14 @@ func cronUpdateSchedule() {
if managedMsg != nil { if managedMsg != nil {
oldEmbed := managedMsg.Embeds[0] oldEmbed := managedMsg.Embeds[0]
if !embedNeedsUpdate(oldEmbed, msgEmbed) { if !m.embedNeedsUpdate(oldEmbed, msgEmbed) {
log.Debug("Stream Schedule is up-to-date") log.Debug("Stream Schedule is up-to-date")
return return
} }
_, err = discord.ChannelMessageEditEmbed(discordAnnouncementChannel, managedMsg.ID, msgEmbed) _, err = m.discord.ChannelMessageEditEmbed(discordAnnouncementChannel, managedMsg.ID, msgEmbed)
} else { } else {
_, err = discord.ChannelMessageSendEmbed(discordAnnouncementChannel, msgEmbed) _, err = m.discord.ChannelMessageSendEmbed(discordAnnouncementChannel, msgEmbed)
} }
if err != nil { if err != nil {
log.WithError(err).Error("Unable to announce streamplan") log.WithError(err).Error("Unable to announce streamplan")
@ -156,7 +173,7 @@ func cronUpdateSchedule() {
log.Info("Updated Stream Schedule") log.Info("Updated Stream Schedule")
} }
func formatGermanShort(t time.Time) string { func (m modStreamSchedule) formatGermanShort(t time.Time) string {
wd := map[time.Weekday]string{ wd := map[time.Weekday]string{
time.Monday: "Mo.", time.Monday: "Mo.",
time.Tuesday: "Di.", 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"}, " ") 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 { if o.Title != n.Title {
return true return true
} }

45
modules.go Normal file
View file

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