diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc4cfb..b83d333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,18 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. -* Moderators can now set the role they'd like to be displayed with their replies on a per-thread basis by using `!role` +* Moderators can now set the role they'd like to be displayed with their replies ("display role") by default and on a per-thread basis by using `!role` * Moderators can only choose roles they currently have - * You can view your currently displayed role by using `!role` - * You can set the displayed role by using `!role `, e.g. `!role Interviewer` - * This feature can be disabled by setting `allowChangingDisplayedRole = off` + * You can view your current display role by using `!role` + * If you're in a modmail thread, this will show your display role for that thread + * If you're *not* in a modmail thread, this will show your *default* display role + * You can set the display role by using `!role `, e.g. `!role Interviewer` + * If you're in a modmail thread, this will set your display role for that thread + * If you're *not* in a modmail thread, this will set your *default* display role + * You can reset the display role by using `!role reset` + * If you're in a modmail thread, this will reset your display role for that thread to the default + * If you're *not* in a modmail thread, this will reset your *default* display role + * This feature can be disabled by setting `allowChangingDisplayRole = off` * New option: `fallbackRoleName` * Sets the role name to display in moderator replies if the moderator doesn't have a hoisted role * Unless `fallbackRoleName` is set, anonymous replies without a role will no longer display "Moderator:" at the beginning of the message diff --git a/docs/commands.md b/docs/commands.md index a6d39f2..e980c37 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -71,10 +71,13 @@ 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 +View your display role for the thread - the role that is shown in front of your name in your replies + +### `!role` +Reset your display role for the thread to the default ### `!role ` -Change the role that is sent with your replies to any role you currently have +Change your display role for the thread to any role you currently have ### `!loglink` Get a link to the open Modmail thread's log. @@ -129,6 +132,15 @@ Check if the specified user is blocked. **Example:** `!is_blocked 106391128718245888` +### `!role` +(Outside a modmail thread) View your default display role - the role that is shown in front of your name in your replies + +### `!role` +(Outside a modmail thread) Reset your default display role + +### `!role ` +(Outside a modmail thread) Change your default display role to any role you currently have + ### `!version` Show the Modmail bot's version. diff --git a/docs/configuration.md b/docs/configuration.md index bd01c60..9bd8eb5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,7 +102,7 @@ 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 +#### allowChangingDisplayRole **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. diff --git a/src/commands.js b/src/commands.js index a0c34e2..c7facd6 100644 --- a/src/commands.js +++ b/src/commands.js @@ -11,6 +11,13 @@ const Thread = require("./data/Thread"); * @param {object} args */ +/** + * @callback InboxServerCommandHandler + * @param {Eris.Message} msg + * @param {object} args + * @param {Thread} [thread] + */ + /** * @callback InboxThreadCommandHandler * @param {Eris.Message} msg @@ -30,7 +37,7 @@ const Thread = require("./data/Thread"); * @callback AddInboxServerCommandFn * @param {string} trigger * @param {string} parameters - * @param {CommandFn} handler + * @param {InboxServerCommandHandler} handler * @param {ICommandConfig} commandConfig */ diff --git a/src/data/Thread.js b/src/data/Thread.js index 9f151f4..27f9a0e 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -9,13 +9,12 @@ const attachments = require("./attachments"); const { formatters } = require("../formatters"); const { callAfterThreadCloseHooks } = require("../hooks/afterThreadClose"); const snippets = require("./snippets"); +const { getModeratorThreadDisplayRoleName } = require("./moderatorRoleOverrides"); 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 @@ -195,7 +194,7 @@ class Thread { */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; - const roleName = this.getModeratorDisplayRoleName(moderator); + const roleName = await getModeratorThreadDisplayRoleName(moderator, this.id); if (config.allowInlineSnippets) { // Replace {{snippet}} with the corresponding snippet @@ -838,43 +837,6 @@ 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 1dbb5eb..4431a30 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -60,7 +60,7 @@ * @property {string} [inlineSnippetStart="{{"] * @property {string} [inlineSnippetEnd="}}"] * @property {boolean} [errorOnUnknownInlineSnippet=true] - * @property {boolean} [allowChangingDisplayedRole=true] + * @property {boolean} [allowChangingDisplayRole=true] * @property {string} [fallbackRoleName=null] * @property {string} [logStorage="local"] * @property {object} [logOptions] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 6955e83..e490195 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -336,7 +336,7 @@ "default": true }, - "allowChangingDisplayedRole": { + "allowChangingDisplayRole": { "$ref": "#/definitions/customBoolean", "default": true }, diff --git a/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js b/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js new file mode 100644 index 0000000..cc82053 --- /dev/null +++ b/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js @@ -0,0 +1,17 @@ +exports.up = async function(knex, Promise) { + if (! await knex.schema.hasTable("moderator_role_overrides")) { + await knex.schema.createTable("moderator_role_overrides", table => { + table.string("moderator_id", 20); + table.string("thread_id", 36).nullable().defaultTo(null); + table.string("role_id", 20); + + table.primary(["moderator_id", "thread_id"]); + }); + } +}; + +exports.down = async function(knex, Promise) { + if (await knex.schema.hasTable("moderator_role_overrides")) { + await knex.schema.dropTable("moderator_role_overrides"); + } +}; diff --git a/src/data/moderatorRoleOverrides.js b/src/data/moderatorRoleOverrides.js new file mode 100644 index 0000000..d65a1fb --- /dev/null +++ b/src/data/moderatorRoleOverrides.js @@ -0,0 +1,165 @@ +const knex = require("../knex"); +const Eris = require("eris"); +const utils = require("../utils"); +const config = require("../cfg"); + +/** + * @param {string} moderatorId + * @returns {Promise} + */ +async function getModeratorDefaultRoleOverride(moderatorId) { + const roleOverride = await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .first(); + + return roleOverride ? roleOverride.role_id : null; +} + +/** + * @param {string} moderatorId + * @param {string} roleId + * @returns {Promise} + */ +async function setModeratorDefaultRoleOverride(moderatorId, roleId) { + const existingGlobalOverride = await getModeratorDefaultRoleOverride(moderatorId); + if (existingGlobalOverride) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .update({ role_id: roleId }); + } else { + await knex("moderator_role_overrides") + .insert({ + moderator_id: moderatorId, + thread_id: null, + role_id: roleId, + }); + } +} + +/** + * @param {string} moderatorId + * @returns {Promise} + */ +async function resetModeratorDefaultRoleOverride(moderatorId) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .delete(); +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadRoleOverride(moderatorId, threadId) { + const roleOverride = await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .first(); + + return roleOverride ? roleOverride.role_id : null; +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @param {string} roleId + * @returns {Promise} + */ +async function setModeratorThreadRoleOverride(moderatorId, threadId, roleId) { + const existingGlobalOverride = await getModeratorThreadRoleOverride(moderatorId, threadId); + if (existingGlobalOverride) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .update({ role_id: roleId }); + } else { + await knex("moderator_role_overrides") + .insert({ + moderator_id: moderatorId, + thread_id: threadId, + role_id: roleId, + }); + } +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @returns {Promise} + */ +async function resetModeratorThreadRoleOverride(moderatorId, threadId) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .delete(); +} + +/** + * @param {Eris.Member} moderator + * @returns {Promise} + */ +async function getModeratorDefaultDisplayRole(moderator) { + const globalOverrideRoleId = await getModeratorDefaultRoleOverride(moderator.id); + if (globalOverrideRoleId && moderator.roles.includes(globalOverrideRoleId)) { + return moderator.guild.roles.get(globalOverrideRoleId); + } + + return utils.getMainRole(moderator); +} + +/** + * @param {Eris.Member} moderator + * @returns {Promise} + */ +async function getModeratorDefaultDisplayRoleName(moderator) { + const defaultDisplayRole = await getModeratorDefaultDisplayRole(moderator); + return defaultDisplayRole + ? defaultDisplayRole.name + : (config.fallbackRoleName || null); +} + +/** + * @param {Eris.Member} moderator + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadDisplayRole(moderator, threadId) { + const threadOverrideRoleId = await getModeratorThreadRoleOverride(moderator.id, threadId); + if (threadOverrideRoleId && moderator.roles.includes(threadOverrideRoleId)) { + return moderator.guild.roles.get(threadOverrideRoleId); + } + + return getModeratorDefaultDisplayRole(moderator); +} + +/** + * @param {Eris.Member} moderator + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadDisplayRoleName(moderator, threadId) { + const threadDisplayRole = await getModeratorThreadDisplayRole(moderator, threadId); + return threadDisplayRole + ? threadDisplayRole.name + : (config.fallbackRoleName || null); +} + +module.exports = { + getModeratorDefaultRoleOverride, + setModeratorDefaultRoleOverride, + resetModeratorDefaultRoleOverride, + + getModeratorThreadRoleOverride, + setModeratorThreadRoleOverride, + resetModeratorThreadRoleOverride, + + getModeratorDefaultDisplayRole, + getModeratorDefaultDisplayRoleName, + + getModeratorThreadDisplayRole, + getModeratorThreadDisplayRoleName, +}; diff --git a/src/modules/roles.js b/src/modules/roles.js index 0f0921a..5ba59a8 100644 --- a/src/modules/roles.js +++ b/src/modules/roles.js @@ -1,63 +1,99 @@ const utils = require("../utils"); +const { + setModeratorDefaultRoleOverride, + resetModeratorDefaultRoleOverride, + + setModeratorThreadRoleOverride, + resetModeratorThreadRoleOverride, + + getModeratorThreadDisplayRoleName, + getModeratorDefaultDisplayRoleName, +} = require("../data/moderatorRoleOverrides"); const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; module.exports = ({ bot, knex, config, commands }) => { - if (! config.allowChangingDisplayedRole) { + if (! config.allowChangingDisplayRole) { return; } - commands.addInboxThreadCommand("role", "[role:string$]", async (msg, args, thread) => { - const moderatorRoleOverrides = thread.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY); + function resolveRoleInput(input) { + if (utils.isSnowflake(input)) { + return utils.getInboxGuild().roles.get(input); + } - // Set display role - if (args.role) { - if (args.role === "reset") { - await thread.resetModeratorRoleOverride(msg.member.id); + return utils.getInboxGuild().roles.find(r => r.name.toLowerCase() === input.toLowerCase()); + } - 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."); - } + // Get display role for a thread + commands.addInboxThreadCommand("role", [], async (msg, args, thread) => { + const displayRole = await getModeratorThreadDisplayRoleName(msg.member, thread.id); + if (displayRole) { + thread.postSystemMessage(`Your display role in this thread is currently **${displayRole}**`); + } else { + thread.postSystemMessage("Your replies in this thread do not currently display a role"); + } + }); - return; - } + // Reset display role for a thread + commands.addInboxThreadCommand("role reset", [], async (msg, args, thread) => { + await resetModeratorThreadRoleOverride(msg.member.id, thread.id); - 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; - } + const displayRole = await getModeratorThreadDisplayRoleName(msg.member, thread.id); + if (displayRole) { + thread.postSystemMessage(`Your display role for this thread has been reset. Your replies will now display the default role **${displayRole}**.`); + } else { + thread.postSystemMessage("Your display role for this thread has been reset. Your replies will no longer display a role."); + } + }, { + aliases: ["role_reset", "reset_role"], + }); - 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\`.`); + // Set display role for a thread + commands.addInboxThreadCommand("role", "", async (msg, args, thread) => { + const role = resolveRoleInput(args.role); + if (! role || ! msg.member.roles.includes(role.id)) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your display role in this thread."); return; } - // Get display role - const displayRole = thread.getModeratorDisplayRoleName(msg.member); + await setModeratorThreadRoleOverride(msg.member.id, thread.id, role.id); + thread.postSystemMessage(`Your display role for this thread has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + }); + + // Get default display role + commands.addInboxServerCommand("role", [], async (msg, args, thread) => { + const displayRole = await getModeratorDefaultDisplayRoleName(msg.member); if (displayRole) { - thread.postSystemMessage(`Your displayed role is currently: **${displayRole}**`); + msg.channel.createMessage(`Your default display role is currently **${displayRole}**`); } else { - thread.postSystemMessage("Your replies do not currently display a role"); + msg.channel.createMessage("Your replies do not currently display a role by default"); } }); + + // Reset default display role + commands.addInboxServerCommand("role reset", [], async (msg, args, thread) => { + await resetModeratorDefaultRoleOverride(msg.member.id); + + const displayRole = await getModeratorDefaultDisplayRoleName(msg.member); + if (displayRole) { + msg.channel.createMessage(`Your default display role has been reset. Your replies will now display the role **${displayRole}** by default.`); + } else { + msg.channel.createMessage("Your default display role has been reset. Your replies will no longer display a role by default."); + } + }, { + aliases: ["role_reset", "reset_role"], + }); + + // Set default display role + commands.addInboxServerCommand("role", "", async (msg, args, thread) => { + const role = resolveRoleInput(args.role); + if (! role || ! msg.member.roles.includes(role.id)) { + msg.channel.createMessage("No matching role found. Make sure you have the role before trying to set it as your default display role."); + return; + } + + await setModeratorDefaultRoleOverride(msg.member.id, role.id); + msg.channel.createMessage(`Your default display role has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + }); };