This commit is contained in:
Layla 2023-03-31 20:49:50 +00:00
parent 6e5450aa80
commit 5e6a433b92
15 changed files with 75 additions and 46 deletions

View File

@ -157,6 +157,7 @@ func (app *Bot) onEventComplete(d *discord.Discord, event common.Event) {
} }
// NewBot creates a new bot instance
func NewBot() *Bot { func NewBot() *Bot {
return &Bot{} return &Bot{}
} }

View File

@ -8,25 +8,23 @@ import (
"github.com/yeslayla/birdbot/common" "github.com/yeslayla/birdbot/common"
) )
type PluginLoader struct{} // LoadPlugin loads a plugin and returns its component if successful
func LoadPlugin(pluginPath string) common.Component {
func NewPluginLoader() PluginLoader {
return PluginLoader{}
}
func (loader PluginLoader) LoadPlugin(pluginPath string) common.Component {
plug, err := plugin.Open(pluginPath) plug, err := plugin.Open(pluginPath)
if err != nil { if err != nil {
log.Printf("Failed to load plugin '%s': %s", pluginPath, err) log.Printf("Failed to load plugin '%s': %s", pluginPath, err)
return nil return nil
} }
// Lookup component symbol
sym, err := plug.Lookup("Component") sym, err := plug.Lookup("Component")
if err != nil { if err != nil {
log.Printf("Failed to load plugin '%s': failed to get Component: %s", pluginPath, err) log.Printf("Failed to load plugin '%s': failed to get Component: %s", pluginPath, err)
return nil return nil
} }
// Validate component type
var component common.Component var component common.Component
component, ok := sym.(common.Component) component, ok := sym.(common.Component)
if !ok { if !ok {
@ -36,7 +34,8 @@ func (loader PluginLoader) LoadPlugin(pluginPath string) common.Component {
return component return component
} }
func (loader PluginLoader) LoadPlugins(directory string) []common.Component { // LoadPlugins loads all plugins and componenets in a directory
func LoadPlugins(directory string) []common.Component {
paths, err := os.ReadDir(directory) paths, err := os.ReadDir(directory)
if err != nil { if err != nil {
@ -50,7 +49,7 @@ func (loader PluginLoader) LoadPlugins(directory string) []common.Component {
continue continue
} }
if comp := loader.LoadPlugin(path.Name()); comp != nil { if comp := LoadPlugin(path.Name()); comp != nil {
components = append(components, comp) components = append(components, comp)
} }
} }

View File

@ -4,6 +4,8 @@ type Component interface {
Initialize(birdbot ComponentManager) error Initialize(birdbot ComponentManager) error
} }
// ComponentManager is the primary way for a component to interact with BirdBot
// by listening to events and committing actions
type ComponentManager interface { type ComponentManager interface {
OnReady(func() error) error OnReady(func() error) error
@ -15,8 +17,9 @@ type ComponentManager interface {
OnEventUpdate(func(Event) error) error OnEventUpdate(func(Event) error) error
OnEventComplete(func(Event) error) error OnEventComplete(func(Event) error) error
RegisterGameModule(ID string, plugin GameModule) error // Actions
CreateEvent(event Event) error CreateEvent(event Event) error
Notify(message string) error Notify(message string) error
RegisterGameModule(ID string, plugin GameModule) error
} }

View File

@ -4,6 +4,7 @@ import (
"time" "time"
) )
// Event represents a calendar event
type Event struct { type Event struct {
Name string Name string
ID string ID string

View File

@ -2,6 +2,7 @@ package common
import "fmt" import "fmt"
// User represents a user within BirdBot
type User struct { type User struct {
ID string ID string
AvatarURL string AvatarURL string

View File

@ -1,4 +1,4 @@
package events package components
import ( import (
"fmt" "fmt"
@ -7,20 +7,22 @@ import (
"github.com/yeslayla/birdbot/mastodon" "github.com/yeslayla/birdbot/mastodon"
) )
type AnnounceEventsComponent struct { type announceEventsComponent struct {
bot common.ComponentManager bot common.ComponentManager
mastodon *mastodon.Mastodon mastodon *mastodon.Mastodon
guildID string guildID string
} }
func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) *AnnounceEventsComponent { // NewAnnounceEventsComponent creates a new component
return &AnnounceEventsComponent{ func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) common.Component {
return &announceEventsComponent{
mastodon: mastodon, mastodon: mastodon,
guildID: guildID, guildID: guildID,
} }
} }
func (c *AnnounceEventsComponent) Initialize(birdbot common.ComponentManager) error { // Initialize registers event listeners
func (c *announceEventsComponent) Initialize(birdbot common.ComponentManager) error {
c.bot = birdbot c.bot = birdbot
_ = birdbot.OnEventCreate(c.OnEventCreate) _ = birdbot.OnEventCreate(c.OnEventCreate)
@ -29,10 +31,12 @@ func (c *AnnounceEventsComponent) Initialize(birdbot common.ComponentManager) er
return nil return nil
} }
func (c *AnnounceEventsComponent) OnEventCreate(e common.Event) error { // OnEventCreate notifies about the event creation to given providers
func (c *announceEventsComponent) OnEventCreate(e common.Event) error {
eventURL := fmt.Sprintf("https://discordapp.com/events/%s/%s", c.guildID, e.ID) eventURL := fmt.Sprintf("https://discordapp.com/events/%s/%s", c.guildID, e.ID)
c.bot.Notify(fmt.Sprintf("%s is organizing an event '%s': %s", e.Organizer.DiscordMention(), e.Name, eventURL)) c.bot.Notify(fmt.Sprintf("%s is organizing an event '%s': %s", e.Organizer.DiscordMention(), e.Name, eventURL))
// Toot an announcement if Mastodon is configured
if c.mastodon != nil { if c.mastodon != nil {
err := c.mastodon.Toot(fmt.Sprintf("A new event has been organized '%s': %s", e.Name, eventURL)) err := c.mastodon.Toot(fmt.Sprintf("A new event has been organized '%s': %s", e.Name, eventURL))
if err != nil { if err != nil {
@ -43,7 +47,7 @@ func (c *AnnounceEventsComponent) OnEventCreate(e common.Event) error {
return nil return nil
} }
func (c *AnnounceEventsComponent) OnEventDelete(e common.Event) error { func (c *announceEventsComponent) OnEventDelete(e common.Event) error {
_ = c.bot.Notify(fmt.Sprintf("%s cancelled '%s' on %s, %d!", e.Organizer.DiscordMention(), e.Name, e.DateTime.Month().String(), e.DateTime.Day())) _ = c.bot.Notify(fmt.Sprintf("%s cancelled '%s' on %s, %d!", e.Organizer.DiscordMention(), e.Name, e.DateTime.Month().String(), e.DateTime.Day()))
if c.mastodon != nil { if c.mastodon != nil {

View File

@ -1,4 +1,4 @@
package events package components
import ( import (
"log" "log"
@ -8,21 +8,23 @@ import (
"github.com/yeslayla/birdbot/discord" "github.com/yeslayla/birdbot/discord"
) )
type ManageEventChannelsComponent struct { type manageEventChannelsComponent struct {
session *discord.Discord session *discord.Discord
categoryID string categoryID string
archiveCategoryID string archiveCategoryID string
} }
func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string, session *discord.Discord) *ManageEventChannelsComponent { // NewManageEventChannelsComponent creates a new component
return &ManageEventChannelsComponent{ func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string, session *discord.Discord) common.Component {
return &manageEventChannelsComponent{
session: session, session: session,
categoryID: categoryID, categoryID: categoryID,
archiveCategoryID: archiveCategoryID, archiveCategoryID: archiveCategoryID,
} }
} }
func (c *ManageEventChannelsComponent) Initialize(birdbot common.ComponentManager) error { // Initialize registers event listeners
func (c *manageEventChannelsComponent) Initialize(birdbot common.ComponentManager) error {
_ = birdbot.OnEventCreate(c.OnEventCreate) _ = birdbot.OnEventCreate(c.OnEventCreate)
_ = birdbot.OnEventComplete(c.OnEventComplete) _ = birdbot.OnEventComplete(c.OnEventComplete)
_ = birdbot.OnEventDelete(c.OnEventDelete) _ = birdbot.OnEventDelete(c.OnEventDelete)
@ -30,8 +32,9 @@ func (c *ManageEventChannelsComponent) Initialize(birdbot common.ComponentManage
return nil return nil
} }
func (c *ManageEventChannelsComponent) OnEventCreate(e common.Event) error { // OnEventCreate creates a new channel for an event and moves it to a given category
channel, err := c.session.NewChannelFromName(core.GenerateChannel(e).Name) func (c *manageEventChannelsComponent) OnEventCreate(e common.Event) error {
channel, err := c.session.NewChannelFromName(core.GenerateChannelFromEvent(e).Name)
if err != nil { if err != nil {
log.Print("Failed to create channel for event: ", err) log.Print("Failed to create channel for event: ", err)
} }
@ -45,16 +48,19 @@ func (c *ManageEventChannelsComponent) OnEventCreate(e common.Event) error {
return nil return nil
} }
func (c *ManageEventChannelsComponent) OnEventDelete(e common.Event) error { // OnEventDelete deletes the channel associated with the given event
_, err := c.session.DeleteChannel(core.GenerateChannel(e)) func (c *manageEventChannelsComponent) OnEventDelete(e common.Event) error {
_, err := c.session.DeleteChannel(core.GenerateChannelFromEvent(e))
if err != nil { if err != nil {
log.Print("Failed to create channel for event: ", err) log.Print("Failed to create channel for event: ", err)
} }
return nil return nil
} }
func (c *ManageEventChannelsComponent) OnEventComplete(e common.Event) error { // OnEventComplete archives a given event channel if not given
channel := core.GenerateChannel(e) // an archive category will delete the channel instead
func (c *manageEventChannelsComponent) OnEventComplete(e common.Event) error {
channel := core.GenerateChannelFromEvent(e)
if c.archiveCategoryID != "" { if c.archiveCategoryID != "" {

View File

@ -1,4 +1,4 @@
package events package components
import ( import (
"log" "log"
@ -8,21 +8,24 @@ import (
"github.com/yeslayla/birdbot/discord" "github.com/yeslayla/birdbot/discord"
) )
type RecurringEventsComponent struct { type recurringEventsComponent struct {
session *discord.Discord session *discord.Discord
} }
func NewRecurringEventsComponent() *RecurringEventsComponent { // NewRecurringEventsComponent creates a new component instance
return &RecurringEventsComponent{} func NewRecurringEventsComponent() common.Component {
return &recurringEventsComponent{}
} }
func (c *RecurringEventsComponent) Initialize(birdbot common.ComponentManager) error { // Initialize registers event listeners
func (c *recurringEventsComponent) Initialize(birdbot common.ComponentManager) error {
_ = birdbot.OnEventComplete(c.OnEventComplete) _ = birdbot.OnEventComplete(c.OnEventComplete)
return nil return nil
} }
func (c *RecurringEventsComponent) OnEventComplete(e common.Event) error { // OnEventComplete checks for keywords before creating a new event
func (c *recurringEventsComponent) OnEventComplete(e common.Event) error {
if strings.Contains(strings.ToLower(e.Description), "recurring weekly") { if strings.Contains(strings.ToLower(e.Description), "recurring weekly") {
startTime := e.DateTime.AddDate(0, 0, 7) startTime := e.DateTime.AddDate(0, 0, 7)

View File

@ -15,6 +15,7 @@ type Channel struct {
Verified bool Verified bool
} }
// GenerateEventChannelName deciphers a channel name from a given set of event data
func GenerateEventChannelName(eventName string, location string, dateTime time.Time) string { func GenerateEventChannelName(eventName string, location string, dateTime time.Time) string {
month := GetMonthPrefix(dateTime) month := GetMonthPrefix(dateTime)
day := dateTime.Day() day := dateTime.Day()
@ -31,8 +32,8 @@ func GenerateEventChannelName(eventName string, location string, dateTime time.T
return channel return channel
} }
// GenerateChannel returns a channel object associated with an event // GenerateChannelFromEvent returns a channel object associated with an event
func GenerateChannel(event common.Event) *Channel { func GenerateChannelFromEvent(event common.Event) *Channel {
channelName := GenerateEventChannelName(event.Name, event.Location, event.DateTime) channelName := GenerateEventChannelName(event.Name, event.Location, event.DateTime)

View File

@ -2,12 +2,14 @@ package core
import "strings" import "strings"
// Config is used to modify the behavior of birdbot externally
type Config struct { type Config struct {
Discord DiscordConfig `yaml:"discord"` Discord DiscordConfig `yaml:"discord"`
Mastodon MastodonConfig `yaml:"mastodon"` Mastodon MastodonConfig `yaml:"mastodon"`
Features Features `yaml:"features"` Features Features `yaml:"features"`
} }
// DiscordConfig contains discord specific configuration
type DiscordConfig struct { type DiscordConfig struct {
Token string `yaml:"token" env:"DISCORD_TOKEN"` Token string `yaml:"token" env:"DISCORD_TOKEN"`
GuildID string `yaml:"guild_id" env:"DISCORD_GUILD_ID"` GuildID string `yaml:"guild_id" env:"DISCORD_GUILD_ID"`
@ -17,6 +19,7 @@ type DiscordConfig struct {
NotificationChannel string `yaml:"notification_channel" env:"DISCORD_NOTIFICATION_CHANNEL"` NotificationChannel string `yaml:"notification_channel" env:"DISCORD_NOTIFICATION_CHANNEL"`
} }
// MastodonConfig contains mastodon specific configuration
type MastodonConfig struct { type MastodonConfig struct {
Server string `yaml:"server" env:"MASTODON_SERVER"` Server string `yaml:"server" env:"MASTODON_SERVER"`
Username string `yaml:"user" env:"MASTODON_USER"` Username string `yaml:"user" env:"MASTODON_USER"`
@ -25,6 +28,7 @@ type MastodonConfig struct {
ClientSecret string `yaml:"client_secret" env:"MASTODON_CLIENT_SECRET"` ClientSecret string `yaml:"client_secret" env:"MASTODON_CLIENT_SECRET"`
} }
// Features contains all features flags that can be used to modify functionality
type Features struct { type Features struct {
ManageEventChannels Feature `yaml:"manage_event_channels" env:"BIRD_EVENT_CHANNELS"` ManageEventChannels Feature `yaml:"manage_event_channels" env:"BIRD_EVENT_CHANNELS"`
AnnounceEvents Feature `yaml:"announce_events" env:"BIRD_ANNOUNCE_EVENTS"` AnnounceEvents Feature `yaml:"announce_events" env:"BIRD_ANNOUNCE_EVENTS"`
@ -32,12 +36,16 @@ type Features struct {
LoadGamePlugins Feature `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS"` LoadGamePlugins Feature `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS"`
} }
// Feature is a boolean string used to toggle functionality
type Feature string type Feature string
// IsEnabled returns true when a feature is set to be true
func (value Feature) IsEnabled() bool { func (value Feature) IsEnabled() bool {
return strings.ToLower(string(value)) == "true" return strings.ToLower(string(value)) == "true"
} }
// IsEnabled returns true when a feature is set to be true
// or if the feature flag is not set at all
func (value Feature) IsEnabledByDefault() bool { func (value Feature) IsEnabledByDefault() bool {
v := strings.ToLower(string(value)) v := strings.ToLower(string(value))
if v == "" { if v == "" {

View File

@ -5,13 +5,14 @@ import (
"strings" "strings"
) )
const REMOTE_LOCATION string = "online" // RemoteLocation is the string used to identify a online event
const RemoteLocation string = "online"
// GetCityFromLocation returns the city name of an event's location // GetCityFromLocation returns the city name of an event's location
func GetCityFromLocation(location string) string { func GetCityFromLocation(location string) string {
if location == REMOTE_LOCATION { if location == RemoteLocation {
return fmt.Sprint("-", REMOTE_LOCATION) return fmt.Sprint("-", RemoteLocation)
} }
parts := strings.Split(location, " ") parts := strings.Split(location, " ")
index := -1 index := -1

View File

@ -31,7 +31,7 @@ func NewEvent(guildEvent *discordgo.GuildScheduledEvent) common.Event {
event.Completed = guildEvent.Status == discordgo.GuildScheduledEventStatusCompleted event.Completed = guildEvent.Status == discordgo.GuildScheduledEventStatusCompleted
if guildEvent.EntityType != discordgo.GuildScheduledEventEntityTypeExternal { if guildEvent.EntityType != discordgo.GuildScheduledEventEntityTypeExternal {
event.Location = core.REMOTE_LOCATION event.Location = core.RemoteLocation
} else { } else {
event.Location = guildEvent.EntityMetadata.Location event.Location = guildEvent.EntityMetadata.Location
} }

11
main.go
View File

@ -10,8 +10,8 @@ import (
"github.com/ilyakaznacheev/cleanenv" "github.com/ilyakaznacheev/cleanenv"
"github.com/yeslayla/birdbot/app" "github.com/yeslayla/birdbot/app"
"github.com/yeslayla/birdbot/components"
"github.com/yeslayla/birdbot/core" "github.com/yeslayla/birdbot/core"
"github.com/yeslayla/birdbot/events"
) )
const PluginsDirectory = "./plugins" const PluginsDirectory = "./plugins"
@ -59,18 +59,17 @@ func main() {
loader := app.NewComponentLoader(bot) loader := app.NewComponentLoader(bot)
if cfg.Features.AnnounceEvents.IsEnabledByDefault() { if cfg.Features.AnnounceEvents.IsEnabledByDefault() {
loader.LoadComponent(events.NewAnnounceEventsComponent(bot.Mastodon, cfg.Discord.NotificationChannel)) loader.LoadComponent(components.NewAnnounceEventsComponent(bot.Mastodon, cfg.Discord.NotificationChannel))
} }
if cfg.Features.ManageEventChannels.IsEnabledByDefault() { if cfg.Features.ManageEventChannels.IsEnabledByDefault() {
loader.LoadComponent(events.NewManageEventChannelsComponent(cfg.Discord.EventCategory, cfg.Discord.ArchiveCategory, bot.Session)) loader.LoadComponent(components.NewManageEventChannelsComponent(cfg.Discord.EventCategory, cfg.Discord.ArchiveCategory, bot.Session))
} }
if cfg.Features.ReccurringEvents.IsEnabledByDefault() { if cfg.Features.ReccurringEvents.IsEnabledByDefault() {
loader.LoadComponent(events.NewRecurringEventsComponent()) loader.LoadComponent(components.NewRecurringEventsComponent())
} }
if _, err := os.Stat(PluginsDirectory); !os.IsNotExist(err) { if _, err := os.Stat(PluginsDirectory); !os.IsNotExist(err) {
pluginLoader := app.NewPluginLoader() components := app.LoadPlugins(PluginsDirectory)
components := pluginLoader.LoadPlugins(PluginsDirectory)
for _, comp := range components { for _, comp := range components {
loader.LoadComponent(comp) loader.LoadComponent(comp)
} }

View File

@ -11,6 +11,7 @@ type Mastodon struct {
client *mastodon.Client client *mastodon.Client
} }
// NewMastodon initializes a new Mastodon client
func NewMastodon(server string, clientID string, clientSecret string, username string, password string) *Mastodon { func NewMastodon(server string, clientID string, clientSecret string, username string, password string) *Mastodon {
m := &Mastodon{} m := &Mastodon{}

View File

@ -6,6 +6,7 @@ import (
"github.com/mattn/go-mastodon" "github.com/mattn/go-mastodon"
) )
// Toot publishes a toot on Mastodon
func (m *Mastodon) Toot(message string) error { func (m *Mastodon) Toot(message string) error {
_, err := m.client.PostStatus(context.Background(), &mastodon.Toot{ _, err := m.client.PostStatus(context.Background(), &mastodon.Toot{
Status: message, Status: message,