discord-community/mod_reactionRole.go
Knut Ahlers c08bacbecf
Lint: Disable some linters for single functions
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2021-08-21 01:14:21 +02:00

235 lines
7.1 KiB
Go

package main
import (
"fmt"
"strings"
"time"
"github.com/Luzifer/go_helpers/v2/env"
"github.com/Luzifer/go_helpers/v2/str"
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
)
/*
* @module reactionrole
* @module_desc Creates a post with pre-set reactions and assigns roles on reaction
*/
func init() {
RegisterModule("reactionrole", func() module { return &modReactionRole{} })
}
type modReactionRole struct {
attrs moduleAttributeStore
discord *discordgo.Session
id string
}
func (m modReactionRole) ID() string { return m.id }
func (m *modReactionRole) Initialize(id string, crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error {
m.attrs = attrs
m.discord = discord
m.id = id
if err := attrs.Expect(
"discord_channel_id",
"embed_title",
"reaction_roles",
); err != nil {
return errors.Wrap(err, "validating attributes")
}
discord.AddHandler(m.handleMessageReactionAdd)
discord.AddHandler(m.handleMessageReactionRemove)
return nil
}
//nolint:funlen,gocyclo // Single task, seeing no sense in splitting
func (m modReactionRole) Setup() error {
var err error
// @attr discord_channel_id required string "" ID of the Discord channel to post the message to
channelID := m.attrs.MustString("discord_channel_id", nil)
// @attr content optional string "" Message content to post above the embed
contentString := m.attrs.MustString("content", ptrStringEmpty)
msgEmbed := &discordgo.MessageEmbed{
// @attr embed_color optional int64 "0x2ECC71" Integer / HEX representation of the color for the embed
Color: int(m.attrs.MustInt64("embed_color", ptrInt64(streamScheduleDefaultColor))),
// @attr embed_description optional string "" Description for the embed block
Description: strings.TrimSpace(m.attrs.MustString("embed_description", ptrStringEmpty)),
Timestamp: time.Now().Format(time.RFC3339),
// @attr embed_title required string "" Title of the embed
Title: m.attrs.MustString("embed_title", nil),
Type: discordgo.EmbedTypeRich,
}
if m.attrs.MustString("embed_thumbnail_url", ptrStringEmpty) != "" {
msgEmbed.Thumbnail = &discordgo.MessageEmbedThumbnail{
// @attr embed_thumbnail_url optional string "" Publically hosted image URL to use as thumbnail
URL: m.attrs.MustString("embed_thumbnail_url", ptrStringEmpty),
// @attr embed_thumbnail_width optional int64 "" Width of the thumbnail
Width: int(m.attrs.MustInt64("embed_thumbnail_width", ptrInt64Zero)),
// @attr embed_thumbnail_height optional int64 "" Height of the thumbnail
Height: int(m.attrs.MustInt64("embed_thumbnail_height", ptrInt64Zero)),
}
}
reactionListRaw, err := m.attrs.StringSlice("reaction_roles")
if err != nil {
return errors.Wrap(err, "getting role list")
}
var reactionList []string
for _, r := range reactionListRaw {
reactionList = append(reactionList, strings.Split(r, "=")[0])
}
var managedMsg *discordgo.Message
if err = store.ReadWithLock(m.id, func(a moduleAttributeStore) error {
mid, err := a.String("message_id")
if err == errValueNotSet {
return nil
}
managedMsg, err = m.discord.ChannelMessage(channelID, mid)
return errors.Wrap(err, "fetching managed message")
}); err != nil && !strings.Contains(err.Error(), "404") {
return err
}
if managedMsg == nil {
managedMsg, err = m.discord.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
Content: contentString,
Embed: msgEmbed,
})
} else if !isDiscordMessageEmbedEqual(managedMsg.Embeds[0], msgEmbed) || managedMsg.Content != contentString {
_, err = m.discord.ChannelMessageEditComplex(&discordgo.MessageEdit{
Content: &contentString,
Embed: msgEmbed,
ID: managedMsg.ID,
Channel: channelID,
})
}
if err != nil {
return errors.Wrap(err, "updating / creating message")
}
if err = store.Set(m.id, "message_id", managedMsg.ID); err != nil {
return errors.Wrap(err, "storing managed message id")
}
var addedReactions []string
for _, r := range managedMsg.Reactions {
okName := str.StringInSlice(r.Emoji.Name, reactionList)
compiledName := fmt.Sprintf(":%s:%s", r.Emoji.Name, r.Emoji.ID)
okCode := str.StringInSlice(compiledName, reactionList)
if !okCode && !okName {
id := r.Emoji.ID
if id == "" {
id = r.Emoji.Name
}
if err = m.discord.MessageReactionsRemoveEmoji(channelID, managedMsg.ID, id); err != nil {
return errors.Wrap(err, "removing reaction emoji")
}
continue
}
addedReactions = append(addedReactions, compiledName, r.Emoji.Name)
}
for _, emoji := range reactionList {
if !str.StringInSlice(emoji, addedReactions) {
log.WithFields(log.Fields{
"emote": emoji,
"message": managedMsg.ID,
"module": m.id,
}).Trace("Adding emoji reaction")
if err = m.discord.MessageReactionAdd(channelID, managedMsg.ID, emoji); err != nil {
return errors.Wrap(err, "adding reaction emoji")
}
}
}
return nil
}
func (m modReactionRole) extractRoles() (map[string]string, error) {
// @attr reaction_roles required []string "" List of strings in format `emote=role-id[:set]`. `emote` equals an unicode emote (✅) or a custom emote in form `:<emote-name>:<emote-id>`. `role-id` is the integer ID of the guilds role to add with this emote. If `:set` is added at the end, the role will only be added but not removed when the reaction is removed.
list, err := m.attrs.StringSlice("reaction_roles")
if err != nil {
return nil, errors.Wrap(err, "getting role list")
}
return env.ListToMap(list), nil
}
func (m modReactionRole) handleMessageReaction(d *discordgo.Session, e *discordgo.MessageReaction, add bool) {
if e.UserID == m.discord.State.User.ID {
// Reaction was manipulated by the bot, ignore
return
}
var (
err error
messageID string
)
if err = store.ReadWithLock(m.id, func(a moduleAttributeStore) error {
messageID, err = a.String("message_id")
return errors.Wrap(err, "reading message ID")
}); err != nil {
log.WithError(err).Error("Unable to get managed message ID")
return
}
if messageID == "" || e.MessageID != messageID {
// This is not our managed message (or we don't have one), we don't care
return
}
roles, err := m.extractRoles()
if err != nil {
log.WithError(err).Error("Unable to extract role mapping")
return
}
for _, check := range []string{e.Emoji.Name, fmt.Sprintf(":%s:%s", e.Emoji.Name, e.Emoji.ID)} {
role, ok := roles[check]
if !ok {
continue
}
if add {
if err = m.discord.GuildMemberRoleAdd(config.GuildID, e.UserID, strings.Split(role, ":")[0]); err != nil {
log.WithError(err).Error("Unable to add role to user")
}
return
}
if !strings.HasSuffix(role, ":set") {
if err = m.discord.GuildMemberRoleRemove(config.GuildID, e.UserID, strings.Split(role, ":")[0]); err != nil {
log.WithError(err).Error("Unable to remove role to user")
}
return
}
}
}
func (m modReactionRole) handleMessageReactionAdd(d *discordgo.Session, e *discordgo.MessageReactionAdd) {
m.handleMessageReaction(d, e.MessageReaction, true)
}
func (m modReactionRole) handleMessageReactionRemove(d *discordgo.Session, e *discordgo.MessageReactionRemove) {
m.handleMessageReaction(d, e.MessageReaction, false)
}