mirror of
https://github.com/Luzifer/discord-community.git
synced 2024-12-20 10:21:22 +00:00
Improve module concept and config
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
b6e1ea94e8
commit
68c8800c62
9 changed files with 261 additions and 54 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
discord-community
|
||||
.env
|
||||
|
|
72
attributeStore.go
Normal file
72
attributeStore.go
Normal 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
38
config.go
Normal 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
3
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
|
||||
)
|
||||
|
|
4
helpers.go
Normal file
4
helpers.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package main
|
||||
|
||||
func ptrInt64(v int64) *int64 { return &v }
|
||||
func ptrString(v string) *string { return &v }
|
34
main.go
34
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 {
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
45
modules.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue