Skip to content

Commit 88b7dcc

Browse files
committed
- Introduce shared MessageFetcher singleton for rate-limited message fetching.
- Add message caching and fetching logic to `DeletedMessagesListener`. - Replace inlined message formatting with reusable `formatMessageForPaste` function. - Integrate `upload` service to generate paste URLs for deleted messages. - Consolidate starboard and moderation message fetching to rely on `DeletedMessagesListener`.
1 parent c8801fa commit 88b7dcc

4 files changed

Lines changed: 84 additions & 67 deletions

File tree

src/modules/moderation/deletedMessages.listener.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import * as Sentry from "@sentry/bun";
2-
import type { Message, Snowflake } from "discord.js";
2+
import type { Collection, Message, Snowflake, TextBasedChannel } from "discord.js";
33
import ExpiryMap from "expiry-map";
44
import { config } from "../../Config.js";
55
import { logger } from "../../logging.js";
6+
import { messageFetcher } from "../../util/ratelimiting.js";
67
import type { EventListener } from "../module.js";
7-
import {
8-
type CachedMessage,
9-
logBulkDeletedMessages,
10-
logDeletedMessage,
11-
} from "./logs.js";
8+
import { type CachedMessage, logBulkDeletedMessages, logDeletedMessage } from "./logs.js";
129

1310
// Configurable TTL - default 24 hours
1411
const CACHE_TTL_MS =
@@ -47,7 +44,55 @@ function cacheMessage(message: Message): void {
4744
});
4845
}
4946

47+
function cacheMessages(messages: Collection<string, Message>): number {
48+
for (const message of messages.values()) {
49+
cacheMessage(message);
50+
}
51+
return messages.size;
52+
}
53+
54+
async function fetchAndCacheMessages(
55+
channel: TextBasedChannel,
56+
limit: number
57+
): Promise<number> {
58+
let cached = 0;
59+
let before: Snowflake | undefined;
60+
61+
while (cached < limit) {
62+
const batch = await channel.messages.fetch({
63+
limit: Math.min(100, limit - cached),
64+
before
65+
});
66+
if (batch.size === 0) break;
67+
68+
cached += cacheMessages(batch);
69+
before = batch.last()?.id;
70+
71+
if (batch.size < 100) break;
72+
}
73+
74+
return cached;
75+
}
76+
5077
export const DeletedMessagesListener: EventListener = {
78+
async clientReady(client) {
79+
const guild = await client.guilds.fetch(config.guildId);
80+
const channels = await guild.channels.fetch();
81+
82+
for (const channel of channels.values()) {
83+
if (!channel?.isTextBased() || isExcludedChannel(channel.id)) continue;
84+
85+
await messageFetcher.addToQueue(async () => {
86+
try {
87+
const cached = await fetchAndCacheMessages(channel, 200);
88+
logger.info(`Cached ${cached} messages from #${channel.name}`);
89+
} catch {
90+
// Skip channels we can't read (permissions)
91+
}
92+
});
93+
}
94+
},
95+
5196
messageCreate(_, message) {
5297
if (!message.inGuild()) return;
5398
cacheMessage(message);

src/modules/moderation/logs.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
} from "discord.js";
99
import { config } from "../../Config.js";
1010
import { logger } from "../../logging.js";
11+
import { createStandardEmbed } from "../../util/embeds.js";
1112
import { prettyPrintDuration } from "../../util/timespan.js";
1213
import { actualMention, fakeMention } from "../../util/users.js";
14+
import { upload } from "../pastify/pastify.js";
1315

1416
export interface CachedMessage {
1517
id: Snowflake;
@@ -164,6 +166,21 @@ export async function logModerationAction(
164166
});
165167
}
166168

169+
function formatMessageForPaste(message: CachedMessage): string {
170+
const timestamp = new Date(message.createdTimestamp).toISOString();
171+
const attachments =
172+
message.attachmentUrls.length > 0
173+
? `\nAttachments:\n${message.attachmentUrls.map((url) => ` - ${url}`).join("\n")}`
174+
: "";
175+
176+
return `Author: ${message.authorTag} (${message.authorId})
177+
Created: ${timestamp}
178+
Message ID: ${message.id}
179+
180+
Content:
181+
${message.content || "[No text content]"}${attachments}`;
182+
}
183+
167184
export async function logDeletedMessage(
168185
client: Client,
169186
message: CachedMessage,
@@ -174,33 +191,21 @@ export async function logDeletedMessage(
174191
return;
175192
}
176193

177-
const contentDisplay =
178-
message.content.slice(0, 1024) || "*[No text content]*";
194+
const pasteContent = formatMessageForPaste(message);
195+
const pasteUrl = await upload({ content: pasteContent });
179196

180-
const embed = new EmbedBuilder()
197+
const embed = createStandardEmbed(message.authorId)
181198
.setTitle("Message Deleted")
182199
.setColor("Grey")
183200
.setDescription(
184201
`**Author**: <@${message.authorId}> (${message.authorTag})\n` +
185202
`**Channel**: <#${message.channelId}>\n` +
186203
`**Created**: <t:${Math.round(message.createdTimestamp / 1000)}:R>\n\n` +
187-
`**Content**:\n${contentDisplay}`,
204+
`**Content**: [View on Paste](${pasteUrl})`,
188205
)
189206
.setFooter({ text: `Message ID: ${message.id}` })
190207
.setTimestamp();
191208

192-
if (message.attachmentUrls.length > 0) {
193-
const attachmentList =
194-
message.attachmentUrls.slice(0, 5).join("\n") +
195-
(message.attachmentUrls.length > 5
196-
? `\n... and ${message.attachmentUrls.length - 5} more`
197-
: "");
198-
embed.addFields({
199-
name: "Attachments",
200-
value: attachmentList,
201-
});
202-
}
203-
204209
await modLogChannel.send({ embeds: [embed] });
205210
}
206211

