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

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

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 (
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 {

View File

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

View File

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

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
}