From ef7192dbe31200b2aa6cd7482a783116cbf630b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mj=C3=B6llnir?= Date: Fri, 6 Dec 2024 18:55:49 +0100 Subject: [PATCH 1/2] spaces2tabs --- module_logs.lua | 358 ++++++++++++++++++++++++------------------------ 1 file changed, 179 insertions(+), 179 deletions(-) diff --git a/module_logs.lua b/module_logs.lua index 9e89697..4735137 100644 --- a/module_logs.lua +++ b/module_logs.lua @@ -11,31 +11,31 @@ Module.Name = "logs" function Module:GetConfigTable() return { - { - Name = "ChannelManagementLogChannel", - Description = "Where channel created/updated/deleted should be logged", - Type = bot.ConfigType.Channel, - Optional = true - }, + { + Name = "ChannelManagementLogChannel", + Description = "Where channel created/updated/deleted should be logged", + Type = bot.ConfigType.Channel, + Optional = true + }, { Name = "DeletedMessageChannel", Description = "Where deleted messages should be logged", Type = bot.ConfigType.Channel, Optional = true }, - { - Name = "NicknameChangedLogChannel", - Description = "Where nickname changes should be logged", - Type = bot.ConfigType.Channel, - Optional = true - }, - { - Name = "IgnoredDeletedMessageChannels", - Description = "Messages deleted in those channels will not be logged", - Type = bot.ConfigType.Channel, - Array = true, - Default = {} - }, + { + Name = "IgnoredDeletedMessageChannels", + Description = "Messages deleted in those channels will not be logged", + Type = bot.ConfigType.Channel, + Array = true, + Default = {} + }, + { + Name = "NicknameChangedLogChannel", + Description = "Where nickname changes should be logged", + Type = bot.ConfigType.Channel, + Optional = true + }, { Global = true, Name = "PersistentMessageCacheSize", @@ -47,213 +47,213 @@ function Module:GetConfigTable() end function Module:OnEnable(guild) - local data = self:GetData(guild) + local data = self:GetData(guild) - -- Keep a reference to the last X messages of every text channel - local messageCacheSize = self.GlobalConfig.PersistentMessageCacheSize - data.cachedMessages = {} + -- Keep a reference to the last X messages of every text channel + local messageCacheSize = self.GlobalConfig.PersistentMessageCacheSize + data.cachedMessages = {} - data.nicknames = {} - for userId, user in pairs(guild.members) do - data.nicknames[userId] = user.nickname - end - data.usernames = {} - for userId, user in pairs(guild.members) do - data.usernames[userId] = user._user._username - end + data.nicknames = {} + for userId, user in pairs(guild.members) do + data.nicknames[userId] = user.nickname + end + data.usernames = {} + for userId, user in pairs(guild.members) do + data.usernames[userId] = user._user._username + end coroutine.wrap(function () - for _, channel in pairs(guild.textChannels) do - data.cachedMessages[channel.id] = Bot:FetchChannelMessages(channel, nil, messageCacheSize, true) - end - end)() + for _, channel in pairs(guild.textChannels) do + data.cachedMessages[channel.id] = Bot:FetchChannelMessages(channel, nil, messageCacheSize, true) + end + end)() return true end function Module:OnChannelDelete(channel) local guild = channel.guild - if not guild then - return - end - - local data = self:GetData(guild) - data.cachedMessages[channel.id] = nil - - local config = self:GetConfig(guild) - local channelManagementLogChannel = config.ChannelManagementLogChannel - if not channelManagementLogChannel then - return - end - - local logChannel = guild:getChannel(channelManagementLogChannel) - if not logChannel then - self:LogWarning(guild, "Channel management log channel %s no longer exists", channelManagementLogChannel) - return - end - - logChannel:send({ - embed = { - title = "Channel deleted", - description = channel.name, - timestamp = discordia.Date():toISO('T', 'Z') - } - }) + if not guild then + return + end + + local data = self:GetData(guild) + data.cachedMessages[channel.id] = nil + + local config = self:GetConfig(guild) + local channelManagementLogChannel = config.ChannelManagementLogChannel + if not channelManagementLogChannel then + return + end + + local logChannel = guild:getChannel(channelManagementLogChannel) + if not logChannel then + self:LogWarning(guild, "Channel management log channel %s no longer exists", channelManagementLogChannel) + return + end + + logChannel:send({ + embed = { + title = "Channel deleted", + description = channel.name, + timestamp = discordia.Date():toISO('T', 'Z') + } + }) end function Module:OnChannelCreate(channel) - local guild = channel.guild - if not guild then - return - end - - local config = self:GetConfig(guild) - local channelManagementLogChannel = config.ChannelManagementLogChannel - if not channelManagementLogChannel then - return - end - - local logChannel = guild:getChannel(channelManagementLogChannel) - if not logChannel then - self:LogWarning(guild, "Channel management log channel %s no longer exists", channelManagementLogChannel) - return - end - - logChannel:send({ - embed = { - title = "Channel created", - description = "<#" .. channel.id .. ">", - timestamp = discordia.Date():toISO('T', 'Z') - } - }) + local guild = channel.guild + if not guild then + return + end + + local config = self:GetConfig(guild) + local channelManagementLogChannel = config.ChannelManagementLogChannel + if not channelManagementLogChannel then + return + end + + local logChannel = guild:getChannel(channelManagementLogChannel) + if not logChannel then + self:LogWarning(guild, "Channel management log channel %s no longer exists", channelManagementLogChannel) + return + end + + logChannel:send({ + embed = { + title = "Channel created", + description = "<#" .. channel.id .. ">", + timestamp = discordia.Date():toISO('T', 'Z') + } + }) end function Module:OnMemberUpdate(member) - local guild = member.guild - if not guild then - return - end + local guild = member.guild + if not guild then + return + end - local config = self:GetConfig(guild) - local nicknameChangeLogChannel = config.NicknameChangedLogChannel - if not nicknameChangeLogChannel then - return - end + local config = self:GetConfig(guild) + local nicknameChangeLogChannel = config.NicknameChangedLogChannel + if not nicknameChangeLogChannel then + return + end - local logChannel = guild:getChannel(nicknameChangeLogChannel) - if not logChannel then - self:LogWarning(guild, "Channel management log channel %s no longer exists", nicknameChangeLogChannel) - return - end + local logChannel = guild:getChannel(nicknameChangeLogChannel) + if not logChannel then + self:LogWarning(guild, "Channel management log channel %s no longer exists", nicknameChangeLogChannel) + return + end - local data = self:GetData(guild) + local data = self:GetData(guild) -- Ignore the first nickname change because new members tend to change it directly after joining which generates a lot of useless logs - if data.nicknames[member.id] ~= nil and data.nicknames[member.id] ~= member.name then - logChannel:send({ - embed = { - title = "Nickname changed", - description = string.format("%s - `%s` → `%s`", member.mentionString, data.nicknames[member.id], member.name), - timestamp = discordia.Date():toISO('T', 'Z') - } - }) - end - - if data.usernames[member.id] ~= nil and data.usernames[member.id] ~= member.user.username then - logChannel:send({ - embed = { - title = "Username changed", - description = string.format("%s - `%s` → `%s`", member.mentionString, data.usernames[member.id], member.user.username), - timestamp = discordia.Date():toISO('T', 'Z') - } - }) - end - - data.nicknames[member.id] = member.name - data.usernames[member.id] = member.user.username + if data.nicknames[member.id] ~= nil and data.nicknames[member.id] ~= member.name then + logChannel:send({ + embed = { + title = "Nickname changed", + description = string.format("%s - `%s` → `%s`", member.mentionString, data.nicknames[member.id], member.name), + timestamp = discordia.Date():toISO('T', 'Z') + } + }) + end + + if data.usernames[member.id] ~= nil and data.usernames[member.id] ~= member.user.username then + logChannel:send({ + embed = { + title = "Username changed", + description = string.format("%s - `%s` → `%s`", member.mentionString, data.usernames[member.id], member.user.username), + timestamp = discordia.Date():toISO('T', 'Z') + } + }) + end + + data.nicknames[member.id] = member.name + data.usernames[member.id] = member.user.username end function Module:OnMessageDelete(message) - local guild = message.guild - local config = self:GetConfig(guild) + local guild = message.guild + local config = self:GetConfig(guild) - if table.search(config.IgnoredDeletedMessageChannels, message.channel.id) then - return - end + if table.search(config.IgnoredDeletedMessageChannels, message.channel.id) then + return + end - local deletedMessageChannel = config.DeletedMessageChannel - if not deletedMessageChannel then - return - end + local deletedMessageChannel = config.DeletedMessageChannel + if not deletedMessageChannel then + return + end - local logChannel = guild:getChannel(deletedMessageChannel) - if not logChannel then - self:LogWarning(guild, "Deleted message log channel %s no longer exists", deletedMessageChannel) - return - end + local logChannel = guild:getChannel(deletedMessageChannel) + if not logChannel then + self:LogWarning(guild, "Deleted message log channel %s no longer exists", deletedMessageChannel) + return + end - local desc = "🗑️ **Deleted message - sent by " .. message.author.mentionString .. " in " .. message.channel.mentionString .. "**\n" + local desc = "🗑️ **Deleted message - sent by " .. message.author.mentionString .. " in " .. message.channel.mentionString .. "**\n" local embed = Bot:BuildQuoteEmbed(message, { initialContentSize = #desc }) - embed.description = desc .. (embed.description or "") + embed.description = desc .. (embed.description or "") embed.footer = { text = string.format("Author ID: %s | Message ID: %s", message.author.id, message.id) } - embed.timestamp = discordia.Date():toISO('T', 'Z') + embed.timestamp = discordia.Date():toISO('T', 'Z') logChannel:send({ - embed = embed + embed = embed }) end function Module:OnMessageDeleteUncached(channel, messageId) - local guild = channel.guild - local config = self:GetConfig(guild) + local guild = channel.guild + local config = self:GetConfig(guild) - if table.search(config.IgnoredDeletedMessageChannels, channel.id) then - return - end + if table.search(config.IgnoredDeletedMessageChannels, channel.id) then + return + end - local deletedMessageChannel = config.DeletedMessageChannel - if not deletedMessageChannel then - return - end + local deletedMessageChannel = config.DeletedMessageChannel + if not deletedMessageChannel then + return + end - local logChannel = guild:getChannel(deletedMessageChannel) - if not logChannel then - self:LogWarning(guild, "Deleted message log channel %s no longer exists", deletedMessageChannel) - return - end + local logChannel = guild:getChannel(deletedMessageChannel) + if not logChannel then + self:LogWarning(guild, "Deleted message log channel %s no longer exists", deletedMessageChannel) + return + end logChannel:send({ - embed = { - description = "🗑️ **Deleted message (uncached) - sent by in " .. channel.mentionString .. "**", - footer = { - text = string.format("Message ID: %s", messageId) - }, - timestamp = discordia.Date():toISO('T', 'Z') - } + embed = { + description = "🗑️ **Deleted message (uncached) - sent by in " .. channel.mentionString .. "**", + footer = { + text = string.format("Message ID: %s", messageId) + }, + timestamp = discordia.Date():toISO('T', 'Z') + } }) end function Module:OnMessageCreate(message) local guild = message.guild - if not guild then - return - end - - local data = self:GetData(guild) - local cachedMessages = data.cachedMessages[message.channel.id] - if not cachedMessages then - cachedMessages = {} - data.cachedMessages[message.channel.id] = cachedMessages - end - - -- Remove oldest message from permanent cache and add the new message - table.insert(cachedMessages, message) - - local messageCacheSize = self.GlobalConfig.PersistentMessageCacheSize - while #cachedMessages > messageCacheSize do - table.remove(cachedMessages, 1) - end + if not guild then + return + end + + local data = self:GetData(guild) + local cachedMessages = data.cachedMessages[message.channel.id] + if not cachedMessages then + cachedMessages = {} + data.cachedMessages[message.channel.id] = cachedMessages + end + + -- Remove oldest message from permanent cache and add the new message + table.insert(cachedMessages, message) + + local messageCacheSize = self.GlobalConfig.PersistentMessageCacheSize + while #cachedMessages > messageCacheSize do + table.remove(cachedMessages, 1) + end end From 2d82b57ad6249043a8d8cdebac836616e2932e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mj=C3=B6llnir?= Date: Mon, 16 Dec 2024 14:52:12 +0100 Subject: [PATCH 2/2] add the ability to rotate logs This feature allows users to delete logs periodically while choosing the retention time. It will help servers located in Europe to comply with GDPR and to keep the logging channels clean. --- module_logs.lua | 134 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 15 deletions(-) diff --git a/module_logs.lua b/module_logs.lua index 4735137..b4146fc 100644 --- a/module_logs.lua +++ b/module_logs.lua @@ -2,50 +2,110 @@ -- This file is part of the "Not a Bot" application -- For conditions of distribution and use, see copyright notice in LICENSE -local client = Client -local discordia = Discordia -local bot = Bot -local enums = discordia.enums +local Bot = Bot +local Client = Client +local Discordia = Discordia +local Clock = Discordia.Clock +local Date = Discordia.Date + Module.Name = "logs" +local messagesToDelete = {} + function Module:GetConfigTable() return { { Name = "ChannelManagementLogChannel", Description = "Where channel created/updated/deleted should be logged", - Type = bot.ConfigType.Channel, + Type = Bot.ConfigType.Channel, Optional = true }, { Name = "DeletedMessageChannel", Description = "Where deleted messages should be logged", - Type = bot.ConfigType.Channel, + Type = Bot.ConfigType.Channel, Optional = true }, + { + Name = "EnableLogRotation", + Description = "Enable log rotation", + Type = Bot.ConfigType.Boolean, + Default = false + }, + { + Name = "LogRetentionPeriod", + Description = "The retention period of logs", + Type = Bot.ConfigType.Duration, + Default = 2 * 365 * 24 * 60 * 60 -- 2 years + }, { Name = "IgnoredDeletedMessageChannels", Description = "Messages deleted in those channels will not be logged", - Type = bot.ConfigType.Channel, + Type = Bot.ConfigType.Channel, Array = true, Default = {} }, { Name = "NicknameChangedLogChannel", Description = "Where nickname changes should be logged", - Type = bot.ConfigType.Channel, + Type = Bot.ConfigType.Channel, Optional = true }, { Global = true, Name = "PersistentMessageCacheSize", Description = "How many of the last messages of every text channel should stay in bot memory?", - Type = bot.ConfigType.Integer, + Type = Bot.ConfigType.Integer, Default = 50 }, } end + +function Module:OnLoaded() + self.RotationClock = Clock() + self.RotationClock:on("day", function () + self:ForEachGuild(function (guildId, config, data, persistentData) + local guild = Client:getGuild(guildId) + if (guild) then + local config = self:GetConfig(guild) + if not config.EnableLogRotation then + return + end + + Module:RotateLogs(guild, + config.LogRetentionPeriod, + { + config.ChannelManagementLogChannel, + config.DeletedMessageChannel, + config.NicknameChangedLogChannel, + } + ) + end + end) + end) + + self.DeletionClock = Clock() + self.DeletionClock:on("sec", function () + if next(messagesToDelete) then + table.remove(messagesToDelete):delete() + end + end) + + return true +end + +function Module:OnUnload() + self.RotationClock:stop() + self.DeletionClock:stop() +end + +function Module:OnReady() + self.RotationClock:start() + self.DeletionClock:start() +end + function Module:OnEnable(guild) local data = self:GetData(guild) @@ -96,7 +156,7 @@ function Module:OnChannelDelete(channel) embed = { title = "Channel deleted", description = channel.name, - timestamp = discordia.Date():toISO('T', 'Z') + timestamp = Date():toISO('T', 'Z') } }) end @@ -123,7 +183,7 @@ function Module:OnChannelCreate(channel) embed = { title = "Channel created", description = "<#" .. channel.id .. ">", - timestamp = discordia.Date():toISO('T', 'Z') + timestamp = Date():toISO('T', 'Z') } }) end @@ -154,7 +214,7 @@ function Module:OnMemberUpdate(member) embed = { title = "Nickname changed", description = string.format("%s - `%s` → `%s`", member.mentionString, data.nicknames[member.id], member.name), - timestamp = discordia.Date():toISO('T', 'Z') + timestamp = Date():toISO('T', 'Z') } }) end @@ -164,7 +224,7 @@ function Module:OnMemberUpdate(member) embed = { title = "Username changed", description = string.format("%s - `%s` → `%s`", member.mentionString, data.usernames[member.id], member.user.username), - timestamp = discordia.Date():toISO('T', 'Z') + timestamp = Date():toISO('T', 'Z') } }) end @@ -199,7 +259,7 @@ function Module:OnMessageDelete(message) embed.footer = { text = string.format("Author ID: %s | Message ID: %s", message.author.id, message.id) } - embed.timestamp = discordia.Date():toISO('T', 'Z') + embed.timestamp = Date():toISO('T', 'Z') logChannel:send({ embed = embed @@ -231,7 +291,7 @@ function Module:OnMessageDeleteUncached(channel, messageId) footer = { text = string.format("Message ID: %s", messageId) }, - timestamp = discordia.Date():toISO('T', 'Z') + timestamp = Date():toISO('T', 'Z') } }) end @@ -257,3 +317,47 @@ function Module:OnMessageCreate(message) table.remove(cachedMessages, 1) end end + +local function isMessageTooOld(message, logRetentionPeriod) + local messageTimestamp = Date.fromSnowflake(message.id):toSeconds() + return os.difftime(os.time(), messageTimestamp) > logRetentionPeriod +end + +local function scheduleOldMessagesToDeletion(firstMessage, channel, logRetentionPeriod) + local buf = {} + + repeat + -- Sort by id to keep the temporal order + local messages = channel:getMessagesAfter(firstMessage.id, 100):toArray("id") + table.insert(messages, 1, firstMessage) + + for _, msg in ipairs(messages) do + if msg.author.id == Client.user.id + and (msg.embed and msg.embed.description and msg.embed.description:match("Deleted message")) + and isMessageTooOld(msg, logRetentionPeriod) then + table.insert(buf, msg) + end + end + + firstMessage = messages[#messages] + until next(buf) or messages == nil + + messagesToDelete = buf +end + +function Module:RotateLogs(guild, logRetentionPeriod, logChannels) + local done = {} + for _, logChannelId in pairs(logChannels) do + if not done[logChannelId] then + local logChannel = guild:getChannel(logChannelId) + local firstMessage = logChannel:getFirstMessage() + + if firstMessage then + scheduleOldMessagesToDeletion(firstMessage, logChannel, logRetentionPeriod) + end + + done[logChannelId] = true + end + end +end +