diff --git a/internal/commands/config/public/config.go b/internal/commands/config/public/config.go index d5c640a..225df4e 100644 --- a/internal/commands/config/public/config.go +++ b/internal/commands/config/public/config.go @@ -108,6 +108,19 @@ var Config = &discordgo.ApplicationCommand{ }, }, }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "setdefaultrole", + Description: "Set the default role given to new members", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionRole, + Name: "role", + Description: "Role to assign on join", + Required: true, + }, + }, + }, }, } @@ -153,6 +166,9 @@ func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { case "setwelcomegif": log.Printf("welcome: /config setwelcomegif invoked by %s in guild %s", i.Member.User.ID, i.GuildID) handleSetWelcomeGIF(s, i) + case "setdefaultrole": + log.Printf("defaultrole: /config setdefaultrole invoked by %s in guild %s", i.Member.User.ID, i.GuildID) + handleSetDefaultRole(s, i) default: respondEphemeral(s, i, "Unknown subcommand.") } @@ -399,6 +415,51 @@ func handleSetWelcomeGIF(s *discordgo.Session, i *discordgo.InteractionCreate) { respondEphemeral(s, i, "Welcome GIF updated.") } +func handleSetDefaultRole(s *discordgo.Session, i *discordgo.InteractionCreate) { + if services.Global.DefaultRole == nil { + respondEphemeral(s, i, "Default role configuration is not available.") + return + } + + guildID, err := strconv.ParseInt(i.GuildID, 10, 64) + if err != nil { + respondEphemeral(s, i, "Invalid guild ID.") + return + } + + opt := i.ApplicationCommandData().Options[0] + var roleOpt *discordgo.ApplicationCommandInteractionDataOption + for _, o := range opt.Options { + if o.Name == "role" { + roleOpt = o + break + } + } + if roleOpt == nil { + respondEphemeral(s, i, "Missing role option.") + return + } + + role := roleOpt.RoleValue(s, i.GuildID) + if role == nil { + respondEphemeral(s, i, "Invalid role.") + return + } + + roleID, err := strconv.ParseInt(role.ID, 10, 64) + if err != nil { + respondEphemeral(s, i, "Invalid role ID.") + return + } + + if err := services.Global.DefaultRole.SetRole(context.Background(), guildID, roleID); err != nil { + respondEphemeral(s, i, "Failed to set default role.") + return + } + + respondEphemeral(s, i, "Default role set to "+role.Mention()+".") +} + func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, diff --git a/internal/db/repos/defaultrolerepo/repo.go b/internal/db/repos/defaultrolerepo/repo.go new file mode 100644 index 0000000..5886e94 --- /dev/null +++ b/internal/db/repos/defaultrolerepo/repo.go @@ -0,0 +1,47 @@ +package defaultrolerepo + +import ( + "context" + "database/sql" +) + +type Repo struct { + db *sql.DB +} + +func NewRepo(db *sql.DB) *Repo { + return &Repo{db: db} +} + +// GetRole returns the configured default role ID for a guild, or 0 if none. +func (r *Repo) GetRole(ctx context.Context, guildID int64) (int64, error) { + const q = ` + SELECT role_id + FROM defaultrole + WHERE guild_id = $1 + ` + var roleID sql.NullInt64 + if err := r.db.QueryRowContext(ctx, q, guildID).Scan(&roleID); err != nil { + if err == sql.ErrNoRows { + return 0, nil + } + return 0, err + } + if !roleID.Valid { + return 0, nil + } + return roleID.Int64, nil +} + +// SetRole upserts the default role for a guild. +func (r *Repo) SetRole(ctx context.Context, guildID, roleID int64) error { + const q = ` + INSERT INTO defaultrole (guild_id, role_id) + VALUES ($1, $2) + ON CONFLICT (guild_id) + DO UPDATE SET role_id = EXCLUDED.role_id + ` + _, err := r.db.ExecContext(ctx, q, guildID, roleID) + return err +} + diff --git a/internal/db/services/defaultrole/service.go b/internal/db/services/defaultrole/service.go new file mode 100644 index 0000000..60e9cdb --- /dev/null +++ b/internal/db/services/defaultrole/service.go @@ -0,0 +1,24 @@ +package defaultrole + +import ( + "context" + + "velox-bot/internal/db/repos/defaultrolerepo" +) + +type Service struct { + repo *defaultrolerepo.Repo +} + +func New(repo *defaultrolerepo.Repo) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetRole(ctx context.Context, guildID int64) (int64, error) { + return s.repo.GetRole(ctx, guildID) +} + +func (s *Service) SetRole(ctx context.Context, guildID, roleID int64) error { + return s.repo.SetRole(ctx, guildID, roleID) +} + diff --git a/internal/db/services/services.go b/internal/db/services/services.go index 820ecab..0c0c96e 100644 --- a/internal/db/services/services.go +++ b/internal/db/services/services.go @@ -10,6 +10,7 @@ import ( "velox-bot/internal/db/services/twitch" "velox-bot/internal/db/services/usersettings" "velox-bot/internal/db/services/welcome" + "velox-bot/internal/db/services/defaultrole" ) type Services struct { @@ -22,11 +23,12 @@ type Services struct { RPS *rps.Service Twitch *twitch.Service Welcome *welcome.Service + DefaultRole *defaultrole.Service } var Global *Services -func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service) *Services { +func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service) *Services { s := &Services{ Level: level, LevelSettings: levelSettings, @@ -37,6 +39,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee RPS: rps, Twitch: twitchSvc, Welcome: welcomeSvc, + DefaultRole: defaultRoleSvc, } Global = s return s diff --git a/internal/events/welcome.go b/internal/events/welcome.go index 9c23b8f..8f7231d 100644 --- a/internal/events/welcome.go +++ b/internal/events/welcome.go @@ -29,10 +29,7 @@ func HandleGuildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc log.Printf("welcome: failed to load settings for guild %s: %v", m.GuildID, err) return } - if settings == nil || settings.ChannelID == 0 { - // No welcome channel configured; nothing to do. - return - } + hasWelcomeChannel := settings != nil && settings.ChannelID != 0 memberMention := m.User.Mention() @@ -48,54 +45,59 @@ func HandleGuildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc userAvatarURL = discordgo.EndpointUserAvatar(m.User.ID, m.User.Avatar) } - // Build guild welcome message. - channelID := strconv.FormatInt(settings.ChannelID, 10) - welcomeMsg := strings.TrimSpace(settings.WelcomeMessage) - if welcomeMsg == "" { - // Default message when none configured. - // "! Welcome to ! Have fun!" - welcomeMsg = "! Welcome to ! Have fun!" - } - welcomeMsg = strings.ReplaceAll(welcomeMsg, "{user}", memberMention) - if guildName != "" { - welcomeMsg = strings.ReplaceAll(welcomeMsg, "{server}", guildName) - welcomeMsg = strings.ReplaceAll(welcomeMsg, "", guildName) - } - welcomeMsg = strings.ReplaceAll(welcomeMsg, "", memberMention) - - embed := &discordgo.MessageEmbed{ - Title: "👋 Welcome!", - Description: welcomeMsg, - Color: 0xFFA500, - Footer: &discordgo.MessageEmbedFooter{ - Text: "ID: " + m.User.ID, - }, - } - if userAvatarURL != "" { - embed.Author = &discordgo.MessageEmbedAuthor{ - Name: m.User.Username, - IconURL: userAvatarURL, + if hasWelcomeChannel { + // Build guild welcome message. + channelID := strconv.FormatInt(settings.ChannelID, 10) + welcomeMsg := strings.TrimSpace(settings.WelcomeMessage) + if welcomeMsg == "" { + // Default message when none configured. + // "! Welcome to ! Have fun!" + welcomeMsg = "! Welcome to ! Have fun!" } - embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ - URL: userAvatarURL, + welcomeMsg = strings.ReplaceAll(welcomeMsg, "{user}", memberMention) + if guildName != "" { + welcomeMsg = strings.ReplaceAll(welcomeMsg, "{server}", guildName) + welcomeMsg = strings.ReplaceAll(welcomeMsg, "", guildName) } - } - gif := strings.TrimSpace(settings.WelcomeGIFURL) - if gif == "" { - // Default GIF when none configured. - gif = "https://images-ext-1.discordapp.net/external/uJ6XfdK2WwDnei3RmNWUqSiOVboC4mK9r78TtgVE_9g/https/media.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif" - } - embed.Image = &discordgo.MessageEmbedImage{URL: gif} + welcomeMsg = strings.ReplaceAll(welcomeMsg, "", memberMention) - if _, err := s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Content: memberMention, - Embed: embed, - }); err != nil { - log.Printf("welcome: failed to send channel welcome in guild %s: %v", m.GuildID, err) + embed := &discordgo.MessageEmbed{ + Title: "👋 Welcome!", + Description: welcomeMsg, + Color: 0xFFA500, + Footer: &discordgo.MessageEmbedFooter{ + Text: "ID: " + m.User.ID, + }, + } + if userAvatarURL != "" { + embed.Author = &discordgo.MessageEmbedAuthor{ + Name: m.User.Username, + IconURL: userAvatarURL, + } + embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: userAvatarURL, + } + } + gif := strings.TrimSpace(settings.WelcomeGIFURL) + if gif == "" { + // Default GIF when none configured. + gif = "https://images-ext-1.discordapp.net/external/uJ6XfdK2WwDnei3RmNWUqSiOVboC4mK9r78TtgVE_9g/https/media.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif" + } + embed.Image = &discordgo.MessageEmbedImage{URL: gif} + + if _, err := s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Content: memberMention, + Embed: embed, + }); err != nil { + log.Printf("welcome: failed to send channel welcome in guild %s: %v", m.GuildID, err) + } } // Optional DM. - dmText := strings.TrimSpace(settings.WelcomeDM) + dmText := "" + if settings != nil { + dmText = strings.TrimSpace(settings.WelcomeDM) + } if dmText == "" { // Default DM when none configured. // "Welcome to ! Have fun!" @@ -115,5 +117,17 @@ func HandleGuildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc if _, err := s.ChannelMessageSend(dmCh.ID, dmText); err != nil { log.Printf("welcome: failed to send DM welcome to user %s: %v", m.User.ID, err) } + + // Default role assignment. + if svc.DefaultRole != nil { + roleID, err := svc.DefaultRole.GetRole(ctx, guildID) + if err != nil { + log.Printf("defaultrole: failed to get default role for guild %s: %v", m.GuildID, err) + } else if roleID != 0 { + if err := s.GuildMemberRoleAdd(m.GuildID, m.User.ID, strconv.FormatInt(roleID, 10)); err != nil { + log.Printf("defaultrole: failed to assign role %d to user %s in guild %s: %v", roleID, m.User.ID, m.GuildID, err) + } + } + } } diff --git a/main.go b/main.go index 5c5f499..a78799e 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "velox-bot/internal/db/repos/welcomerepo" "velox-bot/internal/db/repos/twitchrepo" "velox-bot/internal/db/repos/usersettingsrepo" + "velox-bot/internal/db/repos/defaultrolerepo" "velox-bot/internal/db/services" "velox-bot/internal/db/services/level" "velox-bot/internal/db/services/levelsettings" @@ -28,6 +29,7 @@ import ( "velox-bot/internal/db/services/twitch" "velox-bot/internal/db/services/usersettings" "velox-bot/internal/db/services/welcome" + "velox-bot/internal/db/services/defaultrole" ) func main() { @@ -52,6 +54,7 @@ func main() { userSettingsRepo := usersettingsrepo.NewRepo(db) welcomeRepo := welcomerepo.NewRepo(db) twitchRepo := twitchrepo.NewRepo(db) + defaultRoleRepo := defaultrolerepo.NewRepo(db) levelService := level.New(levelRepo, settingsRepo) levelSettingsService := levelsettings.New(settingsRepo) meetingService := meeting.New(settingsRepo) @@ -61,7 +64,8 @@ func main() { rpsService := rps.New(rpsRepo) twitchService := twitch.New(twitchRepo, config.TwitchClientID) welcomeService := welcome.New(welcomeRepo) - services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService) + defaultRoleService := defaultrole.New(defaultRoleRepo) + services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService) bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services) if err != nil {