Skip to content

Commit 7e681d6

Browse files
committed
Consolidate moderation logs and DM forwarding onto Components V2
Third of three commits collapsing the message layers. src/util/modlog.ts replaces five hand-built embeds across three event files. detectspam, invitefilter and detectcryptoscam each constructed their own near-identical entry — same colour, same author/description/Reason/footer shape — and each repeated the same modlog-channel resolution: const modlogId = current.modlog; const modlogChannel = message.guild.channels.cache.get(modlogId!); if (!modlogId || !modlogChannel || !modlogChannel.isTextBased()) return; That lookup was also subtly wrong in two of them: it sat before the mute / timeout log and returned early, so a guild with no modlog configured would skip the rest of the handler rather than just skip logging. sendModLog no-ops instead, and the caller carries on. Rendering notes: the embed author icon becomes a Section thumbnail, which is the closest V2 equivalent; the footer timestamp becomes Discord's <t:...:f> markup so it still localises per viewer. Entries without an avatar render as flat text with no Section. forwarding.ts: the DM forwarding embed becomes a container, with attachments as markdown links rather than embed fields. about.ts keeps its embed, deliberately, with a comment saying why: its stats are inline fields three to a row, and V2 has no field grid — faking one with padded text does not survive different client widths. It is now the only EmbedBuilder in the codebase, and the comment tells the next person that. This closes step 4. The three ways of sending a message are down to one, plus one documented exception: before Messages.* embeds (54 sites) + raw EmbedBuilder (11) + djsx widgets (12) after notices.* / modlog / plain container data, and /about Verified: tsc and eslint clean; modlog entries render the heading, body, reason, thumbnail and timestamp correctly in both the with-avatar and without-avatar shapes; no event file references EmbedBuilder any more; the loader still registers 10 commands, 3 components and 11 event listeners. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hDX4g1EjTN7424A5wnptE
1 parent f63d15f commit 7e681d6

6 files changed

Lines changed: 121 additions & 62 deletions

File tree

src/commands/about.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export default {
1515
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
1616
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel),
1717

18+
/**
19+
* The one place that still uses an embed rather than a Components V2
20+
* container. The stats below are laid out as inline fields, three to a row;
21+
* V2 has no field grid and faking one with padded text does not survive
22+
* different client widths. Everything else in the bot sends V2.
23+
*/
1824
async execute(interaction: ChatInputCommandInteraction) {
1925
await interaction.deferReply();
2026
const aboutEmbed = new EmbedBuilder();

src/events/detectcryptoscam.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {EmbedBuilder, Events, Message, PermissionFlagsBits} from "discord.js";
1+
import {Events, Message, PermissionFlagsBits} from "discord.js";
22
import {guildDB} from "../db";
3-
import Colors from "../util/colors";
3+
import {sendModLog} from "../util/modlog";
44

55

66
const TIMEOUT_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
@@ -55,17 +55,14 @@ export default {
5555

5656

5757
if (didTimeout) {
58-
const modlogId = current.modlog;
59-
const modlogChannel = message.guild.channels.cache.get(modlogId!);
60-
if (!modlogId || !modlogChannel || !modlogChannel.isTextBased()) return; // Can't log
61-
62-
const mEmbed = new EmbedBuilder().setColor(Colors.Info)
63-
.setAuthor({name: "Member Timed Out", iconURL: message.author.displayAvatarURL()})
64-
.setDescription(`${message.author.displayName} ${message.author.tag}`)
65-
.addFields({name: "Reason", value: "Detected Crypto Scam"})
66-
.setFooter({text: `ID: ${message.author.id}`}).setTimestamp(message.createdTimestamp);
67-
68-
await modlogChannel.send({embeds: [mEmbed]});
58+
await sendModLog(message.guild, current.modlog, {
59+
heading: "Member Timed Out",
60+
iconUrl: message.author.displayAvatarURL(),
61+
body: `${message.author.displayName} ${message.author.tag}`,
62+
reason: "Detected Crypto Scam",
63+
userId: message.author.id,
64+
at: message.createdTimestamp
65+
});
6966
}
7067
},
7168
};

src/events/detectspam.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {EmbedBuilder, Events, Message, PermissionFlagsBits} from "discord.js";
1+
import {Events, Message, PermissionFlagsBits} from "discord.js";
22
import {guildDB} from "../db";
3-
import Colors from "../util/colors";
3+
import {sendModLog} from "../util/modlog";
44

