Add reaction roles, autoresponder, moderation actions, and action log#3
Add reaction roles, autoresponder, moderation actions, and action log#3
Conversation
…on log Agent-Logs-Url: https://github.com/BetterDiscord/BetterDiscordBot/sessions/c9e5628b-6776-4bf2-8736-3caa5d142525 Co-authored-by: zerebos <6865942+zerebos@users.noreply.github.com>
|
@copilot can you make the action log configurable in some way? We don't always want to log all events. For example we might not keep join/leave logs on |
Agent-Logs-Url: https://github.com/BetterDiscord/BetterDiscordBot/sessions/7f9c7049-cf46-4e7c-b033-be83da127060 Co-authored-by: zerebos <6865942+zerebos@users.noreply.github.com>
Done in commit
All events default to enabled (opt-out model), so existing setups aren't affected. Omitting the |
There was a problem hiding this comment.
Pull request overview
Adds missing Dyno-like moderation features to the bot: reaction roles, an autoresponder module, a richer moderation command set (including warnings), and a configurable “action log” channel with multiple event types.
Changes:
- Introduces new DB-backed modules for reaction roles, autoresponder triggers, and per-user warning history.
- Adds
/reactionroles,/autoresponder, and/modcommands, plus extends/moderationwith action log configuration. - Implements new event handlers for reaction roles, autoresponder replies, and action-log event emission; updates client intents/partials and fixes event loader to support array-export event modules.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/base.ts | Extends GuildSettings and adds new feature types (reaction roles, autoresponder entries, warnings, action log event union). |
| src/index.ts | Adds intents/partials needed for new features and fixes event registration for array-export event modules. |
| src/db.ts | Adds new Keyv namespaces for reaction roles, autoresponder, and warnings storage. |
| src/events/reactionroles.ts | Adds reaction add/remove handlers to assign/unassign roles based on configured bindings. |
| src/events/autoresponder.ts | Adds message-create handler to match triggers and send configured responses. |
| src/events/actionlog.ts | Adds action log event handlers (message delete/edit, ban/unban, nickname and role changes). |
| src/commands/reactionroles.ts | Adds /reactionroles add/remove/list for managing bindings and self-reacting for discoverability. |
| src/commands/autoresponder.ts | Adds /autoresponder toggle/add/remove/list plus autocomplete for removal. |
| src/commands/moderation.ts | Adds /moderation actionlog and /moderation actionlogevents settings management. |
| src/commands/mod.ts | Adds /mod moderation actions and SQLite-backed warnings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const member = interaction.guild.members.cache.get(user.id); | ||
|
|
||
| if (!member) return await interaction.reply(Messages.error("That user is not in this server.", {ephemeral: true})); | ||
| if (!member.kickable) return await interaction.reply(Messages.error("I cannot kick that user. They may have a higher role than me.", {ephemeral: true})); |
There was a problem hiding this comment.
interaction.guild.members.cache.get(user.id) can be undefined even when the user is in the guild (member not cached), causing false "user is not in this server" errors and preventing kick/timeout/untimeout. Prefer interaction.options.getMember("user") (for cached interactions) or await interaction.guild.members.fetch(user.id) and reuse that for capability checks like kickable/moderatable.
| data: new SlashCommandBuilder() | ||
| .setName("mod") | ||
| .setDescription("Moderation actions.") | ||
| .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) |
There was a problem hiding this comment.
The command is gated by ModerateMembers, but the subcommands include kick/ban/unban which are separate permissions. As written, members who can timeout (but cannot ban/kick) may still be able to run these subcommands successfully. Add explicit permission checks per subcommand (e.g. KickMembers, BanMembers, ModerateMembers) and/or restructure commands so the default permission matches the most sensitive action.
| .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) | |
| .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) |
| async clearwarnings(interaction: ChatInputCommandInteraction<"cached">) { | ||
| const user = interaction.options.getUser("user", true); | ||
| const key = `${interaction.guild.id}-${user.id}`; | ||
| await warningsDB.delete(key); | ||
| await interaction.reply(Messages.success(`Cleared all warnings for **${user.tag}**.`, {ephemeral: true})); | ||
| }, |
There was a problem hiding this comment.
PR description says all moderation actions emit to the modlog, but clearwarnings currently only deletes from storage and replies to the moderator. Consider logging this action (including target user and moderator) via logModAction for consistency/auditability.
| const emojiInput = interaction.options.getString("emoji", true); | ||
| const role = interaction.options.getRole("role", true); | ||
| const normalizedEmoji = normalizeEmojiString(emojiInput); | ||
|
|
There was a problem hiding this comment.
Before storing the binding, validate that the bot can actually assign the selected role (e.g. role.editable / role not managed). Otherwise /reactionroles add can succeed but reaction handling will always fail at runtime when attempting member.roles.add(...).
| if (role.managed || !role.editable) { | |
| return await interaction.editReply(Messages.error("I can't assign that role. Please choose a non-managed role that is below my highest role.")); | |
| } |
| if (message.content) embed.setDescription(message.content.substring(0, 4096)); | ||
|
|
||
| await logChannel.send({embeds: [embed]}).catch(console.error); | ||
| }, |
There was a problem hiding this comment.
These log messages may include user-generated content (message text, nicknames) containing mentions; channel.send({embeds:[...]}) can therefore ping @everyone/roles/users in the action log. Consider sending with allowedMentions: { parse: [] } to make logs non-pinging.
|
|
||
| if (addedRoles.size) embed.addFields({name: "Roles Added", value: addedRoles.map(r => `<@&${r.id}>`).join(", ")}); | ||
| if (removedRoles.size) embed.addFields({name: "Roles Removed", value: removedRoles.map(r => `<@&${r.id}>`).join(", ")}); | ||
|
|
||
| await logChannel.send({embeds: [embed]}).catch(console.error); |
There was a problem hiding this comment.
Roles Added/Removed field values are built by joining all role mentions, which can exceed Discord's 1024-char embed field limit (causing the send to fail and the log event to be dropped). Consider truncating/splitting into multiple fields when the list is long.
| const description = current | ||
| .map(e => `**\`${e.trigger}\`** (${e.matchType})\n↳ ${e.response.substring(0, 80)}${e.response.length > 80 ? "…" : ""}`) | ||
| .join("\n\n"); | ||
|
|
||
| const embed = new EmbedBuilder() | ||
| .setColor(Colors.Info) | ||
| .setTitle("Auto-Responses") | ||
| .setDescription(description); | ||
|
|
There was a problem hiding this comment.
The embed description is built from all triggers and can exceed Discord's 4096-char embed description limit in larger configs, causing the response to fail. Consider paging (you already have src/paginator.ts) or truncating the list output.
| ) | ||
| .setTimestamp(); | ||
|
|
||
| await logChannel.send({embeds: [embed]}).catch(console.error); |
There was a problem hiding this comment.
When sending modlog embeds, consider setting allowedMentions: { parse: [] } (and optionally disable replying-user mentions elsewhere) so user-provided reason text or IDs in fields can’t trigger @everyone/role/user pings in the log channel.
| await logChannel.send({embeds: [embed]}).catch(console.error); | |
| await logChannel.send({embeds: [embed], allowedMentions: {parse: []}}).catch(console.error); |
| const description = current | ||
| .map(r => `${r.emoji} → <@&${r.roleId}> (in <#${r.channelId}>, msg: \`${r.messageId}\`)`) | ||
| .join("\n"); |
There was a problem hiding this comment.
For custom emojis you store only the numeric ID (normalizeEmojiString), so the list output will show something like 1234567890 → @Role instead of rendering the emoji. Consider storing a display form too (original input) or resolving the emoji from interaction.guild.emojis.cache.get(id) when building the list.
|
|
||
| if (!match) return; | ||
|
|
||
| await message.reply(match.response).catch(console.error); |
There was a problem hiding this comment.
message.reply(match.response) will ping the author by default and will also allow @everyone/role/user mentions contained in stored responses. Consider using allowedMentions: { parse: [], repliedUser: false } (or a more targeted policy) to prevent mention spam/abuse.
| await message.reply(match.response).catch(console.error); | |
| await message.reply({ | |
| content: match.response, | |
| allowedMentions: { | |
| parse: [], | |
| repliedUser: false, | |
| }, | |
| }).catch(console.error); |
Replaces dyno.gg functionality the server currently depends on. Four feature areas were missing or incomplete: reaction roles, autoresponder, moderation actions, and action log.
Reaction Roles (
/reactionroles)add <channel> <message_id> <emoji> <role>— binds emoji→role on a message; bot self-reacts for discoverabilityremove/listAutoresponder (
/autoresponder)toggle— per-guild on/offadd <trigger> <response> [match_type]— three match modes:contains(default),exact,startsWithremove(with autocomplete) /listModeration Actions (
/mod)kick,ban,unban,timeout,untimeout— withkickable/bannable/moderatableguardswarn,warnings,clearwarnings— SQLite-backed warning history per guild+userAction Log (
/moderation actionlog+/moderation actionlogevents)actionlogchannel setting (separate frommodlog)/moderation actionlogevents <event> [enable]— choose frommessage_delete,message_edit,member_ban,member_unban,nickname_change,role_changeenablereports the current state for that eventoldMemberis partial to avoid false positivesInfrastructure
src/index.ts: fixed silent bug where array-export event files (e.g. existingjoinleave.ts) were never registered; addedGuildMessageReactions,GuildModerationintents andPartials.Message,Partials.Reactionsrc/types/base.ts: addedReactionRole,AutoResponderEntry,Warningtypes; addedActionLogEventunion type andactionlogEventsper-event toggle map; extendedGuildSettingswithactionlog,actionlogEvents, andautorespondersrc/db.ts: addedreactionrolesDB,autoresponderDB,warningsDBOriginal prompt
Created from VS Code.