22-- This file is part of the "Not a Bot" application
33-- For conditions of distribution and use, see copyright notice in LICENSE
44
5- local client = Client
6- local discordia = Discordia
7- local bot = Bot
8- local enums = discordia.enums
5+ local Bot = Bot
6+ local Client = Client
7+ local Discordia = Discordia
8+ local Clock = Discordia.Clock
9+ local Date = Discordia.Date
10+
911
1012Module.Name = "logs"
1113
14+ local messagesToDelete = {}
15+
1216function Module:GetConfigTable()
1317 return {
1418 {
1519 Name = "ChannelManagementLogChannel",
1620 Description = "Where channel created/updated/deleted should be logged",
17- Type = bot .ConfigType.Channel,
21+ Type = Bot .ConfigType.Channel,
1822 Optional = true
1923 },
2024 {
2125 Name = "DeletedMessageChannel",
2226 Description = "Where deleted messages should be logged",
23- Type = bot .ConfigType.Channel,
27+ Type = Bot .ConfigType.Channel,
2428 Optional = true
2529 },
30+ {
31+ Name = "EnableLogRotation",
32+ Description = "Enable log rotation",
33+ Type = Bot.ConfigType.Boolean,
34+ Default = false
35+ },
36+ {
37+ Name = "LogRetentionPeriod",
38+ Description = "The retention period of logs",
39+ Type = Bot.ConfigType.Duration,
40+ Default = 2 * 365 * 24 * 60 * 60 -- 2 years
41+ },
2642 {
2743 Name = "IgnoredDeletedMessageChannels",
2844 Description = "Messages deleted in those channels will not be logged",
29- Type = bot .ConfigType.Channel,
45+ Type = Bot .ConfigType.Channel,
3046 Array = true,
3147 Default = {}
3248 },
3349 {
3450 Name = "NicknameChangedLogChannel",
3551 Description = "Where nickname changes should be logged",
36- Type = bot .ConfigType.Channel,
52+ Type = Bot .ConfigType.Channel,
3753 Optional = true
3854 },
3955 {
4056 Global = true,
4157 Name = "PersistentMessageCacheSize",
4258 Description = "How many of the last messages of every text channel should stay in bot memory?",
43- Type = bot .ConfigType.Integer,
59+ Type = Bot .ConfigType.Integer,
4460 Default = 50
4561 },
4662 }
4763end
4864
65+
66+ function Module:OnLoaded()
67+ self.RotationClock = Clock()
68+ self.RotationClock:on("day", function ()
69+ self:ForEachGuild(function (guildId, config, data, persistentData)
70+ local guild = Client:getGuild(guildId)
71+ if (guild) then
72+ local config = self:GetConfig(guild)
73+ if not config.EnableLogRotation then
74+ return
75+ end
76+
77+ Module:RotateLogs(guild,
78+ config.LogRetentionPeriod,
79+ {
80+ config.ChannelManagementLogChannel,
81+ config.DeletedMessageChannel,
82+ config.NicknameChangedLogChannel,
83+ }
84+ )
85+ end
86+ end)
87+ end)
88+
89+ self.DeletionClock = Clock()
90+ self.DeletionClock:on("sec", function ()
91+ if next(messagesToDelete) then
92+ table.remove(messagesToDelete):delete()
93+ end
94+ end)
95+
96+ return true
97+ end
98+
99+ function Module:OnUnload()
100+ self.RotationClock:stop()
101+ self.DeletionClock:stop()
102+ end
103+
104+ function Module:OnReady()
105+ self.RotationClock:start()
106+ self.DeletionClock:start()
107+ end
108+
49109function Module:OnEnable(guild)
50110 local data = self:GetData(guild)
51111
@@ -96,7 +156,7 @@ function Module:OnChannelDelete(channel)
96156 embed = {
97157 title = "Channel deleted",
98158 description = channel.name,
99- timestamp = discordia. Date():toISO('T', 'Z')
159+ timestamp = Date():toISO('T', 'Z')
100160 }
101161 })
102162end
@@ -123,7 +183,7 @@ function Module:OnChannelCreate(channel)
123183 embed = {
124184 title = "Channel created",
125185 description = "<#" .. channel.id .. ">",
126- timestamp = discordia. Date():toISO('T', 'Z')
186+ timestamp = Date():toISO('T', 'Z')
127187 }
128188 })
129189end
@@ -154,7 +214,7 @@ function Module:OnMemberUpdate(member)
154214 embed = {
155215 title = "Nickname changed",
156216 description = string.format("%s - `%s` → `%s`", member.mentionString, data.nicknames[member.id], member.name),
157- timestamp = discordia. Date():toISO('T', 'Z')
217+ timestamp = Date():toISO('T', 'Z')
158218 }
159219 })
160220 end
@@ -164,7 +224,7 @@ function Module:OnMemberUpdate(member)
164224 embed = {
165225 title = "Username changed",
166226 description = string.format("%s - `%s` → `%s`", member.mentionString, data.usernames[member.id], member.user.username),
167- timestamp = discordia. Date():toISO('T', 'Z')
227+ timestamp = Date():toISO('T', 'Z')
168228 }
169229 })
170230 end
@@ -199,7 +259,7 @@ function Module:OnMessageDelete(message)
199259 embed.footer = {
200260 text = string.format("Author ID: %s | Message ID: %s", message.author.id, message.id)
201261 }
202- embed.timestamp = discordia. Date():toISO('T', 'Z')
262+ embed.timestamp = Date():toISO('T', 'Z')
203263
204264 logChannel:send({
205265 embed = embed
@@ -231,7 +291,7 @@ function Module:OnMessageDeleteUncached(channel, messageId)
231291 footer = {
232292 text = string.format("Message ID: %s", messageId)
233293 },
234- timestamp = discordia. Date():toISO('T', 'Z')
294+ timestamp = Date():toISO('T', 'Z')
235295 }
236296 })
237297end
@@ -257,3 +317,47 @@ function Module:OnMessageCreate(message)
257317 table.remove(cachedMessages, 1)
258318 end
259319end
320+
321+ local function isMessageTooOld(message, logRetentionPeriod)
322+ local messageTimestamp = Date.fromSnowflake(message.id):toSeconds()
323+ return os.difftime(os.time(), messageTimestamp) > logRetentionPeriod
324+ end
325+
326+ local function scheduleOldMessagesToDeletion(firstMessage, channel, logRetentionPeriod)
327+ local buf = {}
328+
329+ repeat
330+ -- Sort by id to keep the temporal order
331+ local messages = channel:getMessagesAfter(firstMessage.id, 100):toArray("id")
332+ table.insert(messages, 1, firstMessage)
333+
334+ for _, msg in ipairs(messages) do
335+ if msg.author.id == Client.user.id
336+ and (msg.embed and msg.embed.description and msg.embed.description:match("Deleted message"))
337+ and isMessageTooOld(msg, logRetentionPeriod) then
338+ table.insert(buf, msg)
339+ end
340+ end
341+
342+ firstMessage = messages[#messages]
343+ until next(buf) or messages == nil
344+
345+ messagesToDelete = buf
346+ end
347+
348+ function Module:RotateLogs(guild, logRetentionPeriod, logChannels)
349+ local done = {}
350+ for _, logChannelId in pairs(logChannels) do
351+ if not done[logChannelId] then
352+ local logChannel = guild:getChannel(logChannelId)
353+ local firstMessage = logChannel:getFirstMessage()
354+
355+ if firstMessage then
356+ scheduleOldMessagesToDeletion(firstMessage, logChannel, logRetentionPeriod)
357+ end
358+
359+ done[logChannelId] = true
360+ end
361+ end
362+ end
363+
0 commit comments