Discord Components & Role Selection (#5)
This commit is contained in:
		@ -9,6 +9,7 @@ Bird Bot is a discord bot for managing and organizing events for a small discord
 | 
				
			|||||||
- Delete text channels after events
 | 
					- Delete text channels after events
 | 
				
			||||||
- Archive text channels after events
 | 
					- Archive text channels after events
 | 
				
			||||||
- Create recurring weekly events
 | 
					- Create recurring weekly events
 | 
				
			||||||
 | 
					- Role selection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Usage
 | 
					## Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,7 +17,7 @@ To get up and running, install go and you can run `make run`!
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Using Docker
 | 
					## 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: 
 | 
					Example: 
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ type Bot struct {
 | 
				
			|||||||
	onEventUpdatedHandlers   [](func(common.Event) error)
 | 
						onEventUpdatedHandlers   [](func(common.Event) error)
 | 
				
			||||||
	onEventCompletedHandlers [](func(common.Event) error)
 | 
						onEventCompletedHandlers [](func(common.Event) error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gameModules []common.GameModule
 | 
						gameModules []common.ChatSyncModule
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initalize creates the discord session and registers handlers
 | 
					// 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.OnEventDelete(app.onEventDelete)
 | 
				
			||||||
	app.Session.OnEventUpdate(app.onEventUpdate)
 | 
						app.Session.OnEventUpdate(app.onEventUpdate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						btn := app.Session.NewButton("test", "Click Me")
 | 
				
			||||||
 | 
						btn.OnClick(func(user common.User) {
 | 
				
			||||||
 | 
							print("clicked")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						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 {
 | 
						if err := component.Initialize(loader); err != nil {
 | 
				
			||||||
		log.Print("Failed to load component: ", err)
 | 
							log.Print("Failed to load component: ", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -50,7 +50,7 @@ func (loader *ComponentLoader) OnEventComplete(handler func(common.Event) error)
 | 
				
			|||||||
	return nil
 | 
						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")
 | 
						return fmt.Errorf("unimplemented")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadPlugin loads a plugin and returns its component if successful
 | 
					// 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)
 | 
						plug, err := plugin.Open(pluginPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -25,8 +25,8 @@ func LoadPlugin(pluginPath string) common.Component {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Validate component type
 | 
						// Validate component type
 | 
				
			||||||
	var component common.Component
 | 
						var component common.Module
 | 
				
			||||||
	component, ok := sym.(common.Component)
 | 
						component, ok := sym.(common.Module)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		log.Printf("Failed to load plugin '%s': Plugin component does not properly implement interface!", pluginPath)
 | 
							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
 | 
					// 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)
 | 
						paths, err := os.ReadDir(directory)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("Failed to load plugins: %s", err)
 | 
							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 {
 | 
						for _, path := range paths {
 | 
				
			||||||
		if path.IsDir() {
 | 
							if path.IsDir() {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
package common
 | 
					package common
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type GameModule interface {
 | 
					type ChatSyncModule interface {
 | 
				
			||||||
	SendMessage(user string, message string)
 | 
						SendMessage(user string, message string)
 | 
				
			||||||
	RecieveMessage(user User, message string)
 | 
						RecieveMessage(user User, message string)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,12 +1,12 @@
 | 
				
			|||||||
package common
 | 
					package common
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Component interface {
 | 
					type Module interface {
 | 
				
			||||||
	Initialize(birdbot ComponentManager) error
 | 
						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
 | 
					// by listening to events and committing actions
 | 
				
			||||||
type ComponentManager interface {
 | 
					type ModuleManager interface {
 | 
				
			||||||
	OnReady(func() error) error
 | 
						OnReady(func() error) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	OnNotify(func(string) error) error
 | 
						OnNotify(func(string) error) error
 | 
				
			||||||
@ -21,5 +21,5 @@ type ComponentManager interface {
 | 
				
			|||||||
	CreateEvent(event Event) error
 | 
						CreateEvent(event Event) error
 | 
				
			||||||
	Notify(message string) 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"`
 | 
						EventCategory       string `yaml:"event_category" env:"DISCORD_EVENT_CATEGORY"`
 | 
				
			||||||
	ArchiveCategory     string `yaml:"archive_category" env:"DISCORD_ARCHIVE_CATEGORY"`
 | 
						ArchiveCategory     string `yaml:"archive_category" env:"DISCORD_ARCHIVE_CATEGORY"`
 | 
				
			||||||
	NotificationChannel string `yaml:"notification_channel" env:"DISCORD_NOTIFICATION_CHANNEL"`
 | 
						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
 | 
					// MastodonConfig contains mastodon specific configuration
 | 
				
			||||||
@ -33,6 +48,7 @@ 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"`
 | 
				
			||||||
	ReccurringEvents    Feature `yaml:"recurring_events" env:"BIRD_RECURRING_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"`
 | 
						LoadGamePlugins     Feature `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,3 +4,8 @@ package core
 | 
				
			|||||||
func Bool(v bool) *bool {
 | 
					func Bool(v bool) *bool {
 | 
				
			||||||
	return &v
 | 
						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
 | 
						return event
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateEvent creates a new discord event
 | 
				
			||||||
func (discord *Discord) CreateEvent(event common.Event) error {
 | 
					func (discord *Discord) CreateEvent(event common.Event) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	params := &discordgo.GuildScheduledEventParams{
 | 
						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
 | 
					// NewUser creates a new user object from a discordgo.User object
 | 
				
			||||||
func NewUser(user *discordgo.User) common.User {
 | 
					func NewUser(user *discordgo.User) common.User {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		log.Print("Cannot user object, user is nil!")
 | 
							log.Print("Cannot user object, user is nil!")
 | 
				
			||||||
		return common.User{
 | 
							return common.User{
 | 
				
			||||||
@ -20,3 +21,38 @@ func NewUser(user *discordgo.User) common.User {
 | 
				
			|||||||
		ID: user.ID,
 | 
							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 (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@ -8,13 +8,13 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type announceEventsComponent struct {
 | 
					type announceEventsComponent struct {
 | 
				
			||||||
	bot      common.ComponentManager
 | 
						bot      common.ModuleManager
 | 
				
			||||||
	mastodon *mastodon.Mastodon
 | 
						mastodon *mastodon.Mastodon
 | 
				
			||||||
	guildID  string
 | 
						guildID  string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewAnnounceEventsComponent creates a new component
 | 
					// 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{
 | 
						return &announceEventsComponent{
 | 
				
			||||||
		mastodon: mastodon,
 | 
							mastodon: mastodon,
 | 
				
			||||||
		guildID:  guildID,
 | 
							guildID:  guildID,
 | 
				
			||||||
@ -22,7 +22,7 @@ func NewAnnounceEventsComponent(mastodon *mastodon.Mastodon, guildID string) com
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initialize registers event listeners
 | 
					// Initialize registers event listeners
 | 
				
			||||||
func (c *announceEventsComponent) Initialize(birdbot common.ComponentManager) error {
 | 
					func (c *announceEventsComponent) Initialize(birdbot common.ModuleManager) error {
 | 
				
			||||||
	c.bot = birdbot
 | 
						c.bot = birdbot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ = birdbot.OnEventCreate(c.OnEventCreate)
 | 
						_ = birdbot.OnEventCreate(c.OnEventCreate)
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package components
 | 
					package events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
@ -15,7 +15,7 @@ type manageEventChannelsComponent struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewManageEventChannelsComponent creates a new component
 | 
					// 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{
 | 
						return &manageEventChannelsComponent{
 | 
				
			||||||
		session:           session,
 | 
							session:           session,
 | 
				
			||||||
		categoryID:        categoryID,
 | 
							categoryID:        categoryID,
 | 
				
			||||||
@ -24,7 +24,7 @@ func NewManageEventChannelsComponent(categoryID string, archiveCategoryID string
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initialize registers event listeners
 | 
					// 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.OnEventCreate(c.OnEventCreate)
 | 
				
			||||||
	_ = birdbot.OnEventComplete(c.OnEventComplete)
 | 
						_ = birdbot.OnEventComplete(c.OnEventComplete)
 | 
				
			||||||
	_ = birdbot.OnEventDelete(c.OnEventDelete)
 | 
						_ = birdbot.OnEventDelete(c.OnEventDelete)
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package components
 | 
					package events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
@ -13,12 +13,12 @@ type recurringEventsComponent struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewRecurringEventsComponent creates a new component instance
 | 
					// NewRecurringEventsComponent creates a new component instance
 | 
				
			||||||
func NewRecurringEventsComponent() common.Component {
 | 
					func NewRecurringEventsComponent() common.Module {
 | 
				
			||||||
	return &recurringEventsComponent{}
 | 
						return &recurringEventsComponent{}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initialize registers event listeners
 | 
					// Initialize registers event listeners
 | 
				
			||||||
func (c *recurringEventsComponent) Initialize(birdbot common.ComponentManager) error {
 | 
					func (c *recurringEventsComponent) Initialize(birdbot common.ModuleManager) error {
 | 
				
			||||||
	_ = birdbot.OnEventComplete(c.OnEventComplete)
 | 
						_ = birdbot.OnEventComplete(c.OnEventComplete)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
							
								
								
									
										14
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								main.go
									
									
									
									
									
								
							@ -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/modules"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PluginsDirectory = "./plugins"
 | 
					const PluginsDirectory = "./plugins"
 | 
				
			||||||
@ -59,13 +59,19 @@ func main() {
 | 
				
			|||||||
	loader := app.NewComponentLoader(bot)
 | 
						loader := app.NewComponentLoader(bot)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if cfg.Features.AnnounceEvents.IsEnabledByDefault() {
 | 
						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() {
 | 
						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() {
 | 
						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) {
 | 
						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: ""
 | 
					  archive_category: ""
 | 
				
			||||||
  notification_channel: ""
 | 
					  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:
 | 
					# mastodon:
 | 
				
			||||||
#   server: https://mastodon.social
 | 
					#   server: https://mastodon.social
 | 
				
			||||||
#   username: my_user
 | 
					#   username: my_user
 | 
				
			||||||
#   password: secret
 | 
					#   password: secret
 | 
				
			||||||
#   client_id: 1234
 | 
					#   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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user