mirror of
https://github.com/Luzifer/discord-community.git
synced 2024-11-09 23:50:04 +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
|
.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
|
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
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 (
|
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 {
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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"`
|
||||||
|
@ -47,15 +58,21 @@ type twitchStreamScheduleResponse struct {
|
||||||
Pagination struct {
|
Pagination 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
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