Discord Components & Role Selection (#5)

This commit is contained in:
Layla 2023-04-01 01:23:37 -04:00 committed by GitHub
parent 5e6a433b92
commit e1038a15cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 696 additions and 31 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -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

View File

@ -1,6 +1,6 @@
package common
type GameModule interface {
type ChatSyncModule interface {
SendMessage(user string, message string)
RecieveMessage(user User, message string)
}

View File

@ -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
View 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
View 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)
}

View File

@ -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"`
}

View File

@ -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
View 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)
}
}

View 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,
}
}

View 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,
}
}

View File

@ -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
View 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)
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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
View File

@ -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) {

View 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
}

View 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
}

View 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
View 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
}

View File

@ -10,10 +10,29 @@ discord:
archive_category: ""
notification_channel: ""
# # Configure role selection
# role_selection:
# - title: "SELECTION TITLE"
# description: "SELECTION DESCRIPTION"
# discord_channel: ""
# roles:
# - name: Red
# color: "#f64c38"
# - name: Blue
# color: "#1a88ff"
# mastodon:
# server: https://mastodon.social
# username: my_user
# password: secret
# client_id: 1234
# client_secret: secret2
# client_secret: secret2
# # Feature flags can be used to
# # disable specific features
# features:
# manage_event_channels: true
# announce_events: true
# recurring_events: true
# role_selection: true