55

66
const fakeDiscordRegex = new RegExp(`([a-zA-Z-\\.]+)?d[il][il]?scorr?(cl|[ldb])([a-zA-Z-\\.]+)?\\.(com|net|app|gift|ru|uk)`, "ig");
@@ -64,26 +64,24 @@ export default {
6464
}
6565
}
6666

67-
const modlogId = current.modlog;
68-
const modlogChannel = message.guild.channels.cache.get(modlogId!);
69-
if (!modlogId || !modlogChannel || !modlogChannel.isTextBased()) return; // Can't log
70-
71-
const dEmbed = new EmbedBuilder().setColor(Colors.Info)
72-
.setAuthor({name: message.author.username, iconURL: message.author.displayAvatarURL()})
73-
.setDescription(`Message sent by ${message.author.username} in ${message.channel.name}\n\n` + message.content)
74-
.addFields({name: "Reason", value: reason})
75-
.setFooter({text: `ID: ${message.author.id}`}).setTimestamp(message.createdTimestamp);
76-
await modlogChannel.send({embeds: [dEmbed]});
77-
67+
await sendModLog(message.guild, current.modlog, {
68+
heading: message.author.username,
69+
iconUrl: message.author.displayAvatarURL(),
70+
body: `Message sent by ${message.author.username} in ${message.channel.name}\n\n${message.content}`,
71+
reason,
72+
userId: message.author.id,
73+
at: message.createdTimestamp
74+
});
7875

7976
if (didMute) {
80-
const mEmbed = new EmbedBuilder().setColor(Colors.Info)
81-
.setAuthor({name: "Member Muted", iconURL: message.author.displayAvatarURL()})
82-
.setDescription(`${message.author.displayName} ${message.author.tag}`)
83-
.addFields({name: "Reason", value: reason})
84-
.setFooter({text: `ID: ${message.author.id}`}).setTimestamp(message.createdTimestamp);
85-
86-
await modlogChannel.send({embeds: [mEmbed]});
77+
await sendModLog(message.guild, current.modlog, {
78+
heading: "Member Muted",
79+
iconUrl: message.author.displayAvatarURL(),
80+
body: `${message.author.displayName} ${message.author.tag}`,
81+
reason,
82+
userId: message.author.id,
83+
at: message.createdTimestamp
84+
});
8785
}
8886
},
8987
};

src/events/forwarding.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {EmbedBuilder, Events, type Message} from "discord.js";
1+
import {Events, MessageFlags, type Message} from "discord.js";
2+
import {container, text} from "../framework";
3+
import {Accents} from "../util/colors";
24
import {globalDB} from "../db";
35

46

@@ -18,16 +20,18 @@ export default {
1820
const user = message.client.users.cache.get(target);
1921
if (!user) return;
2022

21-
const embed = new EmbedBuilder()
22-
.setAuthor({name: `${message.author.displayName} (${message.author.id})`, iconURL: message.author.displayAvatarURL()})
23-
.setDescription(message.content ?? "\u200B");
23+
const lines = [
24+
`### ${message.author.displayName} (${message.author.id})`,
25+
message.content || "\u200B"
26+
];
2427

25-
if (message.attachments.size) {
26-
for (const [id, att] of message.attachments) {
27-
embed.addFields({name: att.name, value: `[${id}](${att.url})`});
28-
}
28+
for (const [id, attachment] of message.attachments) {
29+
lines.push(`**${attachment.name}** — [${id}](${attachment.url})`);
2930
}
3031

31-
await user.send({embeds: [embed]});
32+
await user.send({
33+
flags: MessageFlags.IsComponentsV2,
34+
components: [container(lines.map(text), {accentColor: Accents.Info})]
35+
});
3236
},
3337
};

src/events/invitefilter.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {EmbedBuilder, Events, PermissionFlagsBits, type Message} from "discord.js";
1+
import {Events, PermissionFlagsBits, type Message} from "discord.js";
22
import {guildDB} from "../db";
3-
import Colors from "../util/colors";
3+
import {sendModLog} from "../util/modlog";
44

55

66

@@ -54,26 +54,25 @@ export default {
5454
}
5555
}
5656

