Discord Components & Role Selection (#5)
This commit is contained in:
parent
5e6a433b92
commit
e1038a15cd
@ -9,6 +9,7 @@ Bird Bot is a discord bot for managing and organizing events for a small discord
|
||||
- Delete text channels after events
|
||||
- Archive text channels after events
|
||||
- Create recurring weekly events
|
||||
- Role selection
|
||||
|
||||
## Usage
|
||||
|
||||
@ -16,7 +17,7 @@ To get up and running, install go and you can run `make run`!
|
||||
|
||||
## Using Docker
|
||||
|
||||
The container is expecting the config file to be located at `/etc/birdbot/birdbot.yaml`. The easily solution here is to mount the conifg with a volume.
|
||||
The container is expecting the config file to be located at `/etc/birdbot/birdbot.yaml`. The easily solution here is to mount the config with a volume.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
|
@ -31,7 +31,7 @@ type Bot struct {
|
||||
onEventUpdatedHandlers [](func(common.Event) error)
|
||||
onEventCompletedHandlers [](func(common.Event) error)
|
||||
|
||||
gameModules []common.GameModule
|
||||
gameModules []common.ChatSyncModule
|
||||
}
|
||||
|
||||
// Initalize creates the discord session and registers handlers
|
||||
@ -62,6 +62,11 @@ func (app *Bot) Initialize(cfg *core.Config) error {
|
||||
app.Session.OnEventDelete(app.onEventDelete)
|
||||
app.Session.OnEventUpdate(app.onEventUpdate)
|
||||
|
||||
btn := app.Session.NewButton("test", "Click Me")
|
||||
btn.OnClick(func(user common.User) {
|
||||
print("clicked")
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ func NewComponentLoader(bot *Bot) *ComponentLoader {
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *ComponentLoader) LoadComponent(component common.Component) {
|
||||
func (loader *ComponentLoader) LoadComponent(component common.Module) {
|
||||
if err := component.Initialize(loader); err != nil {
|
||||
log.Print("Failed to load component: ", err)
|
||||
}
|
||||
@ -50,7 +50,7 @@ func (loader *ComponentLoader) OnEventComplete(handler func(common.Event) error)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (loader *ComponentLoader) RegisterGameModule(ID string, plugin common.GameModule) error {
|
||||
func (loader *ComponentLoader) RegisterChatSyncModule(ID string, plugin common.ChatSyncModule) error {
|
||||
return fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// LoadPlugin loads a plugin and returns its component if successful
|
||||
func LoadPlugin(pluginPath string) common.Component {
|
||||
func LoadPlugin(pluginPath string) common.Module {
|
||||
|
||||
plug, err := plugin.Open(pluginPath)
|
||||
if err != nil {
|
||||
@ -25,8 +25,8 @@ func LoadPlugin(pluginPath string) common.Component {
|
||||
}
|
||||
|
||||
// Validate component type
|
||||
var component common.Component
|
||||
component, ok := sym.(common.Component)
|
||||
var component common.Module
|
||||
component, ok := sym.(common.Module)
|
||||
if !ok {
|
||||
log.Printf("Failed to load plugin '%s': Plugin component does not properly implement interface!", pluginPath)
|
||||
}
|
||||
@ -35,15 +35,15 @@ func LoadPlugin(pluginPath string) common.Component {
|
||||
}
|
||||
|
||||
// LoadPlugins loads all plugins and componenets in a directory
|
||||
func LoadPlugins(directory string) []common.Component {
|
||||
func LoadPlugins(directory string) []common.Module {
|
||||
|
||||
paths, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load plugins: %s", err)
|
||||
return []common.Component{}
|
||||
return []common.Module{}
|
||||
}
|
||||
|
||||
var components []common.Component = make([]common.Component, 0)
|
||||
var components []common.Module = make([]common.Module, 0)
|
||||
for _, path := range paths {
|
||||
if path.IsDir() {
|
||||
continue
|
||||
|
@ -1,6 +1,6 @@
|
||||
package common
|
||||
|
||||
type GameModule interface {
|
||||
type ChatSyncModule interface {
|
||||
SendMessage(user string, message string)
|
||||
RecieveMessage(user User, message string)
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package common
|
||||
|
||||
type Component interface {
|
||||
Initialize(birdbot ComponentManager) error
|
||||
type Module interface {
|
||||
Initialize(birdbot ModuleManager) error
|
||||
}
|
||||
|
||||
// ComponentManager is the primary way for a component to interact with BirdBot
|
||||
// ModuleManager is the primary way for a module to interact with BirdBot
|
||||
// by listening to events and committing actions
|
||||
type ComponentManager interface {
|
||||
type ModuleManager interface {
|
||||
OnReady(func() error) error
|
||||
|
||||
OnNotify(func(string) error) error
|
||||
@ -21,5 +21,5 @@ type ComponentManager interface {
|
||||
CreateEvent(event Event) error
|
||||
Notify(message string) error
|
||||
|
||||
RegisterGameModule(ID string, plugin GameModule) error
|
||||
RegisterChatSyncModule(ID string, plugin ChatSyncModule) error
|
||||
}
|
34
core/color.go
Normal file
34
core/color.go
Normal file
@ -0,0 +1,34 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IntToColor converts a hex int to a Go Color
|
||||
func IntToColor(hex int) color.Color {
|
||||
r := uint8(hex >> 16 & 0xFF)
|
||||
g := uint8(hex >> 8 & 0xFF)
|
||||
b := uint8(hex & 0xFF)
|
||||
return color.RGBA{r, g, b, 255}
|
||||
}
|
||||
|
||||
// ColorToInt converts a Go Color to a hex int
|
||||
func ColorToInt(c color.Color) int {
|
||||
rgba := color.RGBAModel.Convert(c).(color.RGBA)
|
||||
hex := int(rgba.R)<<16 | int(rgba.G)<<8 | int(rgba.B)
|
||||
return hex
|
||||
}
|
||||
|
||||
// HexToColor coverts hex string to color
|
||||
func HexToColor(s string) (color.Color, error) {
|
||||
s = strings.ReplaceAll(s, "#", "")
|
||||
|
||||
hex, err := strconv.ParseInt(s, 16, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return IntToColor(int(hex)), nil
|
||||
|
||||
}
|
75
core/color_test.go
Normal file
75
core/color_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIntToColor(t *testing.T) {
|
||||
|
||||
// green
|
||||
hex := 0x00FF00
|
||||
expected := color.RGBA{0, 255, 0, 255}
|
||||
got := IntToColor(hex)
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
// black
|
||||
hex = 0x000000
|
||||
expected = color.RGBA{0, 0, 0, 255}
|
||||
got = IntToColor(hex)
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
// white
|
||||
hex = 0xFFFFFF
|
||||
expected = color.RGBA{255, 255, 255, 255}
|
||||
got = IntToColor(hex)
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
}
|
||||
|
||||
func TestColorToHex(t *testing.T) {
|
||||
|
||||
// magenta
|
||||
col := color.RGBA{255, 0, 255, 255}
|
||||
hex := 0xFF00FF
|
||||
require.Equal(t, hex, ColorToInt(col))
|
||||
|
||||
// black
|
||||
col = color.RGBA{0, 0, 0, 255}
|
||||
hex = 0x000000
|
||||
require.Equal(t, hex, ColorToInt(col))
|
||||
|
||||
// white
|
||||
col = color.RGBA{255, 255, 255, 255}
|
||||
hex = 0xFFFFFF
|
||||
require.Equal(t, hex, ColorToInt(col))
|
||||
}
|
||||
|
||||
func TestHexToColor(t *testing.T) {
|
||||
|
||||
// magenta
|
||||
hex := "#ff00ff"
|
||||
col := color.RGBA{255, 0, 255, 255}
|
||||
|
||||
c, err := HexToColor(hex)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, col, c)
|
||||
|
||||
// black
|
||||
hex = "000000"
|
||||
col = color.RGBA{0, 0, 0, 255}
|
||||
|
||||
c, err = HexToColor(hex)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, col, c)
|
||||
|
||||
// white
|
||||
hex = "ffffff"
|
||||
col = color.RGBA{255, 255, 255, 255}
|
||||
|
||||
c, err = HexToColor(hex)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, col, c)
|
||||
}
|
@ -17,6 +17,21 @@ type DiscordConfig struct {
|
||||
EventCategory string `yaml:"event_category" env:"DISCORD_EVENT_CATEGORY"`
|
||||
ArchiveCategory string `yaml:"archive_category" env:"DISCORD_ARCHIVE_CATEGORY"`
|
||||
NotificationChannel string `yaml:"notification_channel" env:"DISCORD_NOTIFICATION_CHANNEL"`
|
||||
|
||||
RoleSelections []RoleSelectionConfig `yaml:"role_selection"`
|
||||
}
|
||||
|
||||
type RoleSelectionConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
|
||||
SelectionChannel string `yaml:"discord_channel"`
|
||||
Roles []RoleConfig `yaml:"roles"`
|
||||
}
|
||||
|
||||
type RoleConfig struct {
|
||||
RoleName string `yaml:"name"`
|
||||
Color string `yaml:"color"`
|
||||
}
|
||||
|
||||
// MastodonConfig contains mastodon specific configuration
|
||||
@ -33,6 +48,7 @@ type Features struct {
|
||||
ManageEventChannels Feature `yaml:"manage_event_channels" env:"BIRD_EVENT_CHANNELS"`
|
||||
AnnounceEvents Feature `yaml:"announce_events" env:"BIRD_ANNOUNCE_EVENTS"`
|
||||
ReccurringEvents Feature `yaml:"recurring_events" env:"BIRD_RECURRING_EVENTS"`
|
||||
RoleSelection Feature `yaml:"role_selection" env:"BIRD_ROLE_SELECTION"`
|
||||
LoadGamePlugins Feature `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS"`
|
||||
}
|
||||
|
||||
|
@ -4,3 +4,8 @@ package core
|
||||
func Bool(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int returns a pointer to an int
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
29
discord/component.go
Normal file
29
discord/component.go
Normal file
@ -0,0 +1,29 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// Component is an object that can be formatted as a discord component
|
||||
type Component interface {
|
||||
toMessageComponent() discordgo.MessageComponent
|
||||
}
|
||||
|
||||
// CreateMessageComponent creates a discord component
|
||||
func (discord *Discord) CreateMessageComponent(channelID string, content string, components []Component) {
|
||||
|
||||
dComponents := make([]discordgo.MessageComponent, len(components))
|
||||
for i, v := range components {
|
||||
dComponents[i] = v.toMessageComponent()
|
||||
}
|
||||
|
||||
if _, err := discord.session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
|
||||
Components: dComponents,
|
||||
Content: content,
|
||||
}); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
}
|
38
discord/component_action_row.go
Normal file
38
discord/component_action_row.go
Normal file
@ -0,0 +1,38 @@
|
||||
package discord
|
||||
|
||||
import "github.com/bwmarrin/discordgo"
|
||||
|
||||
type ActionRow struct {
|
||||
components []Component
|
||||
}
|
||||
|
||||
// NewActionRow creates an empty action row component
|
||||
func (discord *Discord) NewActionRow() *ActionRow {
|
||||
return &ActionRow{
|
||||
components: []Component{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewActionRowWith creates an action row with a set of components
|
||||
func (discord *Discord) NewActionRowWith(comp []Component) *ActionRow {
|
||||
return &ActionRow{
|
||||
components: comp,
|
||||
}
|
||||
}
|
||||
|
||||
// AddComponent adds a component to the action row
|
||||
func (row *ActionRow) AddComponent(comp Component) {
|
||||
row.components = append(row.components, comp)
|
||||
}
|
||||
|
||||
func (row *ActionRow) toMessageComponent() discordgo.MessageComponent {
|
||||
|
||||
comps := make([]discordgo.MessageComponent, len(row.components))
|
||||
for i, v := range row.components {
|
||||
comps[i] = v.toMessageComponent()
|
||||
}
|
||||
|
||||
return discordgo.ActionsRow{
|
||||
Components: comps,
|
||||
}
|
||||
}
|
45
discord/component_button.go
Normal file
45
discord/component_button.go
Normal file
@ -0,0 +1,45 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/yeslayla/birdbot/common"
|
||||
)
|
||||
|
||||
type Button struct {
|
||||
Label string
|
||||
ID string
|
||||
|
||||
discord *Discord
|
||||
}
|
||||
|
||||
// NewButton creates a new button component
|
||||
func (discord *Discord) NewButton(id string, label string) *Button {
|
||||
return &Button{
|
||||
discord: discord,
|
||||
ID: id,
|
||||
Label: label,
|
||||
}
|
||||
}
|
||||
|
||||
// OnClick registers an event when the button is clicked
|
||||
func (button *Button) OnClick(action func(user common.User)) {
|
||||
button.discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.InteractionCreate) {
|
||||
if r.MessageComponentData().CustomID == button.ID {
|
||||
|
||||
action(NewUser(r.Member.User))
|
||||
|
||||
s.InteractionRespond(r.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (button *Button) toMessageComponent() discordgo.MessageComponent {
|
||||
return discordgo.Button{
|
||||
Label: button.Label,
|
||||
CustomID: button.ID,
|
||||
Style: discordgo.PrimaryButton,
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ func NewEvent(guildEvent *discordgo.GuildScheduledEvent) common.Event {
|
||||
return event
|
||||
}
|
||||
|
||||
// CreateEvent creates a new discord event
|
||||
func (discord *Discord) CreateEvent(event common.Event) error {
|
||||
|
||||
params := &discordgo.GuildScheduledEventParams{
|
||||
|
72
discord/role.go
Normal file
72
discord/role.go
Normal file
@ -0,0 +1,72 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/yeslayla/birdbot/core"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
discord *Discord
|
||||
ID string
|
||||
|
||||
Name string
|
||||
Color color.Color
|
||||
}
|
||||
|
||||
// GetRole returns a role that exists on Discord
|
||||
func (discord *Discord) GetRole(name string) *Role {
|
||||
|
||||
roles, err := discord.session.GuildRoles(discord.guildID)
|
||||
if err != nil {
|
||||
log.Printf("Error occured listing roles: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.Managed {
|
||||
continue
|
||||
}
|
||||
if role.Name == name {
|
||||
|
||||
return &Role{
|
||||
Name: role.Name,
|
||||
Color: core.IntToColor(role.Color),
|
||||
discord: discord,
|
||||
ID: role.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRoleAndCreate gets a role and creates it if it doesn't exist
|
||||
func (discord *Discord) GetRoleAndCreate(name string) *Role {
|
||||
role := discord.GetRole(name)
|
||||
if role != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
if _, err := discord.session.GuildRoleCreate(discord.guildID, &discordgo.RoleParams{
|
||||
Name: name,
|
||||
Color: core.Int(0),
|
||||
}); err != nil {
|
||||
log.Printf("Failed to create role: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return discord.GetRole(name)
|
||||
}
|
||||
|
||||
// Save updates the role on Discord
|
||||
func (role *Role) Save() {
|
||||
if _, err := role.discord.session.GuildRoleEdit(role.discord.guildID, role.ID, &discordgo.RoleParams{
|
||||
Name: role.Name,
|
||||
Color: core.Int(core.ColorToInt(role.Color)),
|
||||
}); err != nil {
|
||||
log.Printf("Failed to save role: %s", err)
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
// NewUser creates a new user object from a discordgo.User object
|
||||
func NewUser(user *discordgo.User) common.User {
|
||||
|
||||
if user == nil {
|
||||
log.Print("Cannot user object, user is nil!")
|
||||
return common.User{
|
||||
@ -20,3 +21,38 @@ func NewUser(user *discordgo.User) common.User {
|
||||
ID: user.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// AssignRole adds a role to a user
|
||||
func (discord *Discord) AssignRole(user common.User, role *Role) error {
|
||||
return discord.session.GuildMemberRoleAdd(discord.guildID, user.ID, role.ID)
|
||||
}
|
||||
|
||||
// UnassignRole removes a role from a user
|
||||
func (discord *Discord) UnassignRole(user common.User, role *Role) error {
|
||||
return discord.session.GuildMemberRoleRemove(discord.guildID, user.ID, role.ID)
|
||||
}
|
||||
|
||||
// HasRole returns true when a user has a given role
|
||||
func (discord *Discord) HasRole(user common.User, role *Role) bool {
|
||||
return discord.HasAtLeastOneRole(user, []*Role{role})
|
||||
}
|
||||
|
||||
// HasAtLeastOneRole returns true when a user has at one role from a given array
|
||||
func (discord *Discord) HasAtLeastOneRole(user common.User, roles []*Role) bool {
|
||||
|
||||
member, err := discord.session.GuildMember(discord.guildID, user.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get member: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range member.Roles {
|
||||
for _, targetRole := range roles {
|
||||
if v == targetRole.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package components
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
type announceEventsComponent struct {
|
||||
bot common.ComponentManager
|
||||
bot common.ModuleManager
|
||||
mastodon *mastodon.Mastodon
|
||||
guildID string
|
||||
}
|
||||
|
||||
// NewAnnounceEventsComponent creates a new component
|
||||
func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) common.Component {
|
||||
func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) common.Module {
|
||||
return &announceEventsComponent{
|
||||
mastodon: mastodon,
|
||||
guildID: guildID,
|
||||
@ -22,7 +22,7 @@ func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) com
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *announceEventsComponent) Initialize(birdbot common.ComponentManager) error {
|
||||
func (c *announceEventsComponent) Initialize(birdbot common.ModuleManager) error {
|
||||
c.bot = birdbot
|
||||
|
||||
_ = birdbot.OnEventCreate(c.OnEventCreate)
|
@ -1,4 +1,4 @@
|
||||
package components
|
||||
package events
|
||||
|
||||
import (
|
||||
"log"
|
||||
@ -15,7 +15,7 @@ type manageEventChannelsComponent struct {
|
||||
}
|
||||
|
||||
// NewManageEventChannelsComponent creates a new component
|
||||
func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string, session *discord.Discord) common.Component {
|
||||
func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string, session *discord.Discord) common.Module {
|
||||
return &manageEventChannelsComponent{
|
||||
session: session,
|
||||
categoryID: categoryID,
|
||||
@ -24,7 +24,7 @@ func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *manageEventChannelsComponent) Initialize(birdbot common.ComponentManager) error {
|
||||
func (c *manageEventChannelsComponent) Initialize(birdbot common.ModuleManager) error {
|
||||
_ = birdbot.OnEventCreate(c.OnEventCreate)
|
||||
_ = birdbot.OnEventComplete(c.OnEventComplete)
|
||||
_ = birdbot.OnEventDelete(c.OnEventDelete)
|
@ -1,4 +1,4 @@
|
||||
package components
|
||||
package events
|
||||
|
||||
import (
|
||||
"log"
|
||||
@ -13,12 +13,12 @@ type recurringEventsComponent struct {
|
||||
}
|
||||
|
||||
// NewRecurringEventsComponent creates a new component instance
|
||||
func NewRecurringEventsComponent() common.Component {
|
||||
func NewRecurringEventsComponent() common.Module {
|
||||
return &recurringEventsComponent{}
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *recurringEventsComponent) Initialize(birdbot common.ComponentManager) error {
|
||||
func (c *recurringEventsComponent) Initialize(birdbot common.ModuleManager) error {
|
||||
_ = birdbot.OnEventComplete(c.OnEventComplete)
|
||||
|
||||
return nil
|
14
main.go
14
main.go
@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
"github.com/yeslayla/birdbot/app"
|
||||
"github.com/yeslayla/birdbot/components"
|
||||
"github.com/yeslayla/birdbot/core"
|
||||
"github.com/yeslayla/birdbot/modules"
|
||||
)
|
||||
|
||||
const PluginsDirectory = "./plugins"
|
||||
@ -59,13 +59,19 @@ func main() {
|
||||
loader := app.NewComponentLoader(bot)
|
||||
|
||||
if cfg.Features.AnnounceEvents.IsEnabledByDefault() {
|
||||
loader.LoadComponent(components.NewAnnounceEventsComponent(bot.Mastodon, cfg.Discord.NotificationChannel))
|
||||
loader.LoadComponent(modules.NewAnnounceEventsComponent(bot.Mastodon, cfg.Discord.NotificationChannel))
|
||||
}
|
||||
if cfg.Features.ManageEventChannels.IsEnabledByDefault() {
|
||||
loader.LoadComponent(components.NewManageEventChannelsComponent(cfg.Discord.EventCategory, cfg.Discord.ArchiveCategory, bot.Session))
|
||||
loader.LoadComponent(modules.NewManageEventChannelsComponent(cfg.Discord.EventCategory, cfg.Discord.ArchiveCategory, bot.Session))
|
||||
}
|
||||
if cfg.Features.ReccurringEvents.IsEnabledByDefault() {
|
||||
loader.LoadComponent(components.NewRecurringEventsComponent())
|
||||
loader.LoadComponent(modules.NewRecurringEventsComponent())
|
||||
}
|
||||
|
||||
if cfg.Features.RoleSelection.IsEnabledByDefault() {
|
||||
for _, v := range cfg.Discord.RoleSelections {
|
||||
loader.LoadComponent(modules.NewRoleSelectionComponent(bot.Session, v))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(PluginsDirectory); !os.IsNotExist(err) {
|
||||
|
61
modules/announce_events.go
Normal file
61
modules/announce_events.go
Normal file
@ -0,0 +1,61 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yeslayla/birdbot/common"
|
||||
"github.com/yeslayla/birdbot/mastodon"
|
||||
)
|
||||
|
||||
type announceEventsModule struct {
|
||||
bot common.ModuleManager
|
||||
mastodon *mastodon.Mastodon
|
||||
guildID string
|
||||
}
|
||||
|
||||
// NewAnnounceEventsComponent creates a new component
|
||||
func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) common.Module {
|
||||
return &announceEventsModule{
|
||||
mastodon: mastodon,
|
||||
guildID: guildID,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *announceEventsModule) Initialize(birdbot common.ModuleManager) error {
|
||||
c.bot = birdbot
|
||||
|
||||
_ = birdbot.OnEventCreate(c.OnEventCreate)
|
||||
_ = birdbot.OnEventDelete(c.OnEventDelete)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnEventCreate notifies about the event creation to given providers
|
||||
func (c *announceEventsModule) 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))
|
||||
|
||||
// Toot an announcement if Mastodon is configured
|
||||
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 *announceEventsModule) 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
|
||||
}
|
89
modules/manage_event_channels.go
Normal file
89
modules/manage_event_channels.go
Normal file
@ -0,0 +1,89 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/yeslayla/birdbot/common"
|
||||
"github.com/yeslayla/birdbot/core"
|
||||
"github.com/yeslayla/birdbot/discord"
|
||||
)
|
||||
|
||||
type manageEventChannelsModule struct {
|
||||
session *discord.Discord
|
||||
categoryID string
|
||||
archiveCategoryID string
|
||||
}
|
||||
|
||||
// NewManageEventChannelsComponent creates a new component
|
||||
func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string, session *discord.Discord) common.Module {
|
||||
return &manageEventChannelsModule{
|
||||
session: session,
|
||||
categoryID: categoryID,
|
||||
archiveCategoryID: archiveCategoryID,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *manageEventChannelsModule) Initialize(birdbot common.ModuleManager) error {
|
||||
_ = birdbot.OnEventCreate(c.OnEventCreate)
|
||||
_ = birdbot.OnEventComplete(c.OnEventComplete)
|
||||
_ = birdbot.OnEventDelete(c.OnEventDelete)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnEventCreate creates a new channel for an event and moves it to a given category
|
||||
func (c *manageEventChannelsModule) OnEventCreate(e common.Event) error {
|
||||
channel, err := c.session.NewChannelFromName(core.GenerateChannelFromEvent(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
|
||||
}
|
||||
|
||||
// OnEventDelete deletes the channel associated with the given event
|
||||
func (c *manageEventChannelsModule) OnEventDelete(e common.Event) error {
|
||||
_, err := c.session.DeleteChannel(core.GenerateChannelFromEvent(e))
|
||||
if err != nil {
|
||||
log.Print("Failed to create channel for event: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnEventComplete archives a given event channel if not given
|
||||
// an archive category will delete the channel instead
|
||||
func (c *manageEventChannelsModule) OnEventComplete(e common.Event) error {
|
||||
channel := core.GenerateChannelFromEvent(e)
|
||||
|
||||
if c.archiveCategoryID != "" {
|
||||
|
||||
if err := c.session.MoveChannelToCategory(channel, c.archiveCategoryID); 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
|
||||
}
|
43
modules/recurring_events.go
Normal file
43
modules/recurring_events.go
Normal file
@ -0,0 +1,43 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/yeslayla/birdbot/common"
|
||||
"github.com/yeslayla/birdbot/discord"
|
||||
)
|
||||
|
||||
type recurringEventsModule struct {
|
||||
session *discord.Discord
|
||||
}
|
||||
|
||||
// NewRecurringEventsComponent creates a new component instance
|
||||
func NewRecurringEventsComponent() common.Module {
|
||||
return &recurringEventsModule{}
|
||||
}
|
||||
|
||||
// Initialize registers event listeners
|
||||
func (c *recurringEventsModule) Initialize(birdbot common.ModuleManager) error {
|
||||
_ = birdbot.OnEventComplete(c.OnEventComplete)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnEventComplete checks for keywords before creating a new event
|
||||
func (c *recurringEventsModule) 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
|
||||
}
|
90
modules/role_selection.go
Normal file
90
modules/role_selection.go
Normal file
@ -0,0 +1,90 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/yeslayla/birdbot/common"
|
||||
"github.com/yeslayla/birdbot/core"
|
||||
"github.com/yeslayla/birdbot/discord"
|
||||
)
|
||||
|
||||
type roleSelectionModule struct {
|
||||
session *discord.Discord
|
||||
cfg core.RoleSelectionConfig
|
||||
exlusive bool
|
||||
}
|
||||
|
||||
// NewRoleSelectionComponent creates a new component
|
||||
func NewRoleSelectionComponent(discord *discord.Discord, cfg core.RoleSelectionConfig) common.Module {
|
||||
return &roleSelectionModule{
|
||||
session: discord,
|
||||
cfg: cfg,
|
||||
exlusive: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize setups component on discord and registers handlers
|
||||
func (c *roleSelectionModule) Initialize(birdbot common.ModuleManager) error {
|
||||
|
||||
roles := []*discord.Role{}
|
||||
roleButtons := []discord.Component{}
|
||||
|
||||
for _, roleConfig := range c.cfg.Roles {
|
||||
|
||||
// Create & Validate Roles
|
||||
role := c.session.GetRoleAndCreate(roleConfig.RoleName)
|
||||
configColor, _ := core.HexToColor(roleConfig.Color)
|
||||
|
||||
if role.Color != configColor {
|
||||
role.Color = configColor
|
||||
role.Save()
|
||||
}
|
||||
|
||||
// Create button
|
||||
btn := c.session.NewButton(fmt.Sprint(c.cfg.Title, role.Name), role.Name)
|
||||
btn.OnClick(func(user common.User) {
|
||||
|
||||
// Remove other roles if exclusive
|
||||
if c.exlusive {
|
||||
for _, r := range roles {
|
||||
if r.ID == role.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if c.session.HasRole(user, r) {
|
||||
c.session.UnassignRole(user, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle role
|
||||
if c.session.HasRole(user, role) {
|
||||
if err := c.session.UnassignRole(user, role); err != nil {
|
||||
log.Printf("Failed to unassign role: %s", err)
|
||||
}
|
||||
} else if err := c.session.AssignRole(user, role); err != nil {
|
||||
log.Printf("Failed to assign role: %s", err)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
roles = append(roles, role)
|
||||
roleButtons = append(roleButtons, btn)
|
||||
}
|
||||
|
||||
components := []discord.Component{}
|
||||
var actionRow *discord.ActionRow
|
||||
for i, btn := range roleButtons {
|
||||
if i%5 == 0 {
|
||||
actionRow = c.session.NewActionRow()
|
||||
components = append(components, actionRow)
|
||||
}
|
||||
|
||||
actionRow.AddComponent(btn)
|
||||
}
|
||||
|
||||
c.session.CreateMessageComponent(c.cfg.SelectionChannel, fmt.Sprintf("**%s**\n%s", c.cfg.Title, c.cfg.Description), components)
|
||||
|
||||
return nil
|
||||
}
|
@ -10,10 +10,29 @@ discord:
|
||||
archive_category: ""
|
||||
notification_channel: ""
|
||||
|
||||
# # Configure role selection
|
||||
# role_selection:
|
||||
# - title: "SELECTION TITLE"
|
||||
# description: "SELECTION DESCRIPTION"
|
||||
# discord_channel: ""
|
||||
# roles:
|
||||
# - name: Red
|
||||
# color: "#f64c38"
|
||||
# - name: Blue
|
||||
# color: "#1a88ff"
|
||||
|
||||
|
||||
# mastodon:
|
||||
# server: https://mastodon.social
|
||||
# username: my_user
|
||||
# password: secret
|
||||
# client_id: 1234
|
||||
# client_secret: secret2
|
||||
# client_secret: secret2
|
||||
|
||||
# # Feature flags can be used to
|
||||
# # disable specific features
|
||||
# features:
|
||||
# manage_event_channels: true
|
||||
# announce_events: true
|
||||
# recurring_events: true
|
||||
# role_selection: true
|
Loading…
Reference in New Issue
Block a user