@@ -218,29 +223,23 @@ export async function logBulkDeletedMessages(
218223
// Sort by timestamp
219224
messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
220225

221-
// Build a summary - truncate if too many
222-
const maxDisplayed = 10;
223-
const displayed = messages.slice(0, maxDisplayed);
224-
const remaining = messages.length - maxDisplayed;
225-
226-
let messageList = displayed
227-
.map(
228-
(m) =>
229-
`**${m.authorTag}** (<t:${Math.round(m.createdTimestamp / 1000)}:t>): ${m.content.slice(0, 100) || "*[No text]*"}`,
230-
)
226+
// Format all messages for paste
227+
const pasteContent = messages
228+
.map((m, i) => {
229+
const separator = i > 0 ? "\n" + "─".repeat(50) + "\n\n" : "";
230+
return separator + formatMessageForPaste(m);
231+
})
231232
.join("\n");
232233

233-
if (remaining > 0) {
234-
messageList += `\n... and ${remaining} more messages`;
235-
}
234+
const pasteUrl = await upload({ content: pasteContent });
236235

237236
const embed = new EmbedBuilder()
238237
.setTitle("Bulk Messages Deleted")
239238
.setColor("DarkGrey")
240239
.setDescription(
241240
`**Channel**: <#${channelId}>\n` +
242241
`**Count**: ${messages.length} messages\n\n` +
243-
`**Messages**:\n${messageList.slice(0, 2000)}`,
242+
`**Messages**: [View on Paste](${pasteUrl})`,
244243
)
245244
.setTimestamp();
246245

src/modules/starboard/starboard.listener.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { config } from "../../Config.js";
1111
import { logger } from "../../logging.js";
1212
import { StarboardMessage } from "../../store/models/StarboardMessage.js";
1313
import { getMember } from "../../util/member.js";
14-
import { MessageFetcher } from "../../util/ratelimiting.js";
1514
import type { EventListener } from "../module.js";
1615
import {
1716
createStarboardMessage,
@@ -79,8 +78,6 @@ export const debounceStarboardReaction = (
7978
}, DEBOUNCE_DELAY);
8079
};
8180

82-
const messageFetcher = new MessageFetcher();
83-
8481
const getStarsFromEmbed: (embed: Embed) => number = (embed) => {
8582
const field = embed.fields.find((field) => field.name === "Details:");
8683
if (!field) return 0;
@@ -98,34 +95,7 @@ const isChannelBlacklisted = (channel: Channel): boolean => {
9895

9996
export const StarboardListener: EventListener = {
10097
async clientReady(client) {
101-
for (const guild of client.guilds.cache.values()) {
102-
try {
103-
const channels = await guild.channels.fetch();
104-
for (const channel of channels.values()) {
105-
if (
106-
channel?.isTextBased() &&
107-
channel.id !== config.starboard.channel &&
108-
!isChannelBlacklisted(channel)
109-
) {
110-
// Add to rate-limited queue
111-
await messageFetcher.addToQueue(async () => {
112-
try {
113-
await channel.messages.fetch({ limit: 100 }); // 100 is the maximum allowed by Discord API
114-
logger.info(`Fetched recent messages from #%s`, channel.name);
115-
} catch (error) {
116-
logger.error(
117-
`Error fetching messages from #%s`,
118-
channel.name,
119-
error,
120-
);
121-
}
122-
});
123-
}
124-
}
125-
} catch (error) {
126-
logger.error(`Error processing guild %s:`, guild.name, error);
127-
}
128-
}
98+
// Message fetching handled by DeletedMessagesListener - messages will be in Discord.js cache
12999
let isRunningStarboardCheck = false;
130100
schedule.scheduleJob(
131101
{

src/util/ratelimiting.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ export class MessageFetcher {
3232
this.processing = false;
3333
}
3434
}
35+
36+
/** Shared singleton instance for rate-limited message fetching */
37+
export const messageFetcher = new MessageFetcher();

0 commit comments

Comments
 (0)