-- Copyright (C) 2018 Jrme Leclercq
-- This file is part of the "Not a Bot" application
-- For conditions of distribution and use, see copyright notice in LICENSE
local bot = Bot
local client = Client
local discordia = Discordia
local enums = discordia.enums
local timer = require("timer")
discordia.extensions() -- load all helpful extensions
Module.Name = "channels"
local emojiOrder = {
zero = 0,
one = 1,
two = 2,
three = 3,
four = 4,
five = 5,
six = 6,
seven = 7,
eight = 8,
nine = 9
}
local function arrayRemoveValue(t, v)
local i = 1
while (i 0) then
table.insert(actionStr, string.format("**Adds role%s** %s\n", #addedRoles > 1 and "s" or "", table.concat(addedRoles, ", ")))
end
end
if (actions.RemoveRoles) then
local removedRoles = {}
for _, roleId in pairs(actions.RemoveRoles) do
local role = guild:getRole(roleId)
if (role) then
table.insert(removedRoles, role.mentionString)
else
table.insert(removedRoles, "")
end
end
if (#removedRoles > 0) then
table.insert(actionStr, string.format("**Removes role%s** %s\n", #removedRoles > 1 and "s" or "", table.concat(removedRoles, ", ")))
end
end
if (actions.SendMessage) then
table.insert(actionStr, string.format("**Sends private message:**\n\"%s\"\n", actions.SendMessage))
end
if (actions.ToggleRoles) then
local toggleRoles = {}
for _, roleId in pairs(actions.ToggleRoles) do
local role = guild:getRole(roleId)
if (role) then
table.insert(toggleRoles, role.mentionString)
else
table.insert(toggleRoles, "")
end
end
if (#toggleRoles > 0) then
table.insert(actionStr, string.format("**Toggles role%s** %s", #toggleRoles > 1 and "s" or "", table.concat(toggleRoles, ", ")))
end
end
local emoji = bot:GetEmojiData(guild, emoji)
table.insert(fields, {
name = string.format("- %s:", emoji and emoji.MentionString or ""),
value = table.concat(actionStr, "\n")
})
end
commandMessage:reply({
embed = {
description = string.format("Message in %s:\n%s", channel.mentionString, bot:GenerateMessageLink(message)),
fields = fields,
footer = {
text = "Use `!updatechannelconfig ` to update channels reactions settings."
}
}
})
end
end
end
end
end
})
self:RegisterCommand({
Name = "updatechannelconfig",
Args = {
{Name = "message", Type = Bot.ConfigType.Message},
{Name = "emoji", Type = Bot.ConfigType.Emoji},
{Name = "action", Type = Bot.ConfigType.String},
{Name = "value", Type = Bot.ConfigType.String, Optional = true}
},
PrivilegeCheck = function (member) return member:hasPermission(enums.permission.administrator) end,
Help = "Configures the channel module messages and reactions",
Func = function (commandMessage, message, emoji, action, value)
local guild = commandMessage.guild
local member = commandMessage.member
local config = self:GetConfig(guild)
local GetReactionActionsConfig = function (channelId, messageId, reaction, noCreate)
local guildConfig = config.ReactionActions
local channelTable = guildConfig[channelId]
if (not channelTable) then
if (noCreate) then
return
end
channelTable = {}
guildConfig[channelId] = channelTable
end
local messageTable = channelTable[messageId]
if (not messageTable) then
if (noCreate) then
return
end
messageTable = {}
channelTable[messageId] = messageTable
end
local reactionActions = messageTable[reaction]
if (not reactionActions) then
if (noCreate) then
return
end
reactionActions = {}
messageTable[reaction] = reactionActions
end
return reactionActions
end
local success = false
if (action == "addrole") then
local role, err = Bot:DecodeRole(guild, value)
if (not role) then
commandMessage:reply(string.format("Invalid role: %s", err))
return
end
if not member:hasPermission(enums.permission.manageRoles) then
commandMessage:reply("you need to have the manage roles permission to toggle a role")
return
end
if role.position > member.highestRole.position then
commandMessage:reply("you cannot add or remove a role higher than your own")
return
end
local roleValue = tostring(role.id)
local reactionActions = GetReactionActionsConfig(message.channel.id, message.id, emoji.Name)
if (reactionActions.AddRoles) then
if (not table.search(reactionActions.AddRoles, roleValue)) then
table.insert(reactionActions.AddRoles, tostring(role.id))
end
else
reactionActions.AddRoles = {roleValue}
end
if (reactionActions.RemoveRoles) then
arrayRemoveValue(reactionActions.RemoveRoles, roleValue)
end
if (reactionActions.ToggleRoles) then
arrayRemoveValue(reactionActions.ToggleRoles, roleValue)
end
success = true
commandMessage:reply(string.format("Reactions on %s for %s will now adds role %s (%s)", Bot:GenerateMessageLink(message), emoji.MentionString, role.name, role.id))
elseif (action == "removerole") then
local role, err = Bot:DecodeRole(guild, value)
if (not role) then
commandMessage:reply(string.format("Invalid role: %s", err))
return
end
if not member:hasPermission(enums.permission.manageRoles) then
commandMessage:reply("you need to have the manage roles permission to toggle a role")
return
end
if role.position > member.highestRole.position then
commandMessage:reply("you cannot add or remove a role higher than your own")
return
end
local roleValue = tostring(role.id)
local reactionActions = GetReactionActionsConfig(message.channel.id, message.id, emoji.Name)
if (reactionActions.RemoveRoles) then
if (not table.search(reactionActions.RemoveRoles, roleValue)) then
table.insert(reactionActions.RemoveRoles, tostring(role.id))
end
else
reactionActions.RemoveRoles = {roleValue}
end
if (reactionActions.AddRoles) then
arrayRemoveValue(reactionActions.AddRoles, roleValue)
end
if (reactionActions.ToggleRoles) then
arrayRemoveValue(reactionActions.ToggleRoles, roleValue)
end
success = true
commandMessage:reply(string.format("Reactions on %s for %s will now remove role %s (%s)", Bot:GenerateMessageLink(message), emoji.MentionString, role.name, role.id))
elseif (action == "togglerole") then
local role, err = Bot:DecodeRole(guild, value)
if (not role) then
commandMessage:reply(string.format("Invalid role: %s", err))
return
end
if not member:hasPermission(enums.permission.manageRoles) then
commandMessage:reply("you need to have the manage roles permission to toggle a role")
return
end
if role.position > member.highestRole.position then
commandMessage:reply("you cannot add or remove a role higher than your own")
return
end
local roleValue = tostring(role.id)
local reactionActions = GetReactionActionsConfig(message.channel.id, message.id, emoji.Name)
if (reactionActions.ToggleRoles) then
if (not table.search(reactionActions.ToggleRoles, roleValue)) then
table.insert(reactionActions.ToggleRoles, tostring(role.id))
end
else
reactionActions.ToggleRoles = {roleValue}
end
if (reactionActions.AddRoles) then
arrayRemoveValue(reactionActions.AddRoles, roleValue)
end
if (reactionActions.RemoveRoles) then
arrayRemoveValue(reactionActions.RemoveRoles, roleValue)
end
success = true
commandMessage:reply(string.format("Reactions on %s for %s will now toggle role %s (%s)", Bot:GenerateMessageLink(message), emoji.MentionString, role.name, role.id))
elseif (action == "send") then
if (not value) then
commandMessage:reply("Empty message")
return
end
local reactionActions = GetReactionActionsConfig(message.channel.id, message.id, emoji.Name)
reactionActions.SendMessage = value
success = true
commandMessage:reply(string.format("Reactions on %s for %s will now send private message: %s", Bot:GenerateMessageLink(message), emoji.MentionString, value))
elseif (action == "clear") then
local reactionActions = GetReactionActionsConfig(message.channel.id, message.id, emoji.Name, true)
if (reactionActions) then
reactionActions.AddRoles = nil
reactionActions.RemoveRoles = nil
reactionActions.ToggleRoles = nil
reactionActions.SendMessage = nil
end
success = true
commandMessage:reply(string.format("Reactions actions on %s for %s have been cleared", Bot:GenerateMessageLink(message), emoji.MentionString))
else
commandMessage:reply("Invalid action (must be addrole/clear/removerole/send/togglerole)")
end
if (success) then
self:SaveGuildConfig(guild)
commandMessage:reply(string.format("Configuration of module %s has been saved, use the `!reload %s` command to activate it", self.Name, self.Name))
end
end
})
return true
end
function Module:GetReactionActions(guild, channelId, messageId, reaction, noCreate)
local reactionKey = string.format("%s_%s_%s", channelId, messageId, reaction)
local data = self:GetData(guild)
local roleActions = data.ReactionActions[reactionKey]
if (not roleActions and not noCreate) then
roleActions = {}
roleActions.Add = {}
roleActions.Remove = {}
roleActions.Toggle = {}
data.ReactionActions[reactionKey] = roleActions
end
return roleActions
end
function Module:OnEnable(guild)
local config = self:GetConfig(guild)
local data = self:GetData(guild)
data.ReactionActions = {}
self:HandleConfig(guild, config)
return true
end
function Module:HandleConfig(guild, config)
self:LogInfo(guild, "Processing roles...")
local ProcessRole = function (channelId, messageId, reaction, roleId, field)
local roleActions = self:GetReactionActions(guild, channelId, messageId, reaction)
table.insert(roleActions[field], roleId)
end
local configUpdated = false
for channelId,messageTable in pairs(config.ReactionActions) do
for messageId,reactionTable in pairs(messageTable) do
for reaction, actions in pairs(reactionTable) do
local hasActions = false
if (actions.AddRoles) then
arrayFilter(actions.AddRoles, function (roleId)
local role = guild:getRole(roleId)
if (role) then
ProcessRole(channelId, messageId, reaction, roleId, "Add")
hasActions = true
return true
else
self:LogWarning(guild, "Role %s not found", roleId)
configUpdated = true
return false
end
end)
end
if (actions.RemoveRoles) then
arrayFilter(actions.RemoveRoles, function (roleId)
local role = guild:getRole(roleId)
if (role) then
ProcessRole(channelId, messageId, reaction, roleId, "Remove")
hasActions = true
return true
else
self:LogWarning(guild, "Role %s not found", roleId)
configUpdated = true
return false
end
end)
end
if (actions.ToggleRoles) then
arrayFilter(actions.ToggleRoles, function (roleId)
local role = guild:getRole(roleId)
if (role) then
ProcessRole(channelId, messageId, reaction, roleId, "Toggle")
hasActions = true
return true
else
self:LogWarning(guild, "Role %s not found", roleId)
configUpdated = true
return false
end
end)
end
if (actions.SendMessage) then
local roleActions = self:GetReactionActions(guild, channelId, messageId, reaction)
roleActions.Message = actions.SendMessage
hasActions = true
end
if (not hasActions) then
reactionTable[reaction] = nil
configUpdated = true
end
end
if (next(reactionTable) == nil) then
messageTable[messageId] = nil
configUpdated = true
end
end
if (next(messageTable) == nil) then
config.ReactionActions[channelId] = nil
configUpdated = true
end
end
self:LogInfo(guild, "Adding emojis to concerned messages...")
-- Make sure reactions are present on messages
for channelId,messageTable in pairs(config.ReactionActions) do
local channel = guild:getChannel(channelId)
if (channel) then
for messageId,reactionTable in pairs(messageTable) do
local message = channel:getMessage(messageId)
if (message) then
local hasReaction = {}
local messageReactions = {}
for k,reaction in pairs(message.reactions) do
local emoji = bot:GetEmojiData(guild, reaction.emojiId or reaction.emojiName)
if (emoji) then
hasReaction[emoji.Name] = true
else
self:LogError(guild, "found reaction which does not exist in guild: %s", reaction.emojiName)
end
local expectedCount = 0
if (reaction.me) then
expectedCount = 1
end
if (reaction.count ~= expectedCount) then
messageReactions[reaction] = reaction:getUsers()
end
end
local reactionToAdd = {}
for reaction, _ in pairs(reactionTable) do
local emoji = bot:GetEmojiData(guild, reaction)
if (emoji) then
if (not hasReaction[emoji.Name]) then
table.insert(reactionToAdd, reaction)
end
else
self:LogError(guild, "Emoji \"%s\" does not exist", reaction.emojiName)
end
end
table.sort(reactionToAdd, function (a, b)
local i = emojiOrder[a]
local j = emojiOrder[b]
if (i and j) then
return i < j
else
return a < b
end
end)
for _, reaction in pairs(reactionToAdd) do
local emoji = Bot:GetEmojiData(guild, reaction)
local success, err = message:addReaction(emoji.Emoji or emoji.Id)
if (not success) then
self:LogWarning(guild, "Failed to add reaction %s on message %s (channel: %s): %s", emoji.Name, message.id, channelId, err)
end
end
-- Handle users reactions
for reaction, users in pairs(messageReactions) do
local emoji = bot:GetEmojiData(guild, reaction.emojiId or reaction.emojiName)
if (emoji) then
for _, user in pairs(users) do
if (self:HandleReactionAdd(guild, user.id, channelId, messageId, emoji.Name)) then
local success, err = reaction:delete(user.id)
if (not success) then
self:LogWarning(guild, "Failed to remove reaction on message (%s)", err)
end
end
end
end
end
else
self:LogError(guild, "Message %s no longer exists in channel %s", messageId, channelId)
messageTable[messageId] = nil
configUpdated = true
end
end
else
self:LogError(guild, "Channel %s no longer exists", channelId)
config.ReactionActions[channelId] = nil
configUpdated = true
end
end
if (configUpdated) then
self:SaveGuildConfig(guild)
end
end
function Module:HandleConfigUpdate(guild, config, configName)
if (not configName or configName == "ReactionActions") then
self:HandleConfig(guild, config)
end
end
function Module:HandleReactionAdd(guild, userId, channelId, messageId, reactionName)
if (client.user.id == userId) then
return false
end
local roleActions = self:GetReactionActions(guild, channelId, messageId, reactionName, true)
if (not roleActions) then
return false
end
local isActive = false
local member = guild:getMember(userId)
if (not member) then
return true
end
for _,roleId in pairs(roleActions.Add) do
local role = guild:getRole(roleId)
if (role) then
if (not member:hasRole(role)) then
self:LogInfo(guild, "Adding %s%s to %s", role.name, (role.color ~= 0) and " (colored)" or "", member.tag)
local success, err = member:addRole(roleId)
if (not success) then
self:LogWarning(guild, "Failed to add role %s to %s: %s", role.name, member.tag, err)
end
end
isActive = true
else
self:LogWarning(guild, "Role %s appears to have been removed", roleId)
end
end
for _,roleId in pairs(roleActions.Remove) do
local role = guild:getRole(roleId)
if (role) then
if (member:hasRole(role)) then
self:LogInfo(guild, "Removing %s%s from %s", role.name, (role.color ~= 0) and " (colored)" or "", member.tag)
local success, err = member:removeRole(roleId)
if (not success) then
self:LogWarning(guild, "Failed to remove role %s from %s: %s", role.name, member.tag, err)
end
end
isActive = true
else
self:LogWarning(guild, "Role %s appears to have been removed", roleId)
end
end
for _,roleId in pairs(roleActions.Toggle) do
local role = guild:getRole(roleId)
if (role) then
local hasRole = member:hasRole(role)
self:LogInfo(guild, "Toggling %s%s (%s) from %s", role.name, (role.color ~= 0) and " (colored)" or "", hasRole and "removing" or "adding", member.tag)
local success, err
if (hasRole) then
success, err = member:removeRole(role)
else
success, err = member:addRole(role)
end
if (not success) then
self:LogWarning(guild, "Failed to toggle role %s from %s: %s", role.name, member.tag, err)
end
isActive = true
else
self:LogWarning(guild, "Role %s appears to have been removed", roleId)
end
end
if (roleActions.Message) then
local privateChannel = member.user:getPrivateChannel()
if (privateChannel) then
local success, err = privateChannel:send(string.format("[From %s]\n%s", guild.name, roleActions.Message))
if (not success) then
self:LogWarning(guild, "Failed to send reaction message to %s (maybe user disabled private messages from this server?): %s", member.user.tag, err)
end
isActive = true
else
self:LogWarning(guild, "Failed to get private channel with %s", member.user.tag)
end
end
return isActive
end
function Module:OnReactionAdd(reaction, userId)
if (not bot:IsPublicChannel(reaction.message.channel)) then
return
end
local emoji = bot:GetEmojiData(reaction.message.guild, reaction.emojiId or reaction.emojiName)
if (not emoji) then
return
end
if (self:HandleReactionAdd(reaction.message.channel.guild, userId, reaction.message.channel.id, reaction.message.id, emoji.Name)) then
timer.sleep(1000) -- Wait a bit before removing reaction (so user won't think it failed)
local success, err = reaction.message:removeReaction(emoji.Emoji or emoji.Id, userId)
if (not success) then
self:LogWarning(reaction.message.guild, "Failed to remove reaction for message (%s)", err)
end
end
end
function Module:OnReactionAddUncached(channel, messageId, reactionIdOrName, userId)
if (not bot:IsPublicChannel(channel)) then
return
end
local emoji = bot:GetEmojiData(channel.guild, reactionIdOrName)
if (not emoji) then
self:LogWarning(channel.guild, "Emoji %s was used but not found in guild", reactionIdOrName)
return
end
if (self:HandleReactionAdd(channel.guild, userId, channel.id, messageId, emoji.Name)) then
local message = channel:getMessage(messageId)
if (message) then
timer.sleep(1000) -- Wait a bit before removing reaction (so user won't think it failed)
local success, err = message:removeReaction(emoji.Emoji or emoji.Id, userId)
if (not success) then
self:LogWarning(channel.guild, "Failed to remove reaction for uncached message (%s)", err)
end
end
end
end