diff --git a/app/bot.go b/app/bot.go index 66e6c7e..983c3b0 100644 --- a/app/bot.go +++ b/app/bot.go @@ -1,13 +1,10 @@ package app import ( - "errors" "fmt" "log" - "os" - "strings" - "github.com/ilyakaznacheev/cleanenv" + "github.com/yeslayla/birdbot/common" "github.com/yeslayla/birdbot/core" "github.com/yeslayla/birdbot/discord" "github.com/yeslayla/birdbot/mastodon" @@ -17,34 +14,29 @@ var Version string var Build string type Bot struct { - session *discord.Discord - mastodon *mastodon.Mastodon + Session *discord.Discord + Mastodon *mastodon.Mastodon // Discord Objects guildID string eventCategoryID string archiveCategoryID string notificationChannelID string + + onReadyHandlers [](func() error) + onNotifyHandlers [](func(string) error) + + onEventCreatedHandlers [](func(common.Event) error) + onEventDeletedHandlers [](func(common.Event) error) + onEventUpdatedHandlers [](func(common.Event) error) + onEventCompletedHandlers [](func(common.Event) error) + + gameModules []common.GameModule } // Initalize creates the discord session and registers handlers -func (app *Bot) Initialize(config_path string) error { - log.Printf("Using config: %s", config_path) - cfg := &core.Config{} +func (app *Bot) Initialize(cfg *core.Config) error { - _, err := os.Stat(config_path) - if errors.Is(err, os.ErrNotExist) { - log.Printf("Config file not found: '%s'", config_path) - err := cleanenv.ReadEnv(cfg) - if err != nil { - return err - } - } else { - err := cleanenv.ReadConfig(config_path, cfg) - if err != nil { - return err - } - } // Load directly from config app.guildID = cfg.Discord.GuildID app.eventCategoryID = cfg.Discord.EventCategory @@ -58,30 +50,30 @@ func (app *Bot) Initialize(config_path string) error { if cfg.Mastodon.ClientID != "" && cfg.Mastodon.ClientSecret != "" && cfg.Mastodon.Username != "" && cfg.Mastodon.Password != "" && cfg.Mastodon.Server != "" { - app.mastodon = mastodon.NewMastodon(cfg.Mastodon.Server, cfg.Mastodon.ClientID, cfg.Mastodon.ClientSecret, + app.Mastodon = mastodon.NewMastodon(cfg.Mastodon.Server, cfg.Mastodon.ClientID, cfg.Mastodon.ClientSecret, cfg.Mastodon.Username, cfg.Mastodon.Password) } - app.session = discord.New(app.guildID, cfg.Discord.Token) + app.Session = discord.New(app.guildID, cfg.Discord.Token) // Register Event Handlers - app.session.OnReady(app.onReady) - app.session.OnEventCreate(app.onEventCreate) - app.session.OnEventDelete(app.onEventDelete) - app.session.OnEventUpdate(app.onEventUpdate) + app.Session.OnReady(app.onReady) + app.Session.OnEventCreate(app.onEventCreate) + app.Session.OnEventDelete(app.onEventDelete) + app.Session.OnEventUpdate(app.onEventUpdate) return nil } // Run opens the session with Discord until exit func (app *Bot) Run() error { - return app.session.Run() + return app.Session.Run() } // Stop triggers a graceful shutdown of the app func (app *Bot) Stop() { log.Print("Shuting down...") - app.session.Stop() + app.Session.Stop() } // Notify sends a message to the notification channe; @@ -93,110 +85,76 @@ func (app *Bot) Notify(message string) { log.Print("Notification: ", message) - channel := app.session.NewChannelFromID(app.notificationChannelID) + channel := app.Session.NewChannelFromID(app.notificationChannelID) if channel == nil { log.Printf("Failed notification: channel was not found with ID '%v'", app.notificationChannelID) } - err := app.session.SendMessage(channel, message) + err := app.Session.SendMessage(channel, message) if err != nil { log.Print("Failed notification: ", err) } + + for _, handler := range app.onNotifyHandlers { + if err := handler(message); err != nil { + log.Println(err) + } + } } func (app *Bot) onReady(d *discord.Discord) { - app.session.SetStatus(fmt.Sprintf("with fire! (%s)", Version)) + app.Session.SetStatus(fmt.Sprintf("with fire! (%s)", Version)) + + for _, handler := range app.onReadyHandlers { + if err := handler(); err != nil { + log.Println(err) + } + } } -func (app *Bot) onEventCreate(d *discord.Discord, event *core.Event) { +func (app *Bot) onEventCreate(d *discord.Discord, event common.Event) { log.Print("Event Created: '", event.Name, "':'", event.Location, "'") - - channel, err := app.session.NewChannelFromName(event.Channel().Name) - if err != nil { - log.Print("Failed to create channel for event: ", err) - } - - if app.eventCategoryID != "" { - err = app.session.MoveChannelToCategory(channel, app.eventCategoryID) - if err != nil { - log.Printf("Failed to move channel to events category '%s': %v", channel.Name, err) + for _, handler := range app.onEventCreatedHandlers { + if err := handler(event); err != nil { + log.Println(err) } } - eventURL := fmt.Sprintf("https://discordapp.com/events/%s/%s", app.guildID, event.ID) - app.Notify(fmt.Sprintf("%s is organizing an event '%s': %s", event.Organizer.Mention(), event.Name, eventURL)) - - if app.mastodon != nil { - err = app.mastodon.Toot(fmt.Sprintf("A new event has been organized '%s': %s", event.Name, eventURL)) - if err != nil { - fmt.Println("Failed to send Mastodon Toot:", err) - } - } } -func (app *Bot) onEventDelete(d *discord.Discord, event *core.Event) { +func (app *Bot) onEventDelete(d *discord.Discord, event common.Event) { - _, err := app.session.DeleteChannel(event.Channel()) - if err != nil { - log.Print("Failed to create channel for event: ", err) - } - - app.Notify(fmt.Sprintf("%s cancelled '%s' on %s, %d!", event.Organizer.Mention(), event.Name, event.DateTime.Month().String(), event.DateTime.Day())) - - if app.mastodon != nil { - err = app.mastodon.Toot(fmt.Sprintf("'%s' cancelled on %s, %d!", event.Name, event.DateTime.Month().String(), event.DateTime.Day())) - if err != nil { - fmt.Println("Failed to send Mastodon Toot:", err) + for _, handler := range app.onEventDeletedHandlers { + if err := handler(event); err != nil { + log.Println(err) } } + } -func (app *Bot) onEventUpdate(d *discord.Discord, event *core.Event) { +func (app *Bot) onEventUpdate(d *discord.Discord, event common.Event) { + + for _, handler := range app.onEventUpdatedHandlers { + if err := handler(event); err != nil { + log.Println(err) + } + } + // Pass event onwards if event.Completed { app.onEventComplete(d, event) } } -func (app *Bot) onEventComplete(d *discord.Discord, event *core.Event) { +func (app *Bot) onEventComplete(d *discord.Discord, event common.Event) { - channel := event.Channel() - - if app.archiveCategoryID != "" { - - if err := app.session.MoveChannelToCategory(channel, app.archiveCategoryID); err != nil { - log.Print("Failed to move channel to archive category: ", err) - } - - if err := app.session.ArchiveChannel(channel); err != nil { - log.Print("Failed to archive channel: ", err) - } - - log.Printf("Archived channel: '%s'", channel.Name) - - } else { - - // Delete Channel - _, err := app.session.DeleteChannel(channel) - if err != nil { - log.Print("Failed to delete channel: ", err) - } - - log.Printf("Deleted channel: '%s'", channel.Name) - } - - if strings.Contains(strings.ToLower(event.Description), "recurring weekly") { - startTime := event.DateTime.AddDate(0, 0, 7) - finishTime := event.CompleteTime.AddDate(0, 0, 7) - nextEvent := event - nextEvent.DateTime = startTime - nextEvent.CompleteTime = finishTime - - if err := app.session.CreateEvent(nextEvent); err != nil { - log.Print("Failed to create recurring event: ", err) + for _, handler := range app.onEventCompletedHandlers { + if err := handler(event); err != nil { + log.Println(err) } } + } func NewBot() *Bot { diff --git a/app/component_loader.go b/app/component_loader.go new file mode 100644 index 0000000..76b19c3 --- /dev/null +++ b/app/component_loader.go @@ -0,0 +1,64 @@ +package app + +import ( + "fmt" + "log" + + "github.com/yeslayla/birdbot/common" +) + +type ComponentLoader struct { + bot *Bot +} + +func NewComponentLoader(bot *Bot) *ComponentLoader { + return &ComponentLoader{ + bot: bot, + } +} + +func (loader *ComponentLoader) LoadComponent(component common.Component) { + if err := component.Initialize(loader); err != nil { + log.Print("Failed to load component: ", err) + } +} + +func (loader *ComponentLoader) OnReady(handler func() error) error { + loader.bot.onReadyHandlers = append(loader.bot.onReadyHandlers, handler) + return nil +} + +func (loader *ComponentLoader) OnNotify(handler func(string) error) error { + loader.bot.onNotifyHandlers = append(loader.bot.onNotifyHandlers, handler) + return nil +} + +func (loader *ComponentLoader) OnEventCreate(handler func(common.Event) error) error { + loader.bot.onEventCreatedHandlers = append(loader.bot.onEventCreatedHandlers, handler) + return nil +} +func (loader *ComponentLoader) OnEventDelete(handler func(common.Event) error) error { + loader.bot.onEventDeletedHandlers = append(loader.bot.onEventDeletedHandlers, handler) + return nil +} +func (loader *ComponentLoader) OnEventUpdate(handler func(common.Event) error) error { + loader.bot.onEventUpdatedHandlers = append(loader.bot.onEventUpdatedHandlers, handler) + return nil +} +func (loader *ComponentLoader) OnEventComplete(handler func(common.Event) error) error { + loader.bot.onEventCompletedHandlers = append(loader.bot.onEventCompletedHandlers, handler) + return nil +} + +func (loader *ComponentLoader) RegisterGameModule(ID string, plugin common.GameModule) error { + return fmt.Errorf("unimplemented") +} + +func (loader *ComponentLoader) CreateEvent(event common.Event) error { + return loader.bot.Session.CreateEvent(event) +} + +func (loader *ComponentLoader) Notify(message string) error { + loader.bot.Notify(message) + return nil +} diff --git a/common/component.go b/common/component.go new file mode 100644 index 0000000..35e4c0c --- /dev/null +++ b/common/component.go @@ -0,0 +1,22 @@ +package common + +type Component interface { + Initialize(birdbot ComponentManager) error +} + +type ComponentManager interface { + OnReady(func() error) error + + OnNotify(func(string) error) error + + // Event events + OnEventCreate(func(Event) error) error + OnEventDelete(func(Event) error) error + OnEventUpdate(func(Event) error) error + OnEventComplete(func(Event) error) error + + RegisterGameModule(ID string, plugin GameModule) error + + CreateEvent(event Event) error + Notify(message string) error +} diff --git a/common/event.go b/common/event.go new file mode 100644 index 0000000..502feca --- /dev/null +++ b/common/event.go @@ -0,0 +1,18 @@ +package common + +import ( + "time" +) + +type Event struct { + Name string + ID string + Location string + Completed bool + DateTime time.Time + CompleteDateTime time.Time + Description string + ImageURL string + + Organizer User +} diff --git a/common/game_plugin.go b/common/game_plugin.go new file mode 100644 index 0000000..2e9c805 --- /dev/null +++ b/common/game_plugin.go @@ -0,0 +1,6 @@ +package common + +type GameModule interface { + SendMessage(user string, message string) + RecieveMessage(user User, message string) +} diff --git a/common/user.go b/common/user.go new file mode 100644 index 0000000..a19bf06 --- /dev/null +++ b/common/user.go @@ -0,0 +1,18 @@ +package common + +import "fmt" + +type User struct { + ID string + AvatarURL string + DisplayName string +} + +// DiscordMention generated a Discord mention string for the user +func (user *User) DiscordMention() string { + if user == nil { + return "" + } + + return fmt.Sprintf("<@%s>", user.ID) +} diff --git a/core/channel.go b/core/channel.go index 447e346..ea7ee4e 100644 --- a/core/channel.go +++ b/core/channel.go @@ -1,7 +1,43 @@ package core +import ( + "fmt" + "regexp" + "strings" + "time" + + "github.com/yeslayla/birdbot/common" +) + type Channel struct { Name string ID string Verified bool } + +func GenerateEventChannelName(eventName string, location string, dateTime time.Time) string { + month := GetMonthPrefix(dateTime) + day := dateTime.Day() + city := GetCityFromLocation(location) + year := dateTime.Year() + + channel := fmt.Sprint(month, "-", day, city, "-", eventName, "-", year) + channel = strings.ReplaceAll(channel, " ", "-") + channel = strings.ToLower(channel) + + re, _ := regexp.Compile(`[^\w\-]`) + channel = re.ReplaceAllString(channel, "") + + return channel +} + +// GenerateChannel returns a channel object associated with an event +func GenerateChannel(event common.Event) *Channel { + + channelName := GenerateEventChannelName(event.Name, event.Location, event.DateTime) + + return &Channel{ + Name: channelName, + Verified: false, + } +} diff --git a/core/configuration.go b/core/configuration.go index e693505..1d049ff 100644 --- a/core/configuration.go +++ b/core/configuration.go @@ -3,6 +3,7 @@ package core type Config struct { Discord DiscordConfig `yaml:"discord"` Mastodon MastodonConfig `yaml:"mastodon"` + Features Features `yaml:"features"` } type DiscordConfig struct { @@ -21,3 +22,10 @@ type MastodonConfig struct { ClientID string `yaml:"client_id" env:"MASTODON_CLIENT_ID"` ClientSecret string `yaml:"client_secret" env:"MASTODON_CLIENT_SECRET"` } + +type Features struct { + ManageEventChannels bool `yaml:"manage_event_channels" env:"BIRD_EVENT_CHANNELS" env-default:"true"` + AnnounceEvents bool `yaml:"announce_events" env:"BIRD_ANNOUNCE_EVENTS" env-default:"true"` + ReccurringEvents bool `yaml:"recurring_events" env:"BIRD_RECURRING_EVENTS" env-default:"true"` + LoadGamePlugins bool `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS" env-default:"true"` +} diff --git a/core/event.go b/core/event.go deleted file mode 100644 index 702aaf4..0000000 --- a/core/event.go +++ /dev/null @@ -1,99 +0,0 @@ -package core - -import ( - "fmt" - "regexp" - "strings" - "time" -) - -const REMOTE_LOCATION string = "online" - -type Event struct { - Name string - ID string - Location string - Completed bool - DateTime time.Time - CompleteTime time.Time - Description string - Image string - - Organizer *User -} - -// Channel returns a channel object associated with an event -func (event *Event) Channel() *Channel { - - month := event.GetMonthPrefix() - day := event.DateTime.Day() - city := event.GetCityFromLocation() - year := event.DateTime.Year() - - channel := fmt.Sprint(month, "-", day, city, "-", event.Name, "-", year) - channel = strings.ReplaceAll(channel, " ", "-") - channel = strings.ToLower(channel) - - re, _ := regexp.Compile(`[^\w\-]`) - channel = re.ReplaceAllString(channel, "") - - return &Channel{ - Name: channel, - Verified: false, - } -} - -// GetCityFromLocation returns the city name of an event's location -func (event *Event) GetCityFromLocation() string { - - if event.Location == REMOTE_LOCATION { - return fmt.Sprint("-", REMOTE_LOCATION) - } - parts := strings.Split(event.Location, " ") - index := -1 - loc := event.Location - - for i, v := range parts { - part := strings.ToLower(v) - if part == "mi" || part == "michigan" { - index = i - 1 - if index < 0 { - return "" - } - if index > 0 && parts[index] == "," { - index -= 1 - } - - if index > 1 && strings.Contains(parts[index-2], ",") { - loc = fmt.Sprintf("%s-%s", parts[index-1], parts[index]) - break - } - - loc = parts[index] - break - } - } - - return fmt.Sprint("-", loc) -} - -// GetMonthPrefix returns a month in short form -func (event *Event) GetMonthPrefix() string { - month := event.DateTime.Month() - data := map[time.Month]string{ - time.January: "jan", - time.February: "feb", - time.March: "march", - time.April: "april", - time.May: "may", - time.June: "june", - time.July: "july", - time.August: "aug", - time.September: "sept", - time.October: "oct", - time.November: "nov", - time.December: "dec", - } - - return data[month] -} diff --git a/core/event_test.go b/core/event_test.go deleted file mode 100644 index 410a1ba..0000000 --- a/core/event_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package core - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestGetChannelName(t *testing.T) { - assert := assert.New(t) - - // Test Valid Address - event := Event{ - Name: "Hello World", - Location: "1234 Place Rd, Ann Arbor, MI 00000", - DateTime: time.Date(2022, time.January, 5, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan-5-ann-arbor-hello-world-2022", event.Channel().Name) - - // Test Unparsable Location - // lmanley: Note it'd be nice to expand support for this - event = Event{ - Name: "Hello World", - Location: "Michigan Theater, Ann Arbor", - DateTime: time.Date(2022, time.January, 5, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan-5-hello-world-2022", event.Channel().Name) - - // Test Short Location - event = Event{ - Name: "Hello World", - Location: "Monroe, MI", - DateTime: time.Date(2022, time.January, 5, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan-5-monroe-hello-world-2022", event.Channel().Name) - - // Test Short Location - event = Event{ - Name: "Hello World", - Location: "Monroe St, Monroe , MI", - DateTime: time.Date(2022, time.January, 5, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan-5-monroe-hello-world-2022", event.Channel().Name) - - // Test Remote Event - event = Event{ - Name: "Hello World", - Location: REMOTE_LOCATION, - DateTime: time.Date(2022, time.January, 5, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan-5-online-hello-world-2022", event.Channel().Name) -} - -func TestMonthPrefix(t *testing.T) { - assert := assert.New(t) - - event := Event{ - DateTime: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), - } - assert.Equal("jan", event.GetMonthPrefix()) -} diff --git a/core/location.go b/core/location.go new file mode 100644 index 0000000..1c4f5e8 --- /dev/null +++ b/core/location.go @@ -0,0 +1,42 @@ +package core + +import ( + "fmt" + "strings" +) + +const REMOTE_LOCATION string = "online" + +// GetCityFromLocation returns the city name of an event's location +func GetCityFromLocation(location string) string { + + if location == REMOTE_LOCATION { + return fmt.Sprint("-", REMOTE_LOCATION) + } + parts := strings.Split(location, " ") + index := -1 + loc := location + + for i, v := range parts { + part := strings.ToLower(v) + if part == "mi" || part == "michigan" { + index = i - 1 + if index < 0 { + return "" + } + if index > 0 && parts[index] == "," { + index -= 1 + } + + if index > 1 && strings.Contains(parts[index-2], ",") { + loc = fmt.Sprintf("%s-%s", parts[index-1], parts[index]) + break + } + + loc = parts[index] + break + } + } + + return fmt.Sprint("-", loc) +} diff --git a/core/time.go b/core/time.go new file mode 100644 index 0000000..d177169 --- /dev/null +++ b/core/time.go @@ -0,0 +1,24 @@ +package core + +import "time" + +// GetMonthPrefix returns a month in short form +func GetMonthPrefix(dateTime time.Time) string { + month := dateTime.Month() + data := map[time.Month]string{ + time.January: "jan", + time.February: "feb", + time.March: "march", + time.April: "april", + time.May: "may", + time.June: "june", + time.July: "july", + time.August: "aug", + time.September: "sept", + time.October: "oct", + time.November: "nov", + time.December: "dec", + } + + return data[month] +} diff --git a/core/user.go b/core/user.go deleted file mode 100644 index 29a98b0..0000000 --- a/core/user.go +++ /dev/null @@ -1,16 +0,0 @@ -package core - -import "fmt" - -type User struct { - ID string -} - -// Mention generated a Discord mention string for the user -func (user *User) Mention() string { - if user == nil { - return "" - } - - return fmt.Sprintf("<@%s>", user.ID) -} diff --git a/core/user_test.go b/core/user_test.go deleted file mode 100644 index 6171281..0000000 --- a/core/user_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package core - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestUserMention(t *testing.T) { - assert := assert.New(t) - - // Create user object - user := &User{ - ID: "sample_id", - } - - assert.Equal("<@sample_id>", user.Mention()) - - // Test null user - var nullUser *User = nil - assert.NotEmpty(nullUser.Mention()) - -} diff --git a/discord/discord.go b/discord/discord.go index 1653962..8dddb48 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -8,7 +8,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/stretchr/testify/mock" - "github.com/yeslayla/birdbot/core" + "github.com/yeslayla/birdbot/common" ) type Discord struct { @@ -64,7 +64,7 @@ func (discord *Discord) OnReady(handler func(*Discord)) { } // OnEventCreate registers a handler when a guild scheduled event is created -func (discord *Discord) OnEventCreate(handler func(*Discord, *core.Event)) { +func (discord *Discord) OnEventCreate(handler func(*Discord, common.Event)) { discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.GuildScheduledEventCreate) { if r.GuildID != discord.guildID { return @@ -75,7 +75,7 @@ func (discord *Discord) OnEventCreate(handler func(*Discord, *core.Event)) { } // OnEventDelete registers a handler when a guild scheduled event is deleted -func (discord *Discord) OnEventDelete(handler func(*Discord, *core.Event)) { +func (discord *Discord) OnEventDelete(handler func(*Discord, common.Event)) { discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.GuildScheduledEventDelete) { if r.GuildID != discord.guildID { return @@ -86,7 +86,7 @@ func (discord *Discord) OnEventDelete(handler func(*Discord, *core.Event)) { } // OnEventUpdate registers a handler when a guild scheduled event is updated -func (discord *Discord) OnEventUpdate(handler func(*Discord, *core.Event)) { +func (discord *Discord) OnEventUpdate(handler func(*Discord, common.Event)) { discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.GuildScheduledEventUpdate) { if r.GuildID != discord.guildID { return diff --git a/discord/event.go b/discord/event.go index 5fd5af6..76e4927 100644 --- a/discord/event.go +++ b/discord/event.go @@ -4,27 +4,28 @@ import ( "time" "github.com/bwmarrin/discordgo" + "github.com/yeslayla/birdbot/common" "github.com/yeslayla/birdbot/core" ) // NewEvent converts a discordgo.GuildScheduledEvent to birdbot event -func NewEvent(guildEvent *discordgo.GuildScheduledEvent) *core.Event { - event := &core.Event{ +func NewEvent(guildEvent *discordgo.GuildScheduledEvent) common.Event { + event := common.Event{ Name: guildEvent.Name, Description: guildEvent.Description, ID: guildEvent.ID, - Organizer: &core.User{ + Organizer: common.User{ ID: guildEvent.CreatorID, }, DateTime: guildEvent.ScheduledStartTime, - Image: guildEvent.Image, + ImageURL: guildEvent.Image, } if guildEvent.ScheduledEndTime != nil { - event.CompleteTime = *guildEvent.ScheduledEndTime + event.CompleteDateTime = *guildEvent.ScheduledEndTime } else { year, month, day := guildEvent.ScheduledStartTime.Date() - event.CompleteTime = time.Date(year, month, day, 0, 0, 0, 0, guildEvent.ScheduledStartTime.Location()) + event.CompleteDateTime = time.Date(year, month, day, 0, 0, 0, 0, guildEvent.ScheduledStartTime.Location()) } event.Completed = guildEvent.Status == discordgo.GuildScheduledEventStatusCompleted @@ -38,14 +39,14 @@ func NewEvent(guildEvent *discordgo.GuildScheduledEvent) *core.Event { return event } -func (discord *Discord) CreateEvent(event *core.Event) error { +func (discord *Discord) CreateEvent(event common.Event) error { params := &discordgo.GuildScheduledEventParams{ Name: event.Name, Description: event.Description, ScheduledStartTime: &event.DateTime, - ScheduledEndTime: &event.CompleteTime, - Image: event.Image, + ScheduledEndTime: &event.CompleteDateTime, + Image: event.ImageURL, EntityType: discordgo.GuildScheduledEventEntityTypeExternal, PrivacyLevel: discordgo.GuildScheduledEventPrivacyLevelGuildOnly, } diff --git a/discord/user.go b/discord/user.go index 189a33a..6c096a8 100644 --- a/discord/user.go +++ b/discord/user.go @@ -4,17 +4,19 @@ import ( "log" "github.com/bwmarrin/discordgo" - "github.com/yeslayla/birdbot/core" + "github.com/yeslayla/birdbot/common" ) // NewUser creates a new user object from a discordgo.User object -func NewUser(user *discordgo.User) *core.User { +func NewUser(user *discordgo.User) common.User { if user == nil { log.Print("Cannot user object, user is nil!") - return nil + return common.User{ + ID: "-1", + } } - return &core.User{ + return common.User{ ID: user.ID, } } diff --git a/events/announce_events.go b/events/announce_events.go new file mode 100644 index 0000000..09c10f6 --- /dev/null +++ b/events/announce_events.go @@ -0,0 +1,56 @@ +package events + +import ( + "fmt" + + "github.com/yeslayla/birdbot/common" + "github.com/yeslayla/birdbot/mastodon" +) + +type AnnounceEventsComponent struct { + bot common.ComponentManager + mastodon *mastodon.Mastodon + guildID string +} + +func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) *AnnounceEventsComponent { + return &AnnounceEventsComponent{ + mastodon: mastodon, + guildID: guildID, + } +} + +func (c *AnnounceEventsComponent) Initialize(birdbot common.ComponentManager) error { + c.bot = birdbot + + _ = birdbot.OnEventCreate(c.OnEventCreate) + + return nil +} + +func (c *AnnounceEventsComponent) OnEventCreate(e common.Event) error { + 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)) + + if c.mastodon != nil { + err := c.mastodon.Toot(fmt.Sprintf("A new event has been organized '%s': %s", e.Name, eventURL)) + if err != nil { + fmt.Println("Failed to send Mastodon Toot:", err) + } + } + + return nil +} + +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())) + + if c.mastodon != nil { + err := c.mastodon.Toot(fmt.Sprintf("'%s' cancelled on %s, %d!", e.Name, e.DateTime.Month().String(), e.DateTime.Day())) + if err != nil { + fmt.Println("Failed to send Mastodon Toot:", err) + } + } + + return nil +} diff --git a/events/manage_event_channels.go b/events/manage_event_channels.go new file mode 100644 index 0000000..67893b2 --- /dev/null +++ b/events/manage_event_channels.go @@ -0,0 +1,81 @@ +package events + +import ( + "log" + + "github.com/yeslayla/birdbot/common" + "github.com/yeslayla/birdbot/core" + "github.com/yeslayla/birdbot/discord" +) + +type ManageEventChannelsComponent struct { + session *discord.Discord + categoryID string +} + +func NewManageEventChannelsComponent(categoryID string, session *discord.Discord) *ManageEventChannelsComponent { + return &ManageEventChannelsComponent{ + session: session, + categoryID: categoryID, + } +} + +func (c *ManageEventChannelsComponent) Initialize(birdbot common.ComponentManager) error { + _ = birdbot.OnEventCreate(c.OnEventCreate) + _ = birdbot.OnEventComplete(c.OnEventComplete) + _ = birdbot.OnEventDelete(c.OnEventDelete) + + return nil +} + +func (c *ManageEventChannelsComponent) OnEventCreate(e common.Event) error { + channel, err := c.session.NewChannelFromName(core.GenerateChannel(e).Name) + if err != nil { + log.Print("Failed to create channel for event: ", err) + } + + if c.categoryID != "" { + err = c.session.MoveChannelToCategory(channel, c.categoryID) + if err != nil { + log.Printf("Failed to move channel to events category '%s': %v", channel.Name, err) + } + } + return nil +} + +func (c *ManageEventChannelsComponent) OnEventDelete(e common.Event) error { + _, err := c.session.DeleteChannel(core.GenerateChannel(e)) + if err != nil { + log.Print("Failed to create channel for event: ", err) + } + return nil +} + +func (c *ManageEventChannelsComponent) OnEventComplete(e common.Event) error { + channel := core.GenerateChannel(e) + + if c.categoryID != "" { + + if err := c.session.MoveChannelToCategory(channel, c.categoryID); err != nil { + log.Print("Failed to move channel to archive category: ", err) + } + + if err := c.session.ArchiveChannel(channel); err != nil { + log.Print("Failed to archive channel: ", err) + } + + log.Printf("Archived channel: '%s'", channel.Name) + + } else { + + // Delete Channel + _, err := c.session.DeleteChannel(channel) + if err != nil { + log.Print("Failed to delete channel: ", err) + } + + log.Printf("Deleted channel: '%s'", channel.Name) + } + + return nil +} diff --git a/events/recurring_events.go b/events/recurring_events.go new file mode 100644 index 0000000..8fc44fc --- /dev/null +++ b/events/recurring_events.go @@ -0,0 +1,40 @@ +package events + +import ( + "log" + "strings" + + "github.com/yeslayla/birdbot/common" + "github.com/yeslayla/birdbot/discord" +) + +type RecurringEventsComponent struct { + session *discord.Discord +} + +func NewRecurringEventsComponent() *RecurringEventsComponent { + return &RecurringEventsComponent{} +} + +func (c *RecurringEventsComponent) Initialize(birdbot common.ComponentManager) error { + _ = birdbot.OnEventComplete(c.OnEventComplete) + + return nil +} + +func (c *RecurringEventsComponent) OnEventComplete(e common.Event) error { + + if strings.Contains(strings.ToLower(e.Description), "recurring weekly") { + startTime := e.DateTime.AddDate(0, 0, 7) + finishTime := e.CompleteDateTime.AddDate(0, 0, 7) + nextEvent := e + nextEvent.DateTime = startTime + nextEvent.CompleteDateTime = finishTime + + if err := c.session.CreateEvent(nextEvent); err != nil { + log.Print("Failed to create recurring event: ", err) + } + } + + return nil +} diff --git a/main.go b/main.go index 0335a4f..8a5c2b4 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,17 @@ package main import ( + "errors" "flag" "fmt" "log" "os" "path" + "github.com/ilyakaznacheev/cleanenv" "github.com/yeslayla/birdbot/app" + "github.com/yeslayla/birdbot/core" + "github.com/yeslayla/birdbot/events" ) func main() { @@ -27,12 +31,41 @@ func main() { return } + log.Printf("Using config: %s", config_file) + cfg := &core.Config{} + + _, err := os.Stat(config_file) + if errors.Is(err, os.ErrNotExist) { + log.Printf("Config file not found: '%s'", config_file) + err := cleanenv.ReadEnv(cfg) + if err != nil { + log.Fatal(err) + } + } else { + err := cleanenv.ReadConfig(config_file, cfg) + if err != nil { + log.Fatal(err) + } + } + bot := app.NewBot() - if err := bot.Initialize(config_file); err != nil { + if err := bot.Initialize(cfg); err != nil { log.Fatal("Failed to initialize: ", err) } + loader := app.NewComponentLoader(bot) + + if cfg.Features.AnnounceEvents { + loader.LoadComponent(events.NewAnnounceEventsComponent(bot.Mastodon, cfg.Discord.NotificationChannel)) + } + if cfg.Features.ManageEventChannels { + loader.LoadComponent(events.NewManageEventChannelsComponent(cfg.Discord.EventCategory, bot.Session)) + } + if cfg.Features.ReccurringEvents { + loader.LoadComponent(events.NewRecurringEventsComponent()) + } + if err := bot.Run(); err != nil { log.Fatal(err) }