owobot/internal/systems/roles/commands.go

277 lines
7.3 KiB
Go

/*
* owobot - Your server's guardian and entertainer
* Copyright (C) 2023 owobot Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package roles
import (
"fmt"
"regexp"
"slices"
"strings"
"github.com/bwmarrin/discordgo"
"go.elara.ws/owobot/internal/cache"
"go.elara.ws/owobot/internal/db"
"go.elara.ws/owobot/internal/emoji"
"go.elara.ws/owobot/internal/util"
)
// reactionRolesCmd handles the `/reaction_roles` command and routes it to the correct subcommand.
func reactionRolesCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
switch name := data.Options[0].Name; name {
case "new_category":
return reactionRolesNewCategoryCmd(s, i)
case "remove_category":
return reactionRolesRemoveCategoryCmd(s, i)
case "add":
return reactionRolesAddCmd(s, i)
case "remove":
return reactionRolesRemoveCmd(s, i)
default:
return fmt.Errorf("unknown reaction_roles subcommand: %s", name)
}
}
// reactionRolesNewCategoryCmd handles the `/reaction_roles new_category` command.
func reactionRolesNewCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
args := data.Options[0].Options
rrc := db.ReactionRoleCategory{
Name: args[0].StringValue(),
}
if len(args) > 1 {
rrc.Description = args[1].StringValue()
}
msg, err := s.ChannelMessageSendEmbed(i.ChannelID, &discordgo.MessageEmbed{
Title: rrc.Name,
Description: rrc.Description,
})
if err != nil {
return err
}
rrc.MsgID = msg.ID
err = db.AddReactionRoleCategory(i.ChannelID, rrc)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully added a new reaction role category called `%s`!", rrc.Name))
}
// reactionRolesRemoveCategoryCmd handles the `/reaction_roles remove_category` command.
func reactionRolesRemoveCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
args := data.Options[0].Options
name := args[0].StringValue()
rrc, err := db.GetReactionRoleCategory(i.ChannelID, name)
if err != nil {
return err
}
err = s.ChannelMessageDelete(rrc.ChannelID, rrc.MsgID)
if err != nil {
return err
}
err = db.DeleteReactionRoleCategory(i.ChannelID, name)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Removed reaction role category `%s`", args[0].StringValue()))
}
// reactionRolesAddCmd handles the `/reaction_roles add` command.
func reactionRolesAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
args := data.Options[0].Options
category := args[0].StringValue()
role := args[1].RoleValue(s, i.GuildID)
emojiStr := args[2].StringValue()
_, ok := emoji.Parse(emojiStr)
if !ok {
return fmt.Errorf("invalid reaction role emoji: %s", emojiStr)
}
err := db.AddReactionRole(i.ChannelID, category, emojiStr, role)
if err != nil {
return err
}
err = updateReactionRoleCategoryMsg(s, i.ChannelID, category)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Added reaction role %s to `%s`", role.Mention(), category))
}
// reactionRolesRemoveCmd handles the `/reaction_roles remove` command.
func reactionRolesRemoveCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
args := data.Options[0].Options
category := args[0].StringValue()
role := args[1].RoleValue(s, i.GuildID)
err := db.DeleteReactionRole(i.ChannelID, category, role)
if err != nil {
return err
}
err = updateReactionRoleCategoryMsg(s, i.ChannelID, category)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Removed reaction role %s from `%s`", role.Mention(), category))
}
var neopronounValidationRegex = regexp.MustCompile(`^[a-z]+(/[a-z]+)+$`)
// neopronounCmd handles the `/neopronoun` command.
func neopronounCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
name := data.Options[0].StringValue()
name = strings.ToLower(name)
if !neopronounValidationRegex.MatchString(name) {
return fmt.Errorf("invalid neopronoun: `%s`", name)
}
roles, err := cache.Roles(s, i.GuildID)
if err != nil {
return err
}
var roleID string
for _, role := range roles {
// Skip this role if it provides any permissions, so that
// we don't accidentally grant the member any extra permissions
if role.Permissions != 0 {
continue
}
if role.Name == name {
roleID = role.ID
break
}
}
if roleID == "" {
role, err := s.GuildRoleCreate(i.GuildID, &discordgo.RoleParams{
Name: name,
Mentionable: util.Pointer(false),
Permissions: util.Pointer[int64](0),
})
if err != nil {
return err
}
roleID = role.ID
}
if slices.Contains(i.Member.Roles, roleID) {
err = s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Unassigned the `%s` role", name))
} else {
err = s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID)
if err != nil {
return err
}
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully assigned the `%s` role to you!", name))
}
}
// updateReactionRoleCategoryMsg updates a reaction role category message
func updateReactionRoleCategoryMsg(s *discordgo.Session, channelID, category string) error {
rrc, err := db.GetReactionRoleCategory(channelID, category)
if err != nil {
return err
}
var sb strings.Builder
if rrc.Description != "" {
sb.WriteString(rrc.Description)
sb.WriteString("\n\n")
}
var (
components []discordgo.MessageComponent
currentRow discordgo.ActionsRow
)
for i, roleID := range rrc.Roles {
// Action rows can only contain 5 elements,
// so we create a new row if we reach a multiple
// of 5.
if i > 0 && i%5 == 0 {
components = append(components, currentRow)
currentRow = discordgo.ActionsRow{}
}
e, ok := emoji.Parse(rrc.Emoji[i])
if !ok {
return fmt.Errorf("invalid reaction role emoji: %s", rrc.Emoji[i])
}
sb.WriteString(rrc.Emoji[i])
sb.WriteString(" - <@&")
sb.WriteString(roleID)
sb.WriteString(">\n")
currentRow.Components = append(currentRow.Components, discordgo.Button{
CustomID: "role:" + roleID,
Style: discordgo.SecondaryButton,
Emoji: &discordgo.ComponentEmoji{
Name: e.Name,
ID: e.ID,
},
})
}
if len(currentRow.Components) > 0 {
components = append(components, currentRow)
}
_, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: channelID,
ID: rrc.MsgID,
Embeds: &[]*discordgo.MessageEmbed{
{
Title: rrc.Name,
Description: sb.String(),
},
},
Components: &components,
})
return err
}