57-
const modlogId = current.modlog;
58-
const modlogChannel = message.guild.channels.cache.get(modlogId!);
59-
if (!modlogId || !modlogChannel || !modlogChannel.isTextBased()) return; // Can't log
60-
61-
const dEmbed = new EmbedBuilder().setColor(Colors.Info)
62-
.setAuthor({name: message.author.username, iconURL: message.author.displayAvatarURL()})
63-
.setDescription(`Message sent by ${message.author.username} in ${message.channel.name}\n\n` + message.content)
64-
.addFields({name: "Reason", value: "Discord Invite"})
65-
.setFooter({text: `ID: ${message.author.id}`}).setTimestamp(message.createdTimestamp);
66-
await modlogChannel.send({embeds: [dEmbed]});
67-
57+
const reason = "Discord Invite";
58+
await sendModLog(message.guild, current.modlog, {
59+
heading: message.author.username,
60+
iconUrl: message.author.displayAvatarURL(),
61+
body: `Message sent by ${message.author.username} in ${message.channel.name}\n\n${message.content}`,
62+
reason,
63+
userId: message.author.id,
64+
at: message.createdTimestamp
65+
});
6866

6967
if (didMute) {
70-
const mEmbed = new EmbedBuilder().setColor(Colors.Info)
71-
.setAuthor({name: "Member Muted", iconURL: message.author.displayAvatarURL()})
72-
.setDescription(`${message.author.displayName} ${message.author.tag}`)
73-
.addFields({name: "Reason", value: "Discord Invite"})
74-
.setFooter({text: `ID: ${message.author.id}`}).setTimestamp(message.createdTimestamp);
75-
76-
await modlogChannel.send({embeds: [mEmbed]});
68+
await sendModLog(message.guild, current.modlog, {
69+
heading: "Member Muted",
70+
iconUrl: message.author.displayAvatarURL(),
71+
body: `${message.author.displayName} ${message.author.tag}`,
72+
reason,
73+
userId: message.author.id,
74+
at: message.createdTimestamp
75+
});
7776
}
7877
},
7978
};

src/util/modlog.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Moderation log entries.
3+
*
4+
* detectspam, invitefilter and detectcryptoscam each built their own
5+
* near-identical embeds and each repeated the same channel-resolution dance.
6+
* One helper now covers both entry shapes and the lookup.
7+
*/
8+
9+
import {ComponentType, MessageFlags, type ComponentInContainerData, type Guild, type TextDisplayComponentData} from "discord.js";
10+
import {container, text, type ComponentMessage} from "../framework";
11+
import {Accents} from "./colors";
12+
13+
14+
export interface ModLogEntry {
15+
/** Heading line: the offending user, or the action taken. */
16+
heading: string;
17+
/** Avatar shown alongside the entry, as the embed author icon used to be. */
18+
iconUrl?: string;
19+
body: string;
20+
reason: string;
21+
userId: string;
22+
/** Milliseconds; rendered as Discord's own per-viewer localised timestamp. */
23+
at: number;
24+
}
25+
26+
27+
export function modLogMessage(entry: ModLogEntry): ComponentMessage {
28+
const lines: TextDisplayComponentData[] = [
29+
{type: ComponentType.TextDisplay, content: `### ${entry.heading}`},
30+
{type: ComponentType.TextDisplay, content: entry.body || "​"},
31+
{type: ComponentType.TextDisplay, content: `**Reason:** ${entry.reason}`}
32+
];
33+
34+
// A Section with a thumbnail is the closest V2 has to an embed author icon.
35+
const body: ComponentInContainerData[] = entry.iconUrl
36+
? [{type: ComponentType.Section, components: lines, accessory: {type: ComponentType.Thumbnail, media: {url: entry.iconUrl}}}]
37+
: [...lines];
38+
39+
body.push(text(`-# ID: ${entry.userId} • <t:${Math.floor(entry.at / 1000)}:f>`));
40+
41+
return {
42+
flags: MessageFlags.IsComponentsV2,
43+
components: [container(body, {accentColor: Accents.Info})]
44+
};
45+
}
46+
47+
48+
/** Posts to the guild's configured modlog channel. Silently no-ops if unset. */
49+
export async function sendModLog(guild: Guild, channelId: string | undefined, entry: ModLogEntry): Promise<void> {
50+
if (!channelId) return;
51+
const channel = guild.channels.cache.get(channelId);
52+
if (!channel?.isTextBased()) return;
53+
54+
await channel.send(modLogMessage(entry));
55+
}

0 commit comments

Comments
 (0)