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
 | 
			
		||||
- 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,6 +10,17 @@ 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
 | 
			
		||||
@ -17,3 +28,11 @@ discord:
 | 
			
		||||
#   password: secret
 | 
			
		||||
#   client_id: 1234
 | 
			
		||||
#   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