Add reaction roles, autoresponder, moderation actions, and action log#3
Add reaction roles, autoresponder, moderation actions, and action log#3Copilot wants to merge 3 commits into
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); |
|
/gemini review the current version |
There was a problem hiding this comment.
Code Review
This pull request introduces several new features to the Discord bot, including an autoresponder module, comprehensive moderation commands (kick, ban, timeout, warnings), an action logging system for server events, and reaction roles. The database, types, and main entry point are updated to support these additions. Feedback on the changes highlights a few critical issues: first, partial guild members can trigger false-positive nickname change logs due to undefined properties; second, relying on synchronous member cache lookups in moderation commands can fail for uncached members, so asynchronous fetching should be used instead; and third, checking bot on a partial user in reaction roles can bypass bot checks, which can be resolved by checking it after the user is fully fetched.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async execute(oldMember: GuildMember | PartialGuildMember, newMember: GuildMember) { | ||
| // Nickname changes | ||
| if (oldMember.nickname !== newMember.nickname) { | ||
| const logChannel = await getLogChannel(newMember.guild, "nickname_change"); | ||
| if (logChannel) { | ||
| const embed = new EmbedBuilder() | ||
| .setColor(Colors.Info) | ||
| .setTitle("Nickname Changed") | ||
| .addFields( | ||
| {name: "User", value: `<@${newMember.user.id}> (${newMember.user.tag})`, inline: true}, | ||
| {name: "Before", value: oldMember.nickname ?? "*None*", inline: true}, | ||
| {name: "After", value: newMember.nickname ?? "*None*", inline: true} | ||
| ) | ||
| .setFooter({text: `User ID: ${newMember.user.id}`}) | ||
| .setTimestamp(); | ||
|
|
||
| await logChannel.send({embeds: [embed]}).catch(console.error); | ||
| } | ||
| } | ||
|
|
||
| // Role changes — only reliable when the old member was fully cached | ||
| if (!oldMember.partial) { | ||
| const addedRoles = newMember.roles.cache.filter(r => !oldMember.roles.cache.has(r.id)); | ||
| const removedRoles = oldMember.roles.cache.filter(r => !newMember.roles.cache.has(r.id)); | ||
|
|
||
| if (addedRoles.size || removedRoles.size) { | ||
| const logChannel = await getLogChannel(newMember.guild, "role_change"); | ||
| if (logChannel) { | ||
| const embed = new EmbedBuilder() | ||
| .setColor(Colors.Info) | ||
| .setTitle("Member Roles Updated") | ||
| .addFields( | ||
| {name: "User", value: `<@${newMember.user.id}> (${newMember.user.tag})`, inline: true} | ||
| ) | ||
| .setFooter({text: `User ID: ${newMember.user.id}`}) | ||
| .setTimestamp(); | ||
|
|
||
| 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.
When oldMember is a partial guild member, properties like nickname are undefined. If the member has no nickname, newMember.nickname is null. Comparing undefined !== null evaluates to true, which triggers a false-positive 'Nickname Changed' log (showing *None* to *None*). Additionally, any update to an uncached member will log a false nickname change from *None* to their current nickname.
Since we cannot reliably diff nicknames or roles for partial members, we should return early if oldMember.partial is true. This also allows us to simplify the code by removing the nested !oldMember.partial check for roles.
async execute(oldMember: GuildMember | PartialGuildMember, newMember: GuildMember) {
if (oldMember.partial) return;
// Nickname changes
if (oldMember.nickname !== newMember.nickname) {
const logChannel = await getLogChannel(newMember.guild, "nickname_change");
if (logChannel) {
const embed = new EmbedBuilder()
.setColor(Colors.Info)
.setTitle("Nickname Changed")
.addFields(
{name: "User", value: `<@${newMember.user.id}> (${newMember.user.tag})`, inline: true},
{name: "Before", value: oldMember.nickname ?? "*None*", inline: true},
{name: "After", value: newMember.nickname ?? "*None*", inline: true}
)
.setFooter({text: `User ID: ${newMember.user.id}`})
.setTimestamp();
await logChannel.send({embeds: [embed]}).catch(console.error);
}
}
// Role changes
const addedRoles = newMember.roles.cache.filter(r => !oldMember.roles.cache.has(r.id));
const removedRoles = oldMember.roles.cache.filter(r => !newMember.roles.cache.has(r.id));
if (addedRoles.size || removedRoles.size) {
const logChannel = await getLogChannel(newMember.guild, "role_change");
if (logChannel) {
const embed = new EmbedBuilder()
.setColor(Colors.Info)
.setTitle("Member Roles Updated")
.addFields(
{name: "User", value: `<@${newMember.user.id}> (${newMember.user.tag})`, inline: true}
)
.setFooter({text: `User ID: ${newMember.user.id}`})
.setTimestamp();
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);
}
}
},| async kick(interaction: ChatInputCommandInteraction<"cached">) { | ||
| const user = interaction.options.getUser("user", true); | ||
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | ||
| 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.
Using interaction.guild.members.cache.get(user.id) can return undefined if the member is not currently cached, even if they are in the server. This will cause the bot to incorrectly claim 'That user is not in this server.' for uncached members. Fetching the member asynchronously is much more reliable.
| async kick(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| 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})); | |
| async kick(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| const member = await interaction.guild.members.fetch(user.id).catch(() => null); | |
| 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})); |
| async ban(interaction: ChatInputCommandInteraction<"cached">) { | ||
| const user = interaction.options.getUser("user", true); | ||
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | ||
| const deleteDays = interaction.options.getInteger("delete_days") ?? 0; | ||
| const member = interaction.guild.members.cache.get(user.id); | ||
|
|
||
| if (member && !member.bannable) return await interaction.reply(Messages.error("I cannot ban that user. They may have a higher role than me.", {ephemeral: true})); |
There was a problem hiding this comment.
Using interaction.guild.members.cache.get(user.id) can return undefined if the member is not currently cached, which would bypass the hierarchy check (member && !member.bannable) and attempt to ban them anyway. Fetching the member asynchronously ensures the hierarchy check is always performed correctly.
| async ban(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| const deleteDays = interaction.options.getInteger("delete_days") ?? 0; | |
| const member = interaction.guild.members.cache.get(user.id); | |
| if (member && !member.bannable) return await interaction.reply(Messages.error("I cannot ban that user. They may have a higher role than me.", {ephemeral: true})); | |
| async ban(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| const deleteDays = interaction.options.getInteger("delete_days") ?? 0; | |
| const member = await interaction.guild.members.fetch(user.id).catch(() => null); | |
| if (member && !member.bannable) return await interaction.reply(Messages.error("I cannot ban that user. They may have a higher role than me.", {ephemeral: true})); |
| async timeout(interaction: ChatInputCommandInteraction<"cached">) { | ||
| const user = interaction.options.getUser("user", true); | ||
| const durationMinutes = interaction.options.getInteger("duration", true); | ||
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | ||
| 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.moderatable) return await interaction.reply(Messages.error("I cannot timeout that user. They may have a higher role than me.", {ephemeral: true})); |
There was a problem hiding this comment.
Using interaction.guild.members.cache.get(user.id) can return undefined if the member is not currently cached, even if they are in the server. This will cause the bot to incorrectly claim 'That user is not in this server.' for uncached members. Fetching the member asynchronously is much more reliable.
| async timeout(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const durationMinutes = interaction.options.getInteger("duration", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| 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.moderatable) return await interaction.reply(Messages.error("I cannot timeout that user. They may have a higher role than me.", {ephemeral: true})); | |
| async timeout(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const durationMinutes = interaction.options.getInteger("duration", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| const member = await interaction.guild.members.fetch(user.id).catch(() => null); | |
| if (!member) return await interaction.reply(Messages.error("That user is not in this server.", {ephemeral: true})); | |
| if (!member.moderatable) return await interaction.reply(Messages.error("I cannot timeout that user. They may have a higher role than me.", {ephemeral: true})); |
| async untimeout(interaction: ChatInputCommandInteraction<"cached">) { | ||
| const user = interaction.options.getUser("user", true); | ||
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | ||
| 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})); |
There was a problem hiding this comment.
Using interaction.guild.members.cache.get(user.id) can return undefined if the member is not currently cached, even if they are in the server. This will cause the bot to incorrectly claim 'That user is not in this server.' for uncached members. Fetching the member asynchronously is much more reliable.
| async untimeout(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| 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})); | |
| async untimeout(interaction: ChatInputCommandInteraction<"cached">) { | |
| const user = interaction.options.getUser("user", true); | |
| const reason = interaction.options.getString("reason") ?? "No reason provided"; | |
| const member = await interaction.guild.members.fetch(user.id).catch(() => null); | |
| if (!member) return await interaction.reply(Messages.error("That user is not in this server.", {ephemeral: true})); |
| if (rawUser.bot) return; | ||
|
|
||
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | ||
| if (!reaction) return; | ||
|
|
||
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | ||
| if (!user) return; |
There was a problem hiding this comment.
If rawUser is a PartialUser, the bot property may be undefined. Checking rawUser.bot before fetching can allow bot reactions to bypass the check and trigger reaction roles. It is safer to check user.bot after ensuring the user object is fully fetched.
| if (rawUser.bot) return; | |
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | |
| if (!reaction) return; | |
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | |
| if (!user) return; | |
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | |
| if (!reaction) return; | |
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | |
| if (!user || user.bot) return; |
| if (rawUser.bot) return; | ||
|
|
||
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | ||
| if (!reaction) return; | ||
|
|
||
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | ||
| if (!user) return; |
There was a problem hiding this comment.
If rawUser is a PartialUser, the bot property may be undefined. Checking rawUser.bot before fetching can allow bot reactions to bypass the check and trigger reaction roles. It is safer to check user.bot after ensuring the user object is fully fetched.
| if (rawUser.bot) return; | |
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | |
| if (!reaction) return; | |
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | |
| if (!user) return; | |
| const reaction = rawReaction.partial ? await rawReaction.fetch().catch(() => null) : rawReaction; | |
| if (!reaction) return; | |
| const user = rawUser.partial ? await rawUser.fetch().catch(() => null) : rawUser; | |
| if (!user || user.bot) return; |
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.