diff --git a/docs/commands.md b/docs/commands.md index dcd6bb3..a6d39f2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -70,6 +70,12 @@ Edit your own previous reply sent with `!reply`. Delete your own previous reply sent with `!reply`. `` is the message number shown in front of staff replies in the thread channel. +### `!role` +View the role that is sent with your replies + +### `!role ` +Change the role that is sent with your replies to any role you currently have + ### `!loglink` Get a link to the open Modmail thread's log. diff --git a/docs/configuration.md b/docs/configuration.md index 5d46811..9e9a4a0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,6 +102,10 @@ If enabled, staff members can edit their own replies in modmail threads with `!e If enabled, snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. E.g. `!r Hello! {{rules}}` +#### allowChangingDisplayedRole +**Default:** `on` +If enabled, moderators can change the role that's shown with their replies to any role they currently have using the `!role` command. + See [inlineSnippetStart](#inlineSnippetStart) and [inlineSnippetEnd](#inlineSnippetEnd) to customize the symbols used. #### alwaysReply diff --git a/src/data/Thread.js b/src/data/Thread.js index fd09a63..9f151f4 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -14,6 +14,8 @@ const ThreadMessage = require("./ThreadMessage"); const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = require("./constants"); +const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; + /** * @property {String} id * @property {Number} status @@ -193,8 +195,7 @@ class Thread { */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; - const mainRole = utils.getMainRole(moderator); - const roleName = mainRole ? mainRole.name : null; + const roleName = this.getModeratorDisplayRoleName(moderator); if (config.allowInlineSnippets) { // Replace {{snippet}} with the corresponding snippet @@ -837,6 +838,43 @@ class Thread { }); } + setModeratorRoleOverride(moderatorId, roleId) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + moderatorRoleOverrides[moderatorId] = roleId; + return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides); + } + + resetModeratorRoleOverride(moderatorId) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + delete moderatorRoleOverrides[moderatorId]; + return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides) + } + + /** + * Get the role that is shown in the replies of the specified moderator, + * taking role overrides into account. + * @param {Eris.Member} moderator + * @return {Eris.Role|undefined} + */ + getModeratorDisplayRole(moderator) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + const overrideRoleId = moderatorRoleOverrides[moderator.id]; + const overrideRole = overrideRoleId && moderator.roles.includes(overrideRoleId) && utils.getInboxGuild().roles.get(overrideRoleId); + const finalRole = overrideRole || utils.getMainRole(moderator); + return finalRole; + } + + /** + * Get the role NAME that is shown in the replies of the specified moderator, + * taking role overrides into account. + * @param {Eris.Member} moderator + * @return {Eris.Role|undefined} + */ + getModeratorDisplayRoleName(moderator) { + const displayRole = this.getModeratorDisplayRole(moderator); + return displayRole ? displayRole.name : null; + } + /** * @param {string} key * @param {*} value diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index c23e3c1..438d61f 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -60,6 +60,7 @@ * @property {string} [inlineSnippetStart="{{"] * @property {string} [inlineSnippetEnd="}}"] * @property {boolean} [errorOnUnknownInlineSnippet=true] + * @property {boolean} [allowChangingDisplayedRole=true] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 8c42ab0..b410e09 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -336,6 +336,11 @@ "default": true }, + "allowChangingDisplayedRole": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/main.js b/src/main.js index 45ce593..384b02a 100644 --- a/src/main.js +++ b/src/main.js @@ -301,6 +301,7 @@ async function initPlugins() { "file:./src/modules/id", "file:./src/modules/alert", "file:./src/modules/joinLeaveNotification", + "file:./src/modules/roles", ]; const plugins = [...builtInPlugins, ...config.plugins]; diff --git a/src/modules/roles.js b/src/modules/roles.js new file mode 100644 index 0000000..0f0921a --- /dev/null +++ b/src/modules/roles.js @@ -0,0 +1,63 @@ +const utils = require("../utils"); + +const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; + +module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowChangingDisplayedRole) { + return; + } + + commands.addInboxThreadCommand("role", "[role:string$]", async (msg, args, thread) => { + const moderatorRoleOverrides = thread.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY); + + // Set display role + if (args.role) { + if (args.role === "reset") { + await thread.resetModeratorRoleOverride(msg.member.id); + + const displayRole = thread.getModeratorDisplayRoleName(msg.member); + if (displayRole) { + thread.postSystemMessage(`Your display role has been reset. Your replies will now display the role **${displayRole}**.`); + } else { + thread.postSystemMessage("Your display role has been reset. Your replies will no longer display a role."); + } + + return; + } + + let role; + if (utils.isSnowflake(args.role)) { + if (! msg.member.roles.includes(args.role)) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); + return; + } + + role = utils.getInboxGuild().roles.get(args.role); + } else { + const matchingMemberRole = utils.getInboxGuild().roles.find(r => { + if (! msg.member.roles.includes(r.id)) return false; + return r.name.toLowerCase() === args.role.toLowerCase(); + }); + + if (! matchingMemberRole) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); + return; + } + + role = matchingMemberRole; + } + + await thread.setModeratorRoleOverride(msg.member.id, role.id); + thread.postSystemMessage(`Your display role has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + return; + } + + // Get display role + const displayRole = thread.getModeratorDisplayRoleName(msg.member); + if (displayRole) { + thread.postSystemMessage(`Your displayed role is currently: **${displayRole}**`); + } else { + thread.postSystemMessage("Your replies do not currently display a role"); + } + }); +};