diff --git a/README.md b/README.md index 4993204..eb07403 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Not a Bot Source code of the bot of the french programming discord server NaN (Not a Name). https://discord.gg/zcWp9sC + +[Documentation (WIP)](doc/index.md) \ No newline at end of file diff --git a/bot_commands.lua b/bot_commands.lua index 1a2eda0..6a077b2 100644 --- a/bot_commands.lua +++ b/bot_commands.lua @@ -124,7 +124,8 @@ client:on('messageCreate', function(message) if (commandTable.PrivilegeCheck) then local success, ret = Bot:ProtectedCall("Command " .. commandName .. " privilege check", commandTable.PrivilegeCheck, message.member) - if (not success) then + + if (not success) then message:reply("An error occurred") return end diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..a4d20f8 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,39 @@ +# Not a Bot - Discord Bot + +Not a Bot is a french moderation bot designed for the [Not a Name](https://discord.gg/zcWp9sC). + +It is based on multiple configurable modules where each ones have their own task. + +The bot is made in Lua, with a custom wrapper around the Discordia library. + +## Summary + +### Modules + +- [Ban]() : Ban management module +- [Channel]() : Channel management module +- [Game]() : Global mudule handling bot status +- [Kick]() : Kick management +- [Mention]() : Mention reaction module +- [Message]() : Message handler module (includes custom commands) +- [Modmail]() : Modmail and tickets module +- [Modo]() : Alert and flags module +- [Mute]() : Mute management module +- [Pin]() : Message pinning module +- [Poll]() : Polling module +- [Purge]() : Purge management module +- [Quote]() : Message quoting module +- [Raid]() : Anti-raid module +- [RoleInfo]() : Infos about roles +- [Stats]() : Guild stat module +- [Twitch]() : Twitch notification module +- [Warn](modules/warn_module.md) : Warning module +- [Welcome]() : Welcome message module + +### API + + + +### External Resources + +- [Discordia Library](https://github.com/SinisterRectus/Discordia) \ No newline at end of file diff --git a/doc/modules/warn_module.md b/doc/modules/warn_module.md new file mode 100644 index 0000000..2772727 --- /dev/null +++ b/doc/modules/warn_module.md @@ -0,0 +1,64 @@ +# Warn Module + +Back to the *[Summary](../index.md)*. + +*[Sources of the module](../../module_warn.lua)* + +The warn module is used to give members warnings about their behaviour in the guild. + +After a given amount of warns, the member gets muted __if the mute module is enabled__ + +## Config + + +- Sanctions (Boolean) (default = true) + - Enables sanctions (mute and ban alert) when a member receives a warning. + +- MinimalWarnRole (Role) (default = nothing) + - Minimal role to be able to warn members and see their histories + - Unlocks `warn` and `warnlist` + +- MinimalUnwarnRole (Role) (default = nothing) + - Minimal role to be able to unwarn a member. + - Unlocks `popwarn` and `clearwarn` + +- WarnAmountToMute (Integer) (default = 3) + - Number of warns needed to mute a member. + +- WarnAmountToBan (Integer) (default = 9) + - Numbed of warns needed to send the ban alert to the moderators. + +- DefaultMuteDuration (Duration) (default = 1 hour) + - Default mute duration when a member gets enough warnings. + - The duration increases as the warning amount increases: `duration = default_duration * (warnings / WarnAmountToMute)` + +- BanInformationChannel (Channel) (default = nothing) + - Channel where all the ban notifications are sent when a player has enough warnings + - This setting is required to enable the module + - You still have to manually ban the member, the last choice remains to the moderation team. + +- WarnLogChannel (Channel) (default = nothing) + - Channel where all the notifications about warns and unwarns are logged. + +- SendPrivateMessage (Boolean) (default = true) + - Enable private messages to inform the member of his warning. + +## Commands + +Assuming the bot prefix is `!` + +- `!warn [reason]` + - This command gives a warning to the target with the given reason. It also checks if the member should receive a mute or more (only if the option is enabled). + - If enabled, the member will receive a private message resuming the warning and where it comes from. + - Example : `!warn @SomePlayer You are a terrible liar` + +- `!warnlist ` + - Shows all the warnings that the given user received. + - Example : `!warnlist @SomePlayer` + +- `!clearwarns ` + - Clears all the history of the given member. + - Example : `!clearwarns @SomePlayer` + +- `!popwarns ` + - Removes the last warn of the targeted member \ No newline at end of file diff --git a/module_warn.lua b/module_warn.lua index 665de05..2444336 100644 --- a/module_warn.lua +++ b/module_warn.lua @@ -54,11 +54,6 @@ local function AddWarn(history, memberId, moderatorId, reason) end end -local function GetWarnAmount(history, memberId) - local member = FindMember(history, memberId) - return table.length(member.Warns) -end - local function SendWarnMessage(commandMessage, targetMember, reason) if not reason then commandMessage:reply(string.format("**%s** has warned **%s**.", commandMessage.member.tag, targetMember.tag)) @@ -67,10 +62,75 @@ local function SendWarnMessage(commandMessage, targetMember, reason) end end +local function generateLogEmbed(title, target, message, timestamp) + local result = { + content = "", + embed = { + title = title, + author = { + name = target.tag, + icon_url = target.avatarURL + }, + fields = { + { + name = "Reason", + value = message, + inline = false + } + }, + timestamp = timestamp + } + } + return result +end + -------------------------------- -function Module:CheckPermissions(member) - return member:hasPermission(enums.permission.banMembers) +function Module:LogWarn(guild, moderator, target, message, timestamp) + local config = self:GetConfig(guild) + local logChannel = guild:getChannel(config.WarnLogChannel) + local success, errMessage = logChannel:send(generateLogEmbed( + string.format("**%s** warned **%s**", moderator.tag, target.tag), + target, + message, + timestamp + )) + if not success then + self:LogError(errMessage) + end +end + +function Module:LogWarnModification(guild, moderator, target, message, timestamp) + local config = self:GetConfig(guild) + local logChannel = guild:getChannel(config.WarnLogChannel) + + logChannel:send(generateLogEmbed( + string.format("**%s** made a modification upon **%s** warns", moderator.tag, target.tag), + target, + message, + timestamp + )) +end + +function Module:GetWarnAmount(history, memberId) + local member = FindMember(history, memberId) + return #member.Warns +end + +function Module:HasWarnRole(member) + local config = self:GetConfig(member.guild) + local warnRole = (member.guild):getRole(config.MinimalWarnRole) + local memberRole = member.highestRole + + return memberRole.position >= warnRole.position +end + +function Module:HasUnwarnRole(member) + local config = self:GetConfig(member.guild) + local unwarnRole = (member.guild):getRole(config.MinimalUnwarnRole) + local memberRole = member.highestRole + + return memberRole.position >= unwarnRole.position end function Module:GetConfigTable() @@ -82,6 +142,18 @@ function Module:GetConfigTable() Type = bot.ConfigType.Boolean, Default = true }, + { + Name = "MinimalWarnRole", + Description = "Minimal role to warn members and see history", + Type = bot.ConfigType.Role, + Default = "" + }, + { + Name = "MinimalUnwarnRole", + Description = "Minimal role to unwarn (remove last warn) of a member", + Type = bot.ConfigType.Role, + Default = "", + }, { Name = "WarnAmountToMute", Description = "Number of warns needed to mute the member.", @@ -102,7 +174,13 @@ function Module:GetConfigTable() }, { Name = "BanInformationChannel", - Description = "Default channel where all the ban-able members are listed.", + Description = "Channel where all the ban-able members are listed.", + Type = bot.ConfigType.Channel, + Default = "" + }, + { + Name = "WarnLogChannel", + Description = "Channel where all the warns, unwarns, ... are logged.", Type = bot.ConfigType.Channel, Default = "" }, @@ -124,7 +202,22 @@ function Module:OnEnable(guild) local banInfo = config.BanInformationChannel and guild:getChannel(config.BanInformationChannel) or nil if not banInfo then - return false, "Invalid ban information channel, check your configuration." + return false, "Invalid BanInformationChannel, check your configuration." + end + + local logChan = config.WarnLogChannel and guild:getChannel(config.WarnLogChannel) or nil + if not logChan then + return false, "Invalid WarnLogChannel, check your configuration." + end + + local warnRole = config.MinimalWarnRole and guild:getRole(config.MinimalWarnRole) or nil + if not warnRole then + return false, "Invalid MinimalWarnRole setting, check your configuration." + end + + local unwarnRole = config.MinimalUnwarnRole and guild:getRole(config.MinimalUnwarnRole) or nil + if not unwarnRole then + return false, "Invalid MinimalUnwarnRole setting, check your configuration." end return true @@ -141,7 +234,9 @@ function Module:OnLoaded() {Name = "target", Type = bot.ConfigType.User}, {Name = "reason", Type = bot.ConfigType.String, Optional = true} }, - PrivilegeCheck = function (member) return self:CheckPermissions(member) end, + PrivilegeCheck = function (member) + return self:HasWarnRole(member) + end, Help = "Warns a member", Silent = true, @@ -154,22 +249,35 @@ function Module:OnLoaded() local targetMember = guild:getMember(targetUser) local moderator = commandMessage.member - -- Permission check - if targetMember then - local bannedByRole = moderator.highestRole - local targetRole = targetMember.highestRole - if targetRole.position >= bannedByRole.position then - commandMessage:reply("You cannot warn this user due to your lower permissions.") - return - end + if not targetMember then + commandMessage:reply("The given member does not exists or is invalid.") + return end + -- Permission check + local bannedByRole = moderator.highestRole + local targetRole = targetMember.highestRole + if targetRole.position >= bannedByRole.position then + commandMessage:reply("You cannot warn this user due to your lower permissions.") + return + end + -- Adding warn to the user local targetId = targetUser.id local moderatorId = commandMessage.member.id AddWarn(history, targetId, moderatorId, reason) - + + if reason then + self:LogWarn(guild, moderator, targetMember, reason, commandMessage.timestamp) + else + self:LogWarn( + guild, + moderator, + targetMember, + "No reason provided.", + commandMessage.timestamp) + end if config.SendPrivateMessage then local privateChannel = targetUser:getPrivateChannel() @@ -177,7 +285,7 @@ function Module:OnLoaded() if reason then privateChannel:send(string.format("You have been warned on %s for the following reason:\n **%s**", guild.name, reason)) else - privateChannel:send(string.format("You have been warned on %s", guild.name)) + privateChannel:send(string.format("You have been warned on %s, no reason provided.", guild.name)) end end end @@ -188,7 +296,7 @@ function Module:OnLoaded() if config.Sanctions then local banAmount = config.WarnAmountToBan local muteAmount = config.WarnAmountToMute - local warnAmount = GetWarnAmount(history, targetId) + local warnAmount = self:GetWarnAmount(history, targetId) if warnAmount % banAmount == 0 then -- BAN @@ -232,7 +340,9 @@ function Module:OnLoaded() Args = { {Name = "targetUser", Type = bot.ConfigType.User} }, - PrivilegeCheck = function (member) return self:CheckPermissions(member) end, + PrivilegeCheck = function (member) + return self:HasWarnRole(member) + end, Help = "Shows all the warns of a member.", Silent = true, @@ -241,6 +351,11 @@ function Module:OnLoaded() local history = self:GetPersistentData(guild) local targetMember = guild:getMember(targetUser) + if not targetMember then + commandMessage:reply("The given member does not exists or is invalid.") + return + end + local memberHistory = FindMember(history, targetMember.id) if not memberHistory then commandMessage:reply(string.format("The member **%s** (%d) doesn't have any warns.", targetMember.tag, targetMember.id)) @@ -265,7 +380,9 @@ function Module:OnLoaded() Args = { {Name = "targetUser", Type = bot.ConfigType.User} }, - PrivilegeCheck = function (member) return self:CheckPermissions(member) end, + PrivilegeCheck = function (member) + return self:HasUnwarnRole(member) + end, Help = "Clears all the warns of a specified user.", Silent = true, @@ -273,17 +390,101 @@ function Module:OnLoaded() local guild = commandMessage.guild local history = self:GetPersistentData(guild) local targetMember = guild:getMember(targetUser) + local moderator = commandMessage.author + + if not targetMember then + commandMessage:reply("The given member does not exists or is invalid.") + return + end local memberHistory = FindMember(history, targetMember.id) if not memberHistory then commandMessage:reply(string.format("The member **%s** (%d) already have zero warns.", targetMember.tag, targetMember.id)) else + for _i, warn in ipairs(memberHistory.Warns) do + self:LogWarnModification( + guild, + moderator, + targetMember, + string.format("**%s** cleared the following warn of **%s** (%d).\nIt was: **%s**\n\t*From: %s*", + moderator.tag, + targetMember.tag, + targetMember.id, + warn.Reason, + guild:getMember(warn.From).tag + ) + ) + end + + self:LogWarnModification( + guild, + moderator, + targetMember, + string.format("**%s** cleared %d warns of **%s** (%d).", + moderator.tag, + #memberHistory.Warns, + targetMember.tag, + targetMember.id + ) + ) + memberHistory.Warns = {} + commandMessage:reply(string.format("Cleared **%s** (%d) warns, saving.", targetMember.tag, targetMember.id)) bot:Save() end end }) + -- + -- popwarn command + -- + self:RegisterCommand({ + Name = "popwarn", + Args = { + {Name = "targetUser", Type = bot.ConfigType.User} + }, + PrivilegeCheck = function (member) + return self:HasUnwarnRole(member) + end, + + Help = "Removes the last warn of the given user.", + Silent = true, + Func = function (commandMessage, targetUser) + local guild = commandMessage.guild + local history = self:GetPersistentData(guild) + local targetMember = guild:getMember(targetUser) + local moderator = commandMessage.author + + if not targetMember then + commandMessage:reply("The given member does not exists or is invalid.") + return + end + + local memberHistory = FindMember(history, targetMember.id) + if not memberHistory then + commandMessage:reply(string.format("The member **%s** (%d) already have zero warns.", targetMember.tag, targetMember.id)) + else + local lastWarn = table.remove(memberHistory.Warns) + local lastWarnReason = lastWarn.Reason + if not lastWarnReason then + lastWarnReason = "No reason provided." + end + self:LogWarnModification( + guild, + moderator, + targetMember, + string.format("**%s** removed the last warn of **%s** (%d).\nIt was: **%s**\n\t*From: %s*", + moderator.tag, + targetMember.tag, + targetMember.id, + lastWarnReason, + guild:getMember(lastWarn.From).tag + ) + ) + end + end + }) + return true -end \ No newline at end of file +end