Skip to content

Add reaction roles, autoresponder, moderation actions, and action log#3

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/add-functionality-to-discord-mod
Draft

Add reaction roles, autoresponder, moderation actions, and action log#3
Copilot wants to merge 3 commits intomainfrom
copilot/add-functionality-to-discord-mod

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 24, 2026

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 discoverability
  • remove / list
  • Handles Unicode and custom emoji via stable ID-based normalisation; fetches partial reaction/message/user objects so it works on uncached messages

Autoresponder (/autoresponder)

  • toggle — per-guild on/off
  • add <trigger> <response> [match_type] — three match modes: contains (default), exact, startsWith
  • remove (with autocomplete) / list

Moderation Actions (/mod)

  • kick, ban, unban, timeout, untimeout — with kickable/bannable/moderatable guards
  • warn, warnings, clearwarnings — SQLite-backed warning history per guild+user
  • All actions emit to the existing modlog channel

Action Log (/moderation actionlog + /moderation actionlogevents)

  • New actionlog channel setting (separate from modlog)
  • Events logged: message delete/edit (before/after content), ban/unban, nickname change, role add/remove
  • Each event type is individually configurable via /moderation actionlogevents <event> [enable] — choose from message_delete, message_edit, member_ban, member_unban, nickname_change, role_change
  • All events default to enabled (opt-out model); omitting enable reports the current state for that event
  • Role-change diffing skipped when oldMember is partial to avoid false positives

Infrastructure

  • src/index.ts: fixed silent bug where array-export event files (e.g. existing joinleave.ts) were never registered; added GuildMessageReactions, GuildModeration intents and Partials.Message, Partials.Reaction
  • src/types/base.ts: added ReactionRole, AutoResponderEntry, Warning types; added ActionLogEvent union type and actionlogEvents per-event toggle map; extended GuildSettings with actionlog, actionlogEvents, and autoresponder
  • src/db.ts: added reactionrolesDB, autoresponderDB, warningsDB
Original prompt

Add more functionality to this Discord mod. We are looking to move off of dyno.gg. From that bot we currently use: reaction roles, automod, autoresponder, moderation, and action log. For some of those this bot has no equivalent, for others (like moderation and automod) it is woefully incomplete.

Created from VS Code.

Copilot AI changed the title [WIP] Add functionality to Discord mod to replace dyno.gg features Add reaction roles, autoresponder, moderation actions, and action log Apr 24, 2026
Copilot AI requested a review from zerebos April 24, 2026 04:00
@zerebos
Copy link
Copy Markdown
Member

zerebos commented Apr 24, 2026

@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

Copy link
Copy Markdown
Author

Copilot AI commented Apr 24, 2026

@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

Done in commit 3312ccb. Added a new /moderation actionlogevents <event> [enable] subcommand with a fixed choices dropdown covering all six logged event types:

  • Message Deleted (message_delete)
  • Message Edited (message_edit)
  • Member Banned (member_ban)
  • Member Unbanned (member_unban)
  • Nickname Changed (nickname_change)
  • Role Changes (role_change)

All events default to enabled (opt-out model), so existing setups aren't affected. Omitting the enable option reports the current state for that event type. Each event is checked independently in the handler, so nickname and role-change events can be toggled separately despite sharing the same Discord event.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /mod commands, plus extends /moderation with 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.

Comment thread src/commands/mod.ts
Comment on lines +93 to +96
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}));
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/commands/mod.ts
data: new SlashCommandBuilder()
.setName("mod")
.setDescription("Moderation actions.")
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)

Copilot uses AI. Check for mistakes.
Comment thread src/commands/mod.ts
Comment on lines +216 to +221
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}));
},
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const emojiInput = interaction.options.getString("emoji", true);
const role = interaction.options.getRole("role", true);
const normalizedEmoji = normalizeEmojiString(emojiInput);

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...).

Suggested change
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."));
}

Copilot uses AI. Check for mistakes.
Comment thread src/events/actionlog.ts
Comment on lines +38 to +41
if (message.content) embed.setDescription(message.content.substring(0, 4096));

await logChannel.send({embeds: [embed]}).catch(console.error);
},
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/events/actionlog.ts
Comment on lines +149 to +153

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);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +109
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);

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/commands/mod.ts
)
.setTimestamp();

await logChannel.send({embeds: [embed]}).catch(console.error);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
await logChannel.send({embeds: [embed]}).catch(console.error);
await logChannel.send({embeds: [embed], allowedMentions: {parse: []}}).catch(console.error);

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +126
const description = current
.map(r => `${r.emoji} → <@&${r.roleId}> (in <#${r.channelId}>, msg: \`${r.messageId}\`)`)
.join("\n");
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

if (!match) return;

await message.reply(match.response).catch(console.error);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
await message.reply(match.response).catch(console.error);
await message.reply({
content: match.response,
allowedMentions: {
parse: [],
repliedUser: false,
},
}).catch(console.error);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants