From fb4979ff00dfcc094429b457625ce3351216f676 Mon Sep 17 00:00:00 2001
From: Jonathan <54381371+yaboyaxis@users.noreply.github.com>
Date: Fri, 2 Apr 2021 09:38:24 -0400
Subject: [PATCH 1/8] Add !reason alias for !update (#141)

---
 backend/src/plugins/ModActions/commands/UpdateCmd.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/src/plugins/ModActions/commands/UpdateCmd.ts b/backend/src/plugins/ModActions/commands/UpdateCmd.ts
index 3a55463a..3a048b42 100644
--- a/backend/src/plugins/ModActions/commands/UpdateCmd.ts
+++ b/backend/src/plugins/ModActions/commands/UpdateCmd.ts
@@ -8,7 +8,7 @@ import { LogType } from "../../../data/LogType";
 import { CaseTypes } from "../../../data/CaseTypes";
 
 export const UpdateCmd = modActionsCmd({
-  trigger: "update",
+  trigger: ["update", "reason"],
   permission: "can_note",
   description:
     "Update the specified case (or, if case number is omitted, your latest case) by adding more notes/details to it",

From 2fc8cffd80a8a17a003f0932d54b4d5ce28b884e Mon Sep 17 00:00:00 2001
From: Nils <7890309+DarkView@users.noreply.github.com>
Date: Fri, 2 Apr 2021 15:39:22 +0200
Subject: [PATCH 2/8] Allow kicking the user from VC on mute (#156)

* Allow kicking the user from VC on mute

If any non-id string is entered in `move_to_voice_channel`, the user is kicked from the VC instead of being moved. We do not automatically kick if the option is set to null in order to not make this a breaking change for old, intended behavior

* Add explicit config option for kicking instead of kicking on any non-id

Kicking takes precedent in this case and will take effect instead of moving to voice id
---
 backend/src/plugins/Mutes/MutesPlugin.ts        | 1 +
 backend/src/plugins/Mutes/functions/muteUser.ts | 7 ++++---
 backend/src/plugins/Mutes/types.ts              | 1 +
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts
index ac55acc6..36fb425e 100644
--- a/backend/src/plugins/Mutes/MutesPlugin.ts
+++ b/backend/src/plugins/Mutes/MutesPlugin.ts
@@ -25,6 +25,7 @@ const defaultOptions = {
   config: {
     mute_role: null,
     move_to_voice_channel: null,
+    kick_from_voice_channel: false,
 
     dm_on_mute: false,
     dm_on_update: false,
diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts
index 2f8e4e28..393a00e1 100644
--- a/backend/src/plugins/Mutes/functions/muteUser.ts
+++ b/backend/src/plugins/Mutes/functions/muteUser.ts
@@ -120,11 +120,12 @@ export async function muteUser(
     }
 
     // If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role)
-    const moveToVoiceChannelId = pluginData.config.get().move_to_voice_channel;
-    if (moveToVoiceChannelId) {
+    const cfg = pluginData.config.get();
+    const moveToVoiceChannel = cfg.kick_from_voice_channel ? null : cfg.move_to_voice_channel;
+    if (moveToVoiceChannel || cfg.kick_from_voice_channel) {
       // TODO: Add back the voiceState check once we figure out how to get voice state for guild members that are loaded on-demand
       try {
-        await member.edit({ channelID: moveToVoiceChannelId });
+        await member.edit({ channelID: moveToVoiceChannel });
       } catch (e) {} // tslint:disable-line
     }
   }
diff --git a/backend/src/plugins/Mutes/types.ts b/backend/src/plugins/Mutes/types.ts
index 72ea52c2..7da2cb0d 100644
--- a/backend/src/plugins/Mutes/types.ts
+++ b/backend/src/plugins/Mutes/types.ts
@@ -15,6 +15,7 @@ import { EventEmitter } from "events";
 export const ConfigSchema = t.type({
   mute_role: tNullable(t.string),
   move_to_voice_channel: tNullable(t.string),
+  kick_from_voice_channel: t.boolean,
 
   dm_on_mute: t.boolean,
   dm_on_update: t.boolean,

From 2e50fa763078a07d27847d5b72c80118cfbf8dde Mon Sep 17 00:00:00 2001
From: Nils <7890309+DarkView@users.noreply.github.com>
Date: Fri, 2 Apr 2021 15:40:20 +0200
Subject: [PATCH 3/8] Add allow_mentions option to enable tags mentioning
 someone (#160)

Checked when a tag is posted - so a tag can have for example pings enabled if a mod executes that tag, but not if anyone below mod does it.
---
 backend/src/plugins/Tags/TagsPlugin.ts           | 1 +
 backend/src/plugins/Tags/types.ts                | 2 ++
 backend/src/plugins/Tags/util/onMessageCreate.ts | 6 +++++-
 3 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts
index e8bb8541..f6aa89e1 100644
--- a/backend/src/plugins/Tags/TagsPlugin.ts
+++ b/backend/src/plugins/Tags/TagsPlugin.ts
@@ -29,6 +29,7 @@ const defaultOptions: PluginOptions<TagsPluginType> = {
     user_tag_cooldown: null,
     global_tag_cooldown: null,
     user_cooldown: null,
+    allow_mentions: false,
     global_cooldown: null,
     auto_delete_command: false,
 
diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts
index 15b0ce5b..21324be0 100644
--- a/backend/src/plugins/Tags/types.ts
+++ b/backend/src/plugins/Tags/types.ts
@@ -15,6 +15,7 @@ export const TagCategory = t.type({
   user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
   user_category_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag category
   global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
+  allow_mentions: tNullable(t.boolean), // Per user, per category
   global_category_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per category
   auto_delete_command: tNullable(t.boolean), // Any tag, per tag category
 
@@ -31,6 +32,7 @@ export const ConfigSchema = t.type({
   user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
   global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
   user_cooldown: tNullable(t.union([t.string, t.number])), // Per user
+  allow_mentions: t.boolean, // Per user
   global_cooldown: tNullable(t.union([t.string, t.number])), // Any tag use
   auto_delete_command: t.boolean, // Any tag
 
diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts
index d7d8530b..c3e92a83 100644
--- a/backend/src/plugins/Tags/util/onMessageCreate.ts
+++ b/backend/src/plugins/Tags/util/onMessageCreate.ts
@@ -99,7 +99,11 @@ export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType
     return;
   }
 
-  const responseMsg = await channel.createMessage(tagResult.renderedContent);
+  const allowMentions = tagResult.category?.allow_mentions ?? config.allow_mentions;
+  const responseMsg = await channel.createMessage({
+    ...tagResult.renderedContent,
+    allowedMentions: { roles: allowMentions, users: allowMentions },
+  });
 
   // Save the command-response message pair once the message is in our database
   const deleteWithCommand = tagResult.category?.delete_with_command ?? config.delete_with_command;

From fcbb25b7fff22e6193e3b2006bc86cbeb7a9b5e2 Mon Sep 17 00:00:00 2001
From: vcokltfre <vcokltfre@gmail.com>
Date: Fri, 2 Apr 2021 14:42:25 +0100
Subject: [PATCH 4/8] feat: add color option to starboard (#163)

Co-authored-by: Almeida <42935195+almeidx@users.noreply.github.com>
---
 backend/src/plugins/Starboard/StarboardPlugin.ts    | 13 +++++++++++++
 backend/src/plugins/Starboard/types.ts              |  2 ++
 .../util/createStarboardEmbedFromMessage.ts         |  3 ++-
 .../Starboard/util/saveMessageToStarboard.ts        |  2 +-
 4 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/backend/src/plugins/Starboard/StarboardPlugin.ts b/backend/src/plugins/Starboard/StarboardPlugin.ts
index c3374e56..8767d7e9 100644
--- a/backend/src/plugins/Starboard/StarboardPlugin.ts
+++ b/backend/src/plugins/Starboard/StarboardPlugin.ts
@@ -57,6 +57,19 @@ export const StarboardPlugin = zeppelinGuildPlugin<StarboardPluginType>()("starb
               stars_required: 5
       ~~~
       
+      ### Basic starboard with custom color
+      Any message on the server that gets 5 star reactions will be posted into the starboard channel (604342689038729226), with the given color (0x87CEEB).
+      
+      ~~~yml
+      starboard:
+        config:
+          boards:
+            basic:
+              channel_id: "604342689038729226"
+              stars_required: 5
+              color: 0x87CEEB
+      ~~~
+      
       ### Custom star emoji
       This is identical to the basic starboard above, but accepts two emoji: the regular star and a custom :mrvnSmile: emoji
       
diff --git a/backend/src/plugins/Starboard/types.ts b/backend/src/plugins/Starboard/types.ts
index 8c94f415..b2118f55 100644
--- a/backend/src/plugins/Starboard/types.ts
+++ b/backend/src/plugins/Starboard/types.ts
@@ -12,6 +12,7 @@ const StarboardOpts = t.type({
   copy_full_embed: tNullable(t.boolean),
   enabled: tNullable(t.boolean),
   show_star_count: t.boolean,
+  color: t.number,
 });
 export type TStarboardOpts = t.TypeOf<typeof StarboardOpts>;
 
@@ -27,6 +28,7 @@ export const defaultStarboardOpts: Partial<TStarboardOpts> = {
   star_emoji: ["⭐"],
   enabled: true,
   show_star_count: true,
+  color: 0,
 };
 
 export interface StarboardPluginType extends BasePluginType {
diff --git a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts
index d63b519c..8ea900e2 100644
--- a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts
+++ b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts
@@ -8,7 +8,7 @@ const videoAttachmentExtensions = ["mp4", "mkv", "mov"];
 
 type StarboardEmbed = EmbedWith<"footer" | "author" | "fields" | "timestamp">;
 
-export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boolean): StarboardEmbed {
+export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boolean, color: number): StarboardEmbed {
   const embed: StarboardEmbed = {
     footer: {
       text: `#${(msg.channel as GuildChannel).name}`,
@@ -18,6 +18,7 @@ export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boo
     },
     fields: [],
     timestamp: new Date(msg.timestamp).toISOString(),
+    color,
   };
 
   if (msg.author.avatarURL) {
diff --git a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts
index f30beffc..e7587abd 100644
--- a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts
+++ b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts
@@ -16,7 +16,7 @@ export async function saveMessageToStarboard(
   if (!channel) return;
 
   const starCount = (await pluginData.state.starboardReactions.getAllReactionsForMessageId(msg.id)).length;
-  const embed = createStarboardEmbedFromMessage(msg, Boolean(starboard.copy_full_embed));
+  const embed = createStarboardEmbedFromMessage(msg, Boolean(starboard.copy_full_embed), starboard.color);
   embed.fields!.push(createStarboardPseudoFooterForMessage(starboard, msg, starboard.star_emoji![0], starCount));
 
   const starboardMessage = await (channel as TextChannel).createMessage({ embed });

From c5d68650a3ba52b22f5b8235d5589bd364744f44 Mon Sep 17 00:00:00 2001
From: Nils <7890309+DarkView@users.noreply.github.com>
Date: Fri, 2 Apr 2021 15:43:13 +0200
Subject: [PATCH 5/8] Enforce unified lock names by using functions to generate
 lock keys (#165)

---
 .../src/plugins/Automod/actions/addRoles.ts   |  5 ++--
 .../plugins/Automod/actions/removeRoles.ts    |  5 ++--
 .../plugins/Censor/util/onMessageCreate.ts    |  3 ++-
 .../plugins/Censor/util/onMessageUpdate.ts    |  3 ++-
 .../Counters/functions/changeCounterValue.ts  |  3 ++-
 .../Counters/functions/decayCounter.ts        |  3 ++-
 .../Counters/functions/setCounterValue.ts     |  3 ++-
 .../src/plugins/ModActions/commands/BanCmd.ts |  9 ++++---
 .../events/ReapplyActiveMuteOnJoinEvt.ts      |  5 ++--
 .../src/plugins/Mutes/functions/muteUser.ts   |  6 ++---
 .../src/plugins/Persist/events/LoadDataEvt.ts |  7 ++---
 .../util/addMemberPendingRoleChange.ts        |  3 ++-
 .../SelfGrantableRoles/commands/RoleAddCmd.ts |  3 ++-
 .../commands/RoleRemoveCmd.ts                 |  3 ++-
 .../plugins/Slowmode/util/onMessageCreate.ts  |  3 ++-
 .../events/StarboardReactionAddEvt.ts         |  3 ++-
 .../events/StarboardReactionRemoveEvts.ts     |  5 ++--
 backend/src/utils/lockNameHelpers.ts          | 26 +++++++++++++++++++
 18 files changed, 70 insertions(+), 28 deletions(-)
 create mode 100644 backend/src/utils/lockNameHelpers.ts

diff --git a/backend/src/plugins/Automod/actions/addRoles.ts b/backend/src/plugins/Automod/actions/addRoles.ts
index 4da915e7..976300a5 100644
--- a/backend/src/plugins/Automod/actions/addRoles.ts
+++ b/backend/src/plugins/Automod/actions/addRoles.ts
@@ -9,6 +9,7 @@ import { getMissingPermissions } from "../../../utils/getMissingPermissions";
 import { canAssignRole } from "../../../utils/canAssignRole";
 import { missingPermissionError } from "../../../utils/missingPermissionError";
 import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 const p = Constants.Permissions;
 
@@ -64,7 +65,7 @@ export const AddRolesAction = automodAction({
           return;
         }
 
-        const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
+        const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
 
         const rolesArr = Array.from(memberRoles.values());
         await member.edit({
@@ -72,7 +73,7 @@ export const AddRolesAction = automodAction({
         });
         member.roles = rolesArr; // Make sure we know of the new roles internally as well
 
-        memberRolesLock.unlock();
+        memberRoleLock.unlock();
       }),
     );
   },
diff --git a/backend/src/plugins/Automod/actions/removeRoles.ts b/backend/src/plugins/Automod/actions/removeRoles.ts
index 6049cbf2..c6c74e49 100644
--- a/backend/src/plugins/Automod/actions/removeRoles.ts
+++ b/backend/src/plugins/Automod/actions/removeRoles.ts
@@ -10,6 +10,7 @@ import { missingPermissionError } from "../../../utils/missingPermissionError";
 import { canAssignRole } from "../../../utils/canAssignRole";
 import { Constants } from "eris";
 import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 const p = Constants.Permissions;
 
@@ -66,7 +67,7 @@ export const RemoveRolesAction = automodAction({
           return;
         }
 
-        const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
+        const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
 
         const rolesArr = Array.from(memberRoles.values());
         await member.edit({
@@ -74,7 +75,7 @@ export const RemoveRolesAction = automodAction({
         });
         member.roles = rolesArr; // Make sure we know of the new roles internally as well
 
-        memberRolesLock.unlock();
+        memberRoleLock.unlock();
       }),
     );
   },
diff --git a/backend/src/plugins/Censor/util/onMessageCreate.ts b/backend/src/plugins/Censor/util/onMessageCreate.ts
index 2042126d..fb9cfaeb 100644
--- a/backend/src/plugins/Censor/util/onMessageCreate.ts
+++ b/backend/src/plugins/Censor/util/onMessageCreate.ts
@@ -2,10 +2,11 @@ import { GuildPluginData } from "knub";
 import { CensorPluginType } from "../types";
 import { SavedMessage } from "../../../data/entities/SavedMessage";
 import { applyFiltersToMsg } from "./applyFiltersToMsg";
+import { messageLock } from "../../../utils/lockNameHelpers";
 
 export async function onMessageCreate(pluginData: GuildPluginData<CensorPluginType>, savedMessage: SavedMessage) {
   if (savedMessage.is_bot) return;
-  const lock = await pluginData.locks.acquire(`message-${savedMessage.id}`);
+  const lock = await pluginData.locks.acquire(messageLock(savedMessage));
 
   const wasDeleted = await applyFiltersToMsg(pluginData, savedMessage);
 
diff --git a/backend/src/plugins/Censor/util/onMessageUpdate.ts b/backend/src/plugins/Censor/util/onMessageUpdate.ts
index 4c279caa..7afd4c17 100644
--- a/backend/src/plugins/Censor/util/onMessageUpdate.ts
+++ b/backend/src/plugins/Censor/util/onMessageUpdate.ts
@@ -2,10 +2,11 @@ import { GuildPluginData } from "knub";
 import { CensorPluginType } from "../types";
 import { SavedMessage } from "../../../data/entities/SavedMessage";
 import { applyFiltersToMsg } from "./applyFiltersToMsg";
+import { messageLock } from "../../../utils/lockNameHelpers";
 
 export async function onMessageUpdate(pluginData: GuildPluginData<CensorPluginType>, savedMessage: SavedMessage) {
   if (savedMessage.is_bot) return;
-  const lock = await pluginData.locks.acquire(`message-${savedMessage.id}`);
+  const lock = await pluginData.locks.acquire(messageLock(savedMessage));
 
   const wasDeleted = await applyFiltersToMsg(pluginData, savedMessage);
 
diff --git a/backend/src/plugins/Counters/functions/changeCounterValue.ts b/backend/src/plugins/Counters/functions/changeCounterValue.ts
index be214eb7..d5da6825 100644
--- a/backend/src/plugins/Counters/functions/changeCounterValue.ts
+++ b/backend/src/plugins/Counters/functions/changeCounterValue.ts
@@ -1,4 +1,5 @@
 import { GuildPluginData } from "knub";
+import { counterIdLock } from "../../../utils/lockNameHelpers";
 import { CountersPluginType } from "../types";
 import { checkCounterTrigger } from "./checkCounterTrigger";
 import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
@@ -28,7 +29,7 @@ export async function changeCounterValue(
   userId = counter.per_user ? userId : null;
 
   const counterId = pluginData.state.counterIds[counterName];
-  const lock = await pluginData.locks.acquire(counterId.toString());
+  const lock = await pluginData.locks.acquire(counterIdLock(counterId));
 
   await pluginData.state.counters.changeCounterValue(counterId, channelId, userId, change);
 
diff --git a/backend/src/plugins/Counters/functions/decayCounter.ts b/backend/src/plugins/Counters/functions/decayCounter.ts
index 175cb158..7db4cef3 100644
--- a/backend/src/plugins/Counters/functions/decayCounter.ts
+++ b/backend/src/plugins/Counters/functions/decayCounter.ts
@@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
 import { CountersPluginType } from "../types";
 import { checkAllValuesForTrigger } from "./checkAllValuesForTrigger";
 import { checkAllValuesForReverseTrigger } from "./checkAllValuesForReverseTrigger";
+import { counterIdLock } from "../../../utils/lockNameHelpers";
 
 export async function decayCounter(
   pluginData: GuildPluginData<CountersPluginType>,
@@ -16,7 +17,7 @@ export async function decayCounter(
   }
 
   const counterId = pluginData.state.counterIds[counterName];
-  const lock = await pluginData.locks.acquire(counterId.toString());
+  const lock = await pluginData.locks.acquire(counterIdLock(counterId));
 
   await pluginData.state.counters.decay(counterId, decayPeriodMS, decayAmount);
 
diff --git a/backend/src/plugins/Counters/functions/setCounterValue.ts b/backend/src/plugins/Counters/functions/setCounterValue.ts
index 2eefed8f..697c8503 100644
--- a/backend/src/plugins/Counters/functions/setCounterValue.ts
+++ b/backend/src/plugins/Counters/functions/setCounterValue.ts
@@ -1,4 +1,5 @@
 import { GuildPluginData } from "knub";
+import { counterIdLock } from "../../../utils/lockNameHelpers";
 import { CountersPluginType } from "../types";
 import { checkCounterTrigger } from "./checkCounterTrigger";
 import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
@@ -25,7 +26,7 @@ export async function setCounterValue(
   }
 
   const counterId = pluginData.state.counterIds[counterName];
-  const lock = await pluginData.locks.acquire(counterId.toString());
+  const lock = await pluginData.locks.acquire(counterIdLock(counterId));
 
   await pluginData.state.counters.setCounterValue(counterId, channelId, userId, value);
 
diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts
index 4fe9561e..1f943087 100644
--- a/backend/src/plugins/ModActions/commands/BanCmd.ts
+++ b/backend/src/plugins/ModActions/commands/BanCmd.ts
@@ -8,9 +8,10 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach
 import { banUserId } from "../functions/banUserId";
 import { getMemberLevel, waitForReaction } from "knub/dist/helpers";
 import humanizeDuration from "humanize-duration";
-import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
-import { CaseTypes } from "src/data/CaseTypes";
-import { LogType } from "src/data/LogType";
+import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
+import { CaseTypes } from "../../../data/CaseTypes";
+import { LogType } from "../../../data/LogType";
+import { banLock } from "../../../utils/lockNameHelpers";
 
 const opts = {
   mod: ct.member({ option: true }),
@@ -62,7 +63,7 @@ export const BanCmd = modActionsCmd({
     }
 
     // acquire a lock because of the needed user-inputs below (if banned/not on server)
-    const lock = await pluginData.locks.acquire(`ban-${user.id}`);
+    const lock = await pluginData.locks.acquire(banLock(user));
     let forceban = false;
     const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
     const banned = await isBanned(pluginData, user.id);
diff --git a/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts b/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts
index 890636a6..2e57e8da 100644
--- a/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts
+++ b/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts
@@ -1,6 +1,7 @@
 import { mutesEvt } from "../types";
 import { LogType } from "../../../data/LogType";
 import { stripObjectToScalars } from "../../../utils";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 /**
  * Reapply active mutes on join
@@ -11,9 +12,9 @@ export const ReapplyActiveMuteOnJoinEvt = mutesEvt("guildMemberAdd", async ({ pl
     const muteRole = pluginData.config.get().mute_role;
 
     if (muteRole) {
-      const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
+      const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
       await member.addRole(muteRole);
-      memberRolesLock.unlock();
+      memberRoleLock.unlock();
     }
 
     pluginData.state.serverLogs.log(LogType.MEMBER_MUTE_REJOIN, {
diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts
index 393a00e1..29ba54ab 100644
--- a/backend/src/plugins/Mutes/functions/muteUser.ts
+++ b/backend/src/plugins/Mutes/functions/muteUser.ts
@@ -17,8 +17,8 @@ import { CasesPlugin } from "../../Cases/CasesPlugin";
 import { CaseTypes } from "../../../data/CaseTypes";
 import { LogType } from "../../../data/LogType";
 import { Case } from "../../../data/entities/Case";
-import { sendErrorMessage } from "src/pluginUtils";
-import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
+import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
+import { muteLock } from "../../../utils/lockNameHelpers";
 
 export async function muteUser(
   pluginData: GuildPluginData<MutesPluginType>,
@@ -29,7 +29,7 @@ export async function muteUser(
   removeRolesOnMuteOverride: boolean | string[] | null = null,
   restoreRolesOnMuteOverride: boolean | string[] | null = null,
 ) {
-  const lock = await pluginData.locks.acquire(`mute-${userId}`);
+  const lock = await pluginData.locks.acquire(muteLock({ id: userId }));
 
   const muteRole = pluginData.config.get().mute_role;
   if (!muteRole) {
diff --git a/backend/src/plugins/Persist/events/LoadDataEvt.ts b/backend/src/plugins/Persist/events/LoadDataEvt.ts
index e88cb333..bd356acb 100644
--- a/backend/src/plugins/Persist/events/LoadDataEvt.ts
+++ b/backend/src/plugins/Persist/events/LoadDataEvt.ts
@@ -7,6 +7,7 @@ import { getMissingPermissions } from "../../../utils/getMissingPermissions";
 import { LogsPlugin } from "../../Logs/LogsPlugin";
 import { missingPermissionError } from "../../../utils/missingPermissionError";
 import { canAssignRole } from "../../../utils/canAssignRole";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 const p = Constants.Permissions;
 
@@ -17,11 +18,11 @@ export const LoadDataEvt = persistEvt({
     const member = meta.args.member;
     const pluginData = meta.pluginData;
 
-    const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
+    const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
 
     const persistedData = await pluginData.state.persistedData.find(member.id);
     if (!persistedData) {
-      memberRolesLock.unlock();
+      memberRoleLock.unlock();
       return;
     }
 
@@ -79,6 +80,6 @@ export const LoadDataEvt = persistEvt({
       });
     }
 
-    memberRolesLock.unlock();
+    memberRoleLock.unlock();
   },
 });
diff --git a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts
index 2b2dfd0d..00ae6108 100644
--- a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts
+++ b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts
@@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
 import { ReactionRolesPluginType, RoleChangeMode, PendingMemberRoleChanges } from "../types";
 import { resolveMember } from "../../../utils";
 import { logger } from "../../../logger";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 const ROLE_CHANGE_BATCH_DEBOUNCE_TIME = 1500;
 
@@ -18,7 +19,7 @@ export async function addMemberPendingRoleChange(
       applyFn: async () => {
         pluginData.state.pendingRoleChanges.delete(memberId);
 
-        const lock = await pluginData.locks.acquire(`member-roles-${memberId}`);
+        const lock = await pluginData.locks.acquire(memberRolesLock({ id: memberId }));
 
         const member = await resolveMember(pluginData.client, pluginData.guild, memberId);
         if (member) {
diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts
index 2c9a1d0f..8fe86f04 100644
--- a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts
+++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts
@@ -6,6 +6,7 @@ import { splitRoleNames } from "../util/splitRoleNames";
 import { normalizeRoleNames } from "../util/normalizeRoleNames";
 import { findMatchingRoles } from "../util/findMatchingRoles";
 import { Role } from "eris";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 export const RoleAddCmd = selfGrantableRolesCmd({
   trigger: ["role", "role add"],
@@ -16,7 +17,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({
   },
 
   async run({ message: msg, args, pluginData }) {
-    const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
+    const lock = await pluginData.locks.acquire(memberRolesLock(msg.author));
 
     const applyingEntries = getApplyingEntries(pluginData, msg);
     if (applyingEntries.length === 0) {
diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts
index 7cabb3c4..c2011e05 100644
--- a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts
+++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts
@@ -5,6 +5,7 @@ import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
 import { splitRoleNames } from "../util/splitRoleNames";
 import { normalizeRoleNames } from "../util/normalizeRoleNames";
 import { findMatchingRoles } from "../util/findMatchingRoles";
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
 
 export const RoleRemoveCmd = selfGrantableRolesCmd({
   trigger: "role remove",
@@ -15,7 +16,7 @@ export const RoleRemoveCmd = selfGrantableRolesCmd({
   },
 
   async run({ message: msg, args, pluginData }) {
-    const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
+    const lock = await pluginData.locks.acquire(memberRolesLock(msg.author));
 
     const applyingEntries = getApplyingEntries(pluginData, msg);
     if (applyingEntries.length === 0) {
diff --git a/backend/src/plugins/Slowmode/util/onMessageCreate.ts b/backend/src/plugins/Slowmode/util/onMessageCreate.ts
index 8e1b8564..4f0c3989 100644
--- a/backend/src/plugins/Slowmode/util/onMessageCreate.ts
+++ b/backend/src/plugins/Slowmode/util/onMessageCreate.ts
@@ -10,6 +10,7 @@ import { BOT_SLOWMODE_PERMISSIONS } from "../requiredPermissions";
 import { LogsPlugin } from "../../Logs/LogsPlugin";
 import { LogType } from "../../../data/LogType";
 import { missingPermissionError } from "../../../utils/missingPermissionError";
+import { messageLock } from "../../../utils/lockNameHelpers";
 
 export async function onMessageCreate(pluginData: GuildPluginData<SlowmodePluginType>, msg: SavedMessage) {
   if (msg.is_bot) return;
@@ -18,7 +19,7 @@ export async function onMessageCreate(pluginData: GuildPluginData<SlowmodePlugin
   if (!channel) return;
 
   // Don't apply slowmode if the lock was interrupted earlier (e.g. the message was caught by word filters)
-  const thisMsgLock = await pluginData.locks.acquire(`message-${msg.id}`);
+  const thisMsgLock = await pluginData.locks.acquire(messageLock(msg));
   if (thisMsgLock.interrupted) return;
 
   // Check if this channel even *has* a bot-maintained slowmode
diff --git a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts
index b6142655..6c6cf93a 100644
--- a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts
+++ b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts
@@ -3,6 +3,7 @@ import { Message, TextChannel } from "eris";
 import { UnknownUser, resolveMember, noop, resolveUser } from "../../../utils";
 import { saveMessageToStarboard } from "../util/saveMessageToStarboard";
 import { updateStarboardMessageStarCount } from "../util/updateStarboardMessageStarCount";
+import { allStarboardsLock } from "../../../utils/lockNameHelpers";
 
 export const StarboardReactionAddEvt = starboardEvt({
   event: "messageReactionAdd",
@@ -36,7 +37,7 @@ export const StarboardReactionAddEvt = starboardEvt({
       categoryId: (msg.channel as TextChannel).parentID,
     });
 
-    const boardLock = await pluginData.locks.acquire(`starboards`);
+    const boardLock = await pluginData.locks.acquire(allStarboardsLock());
 
     const applicableStarboards = Object.values(config.boards)
       .filter(board => board.enabled)
diff --git a/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts b/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts
index 07f85670..90160a4a 100644
--- a/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts
+++ b/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts
@@ -1,10 +1,11 @@
+import { allStarboardsLock } from "../../../utils/lockNameHelpers";
 import { starboardEvt } from "../types";
 
 export const StarboardReactionRemoveEvt = starboardEvt({
   event: "messageReactionRemove",
 
   async listener(meta) {
-    const boardLock = await meta.pluginData.locks.acquire(`starboards`);
+    const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock());
     await meta.pluginData.state.starboardReactions.deleteStarboardReaction(meta.args.message.id, meta.args.member.id);
     boardLock.unlock();
   },
@@ -14,7 +15,7 @@ export const StarboardReactionRemoveAllEvt = starboardEvt({
   event: "messageReactionRemoveAll",
 
   async listener(meta) {
-    const boardLock = await meta.pluginData.locks.acquire(`starboards`);
+    const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock());
     await meta.pluginData.state.starboardReactions.deleteAllStarboardReactionsForMessageId(meta.args.message.id);
     boardLock.unlock();
   },
diff --git a/backend/src/utils/lockNameHelpers.ts b/backend/src/utils/lockNameHelpers.ts
new file mode 100644
index 00000000..4a16bd33
--- /dev/null
+++ b/backend/src/utils/lockNameHelpers.ts
@@ -0,0 +1,26 @@
+import { Member, Message, User } from "eris";
+import { SavedMessage } from "../data/entities/SavedMessage";
+
+export function allStarboardsLock() {
+  return `starboards`;
+}
+
+export function banLock(user: Member | User | { id: string }) {
+  return `ban-${user.id}`;
+}
+
+export function counterIdLock(counterId: number | string) {
+  return `counter-${counterId}`;
+}
+
+export function memberRolesLock(member: Member | User | { id: string }) {
+  return `member-roles-${member.id}`;
+}
+
+export function messageLock(message: Message | SavedMessage | { id: string }) {
+  return `message-${message.id}`;
+}
+
+export function muteLock(user: Member | User | { id: string }) {
+  return `mute-${user.id}`;
+}

From 2af168b8e3a617b52fa6f4e7cb43d9a55bc29e26 Mon Sep 17 00:00:00 2001
From: vcokltfre <vcokltfre@gmail.com>
Date: Fri, 2 Apr 2021 14:43:52 +0100
Subject: [PATCH 6/8] chore: update wording of permission error for -mod (#172)

---
 backend/src/plugins/ModActions/commands/AddCaseCmd.ts           | 2 +-
 backend/src/plugins/ModActions/commands/BanCmd.ts               | 2 +-
 backend/src/plugins/ModActions/commands/ForcebanCmd.ts          | 2 +-
 backend/src/plugins/ModActions/commands/UnbanCmd.ts             | 2 +-
 backend/src/plugins/ModActions/commands/WarnCmd.ts              | 2 +-
 backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts | 2 +-
 backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts   | 2 +-
 backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts | 2 +-
 8 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts
index 7bd06bf8..3bed2911 100644
--- a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts
+++ b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts
@@ -45,7 +45,7 @@ export const AddCaseCmd = modActionsCmd({
     let mod = msg.member;
     if (args.mod) {
       if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
-        sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+        sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
         return;
       }
 
diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts
index 1f943087..da7156c2 100644
--- a/backend/src/plugins/ModActions/commands/BanCmd.ts
+++ b/backend/src/plugins/ModActions/commands/BanCmd.ts
@@ -55,7 +55,7 @@ export const BanCmd = modActionsCmd({
     let mod = msg.member;
     if (args.mod) {
       if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
-        sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+        sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
         return;
       }
 
diff --git a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts
index 907237ef..afda940e 100644
--- a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts
+++ b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts
@@ -54,7 +54,7 @@ export const ForcebanCmd = modActionsCmd({
     let mod = msg.member;
     if (args.mod) {
       if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
-        sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+        sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
         return;
       }
 
diff --git a/backend/src/plugins/ModActions/commands/UnbanCmd.ts b/backend/src/plugins/ModActions/commands/UnbanCmd.ts
index 80cac4f2..0d5abc1c 100644
--- a/backend/src/plugins/ModActions/commands/UnbanCmd.ts
+++ b/backend/src/plugins/ModActions/commands/UnbanCmd.ts
@@ -37,7 +37,7 @@ export const UnbanCmd = modActionsCmd({
     let mod = msg.member;
     if (args.mod) {
       if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
-        sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+        sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
         return;
       }
 
diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts
index 8ac19baa..e36dbb86 100644
--- a/backend/src/plugins/ModActions/commands/WarnCmd.ts
+++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts
@@ -57,7 +57,7 @@ export const WarnCmd = modActionsCmd({
     let mod = msg.member;
     if (args.mod) {
       if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
-        msg.channel.createMessage(errorMessage("No permission for -mod"));
+        msg.channel.createMessage(errorMessage("You don't have permission to use -mod"));
         return;
       }
 
diff --git a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts
index 18ee2137..e467212c 100644
--- a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts
+++ b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts
@@ -52,7 +52,7 @@ export async function actualKickMemberCmd(
   let mod = msg.member;
   if (args.mod) {
     if (!hasPermission(pluginData.config.getForMessage(msg), "can_act_as_other")) {
-      sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+      sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
       return;
     }
 
diff --git a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts
index 6d640ec6..19c13477 100644
--- a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts
+++ b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts
@@ -28,7 +28,7 @@ export async function actualMuteUserCmd(
 
   if (args.mod) {
     if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
-      sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+      sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
       return;
     }
 
diff --git a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts
index 0faee47c..7931aac1 100644
--- a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts
+++ b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts
@@ -19,7 +19,7 @@ export async function actualUnmuteCmd(
 
   if (args.mod) {
     if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
-      sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
+      sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
       return;
     }
 

From c4a8c3014e430583dc8cbdc7ee63e80a7d95dd17 Mon Sep 17 00:00:00 2001
From: DEX <zodpixel@gmail.com>
Date: Fri, 2 Apr 2021 17:45:00 +0400
Subject: [PATCH 7/8] Adding av as an alias for the avatar command (#169)

---
 backend/src/plugins/Utility/commands/AvatarCmd.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/src/plugins/Utility/commands/AvatarCmd.ts b/backend/src/plugins/Utility/commands/AvatarCmd.ts
index 9e413a3d..7ab1fac8 100644
--- a/backend/src/plugins/Utility/commands/AvatarCmd.ts
+++ b/backend/src/plugins/Utility/commands/AvatarCmd.ts
@@ -5,7 +5,7 @@ import { sendErrorMessage } from "../../../pluginUtils";
 import { EmbedOptions } from "eris";
 
 export const AvatarCmd = utilityCmd({
-  trigger: "avatar",
+  trigger: ["avatar", "av"],
   description: "Retrieves a user's profile picture",
   permission: "can_avatar",
 

From 56ade239dcd535bcccbfc68700955476233d3dbe Mon Sep 17 00:00:00 2001
From: Nils <7890309+DarkView@users.noreply.github.com>
Date: Fri, 2 Apr 2021 15:53:09 +0200
Subject: [PATCH 8/8] Allow mute and unmute to be upgraded to their force
 variant (#174)

---
 .../plugins/ModActions/commands/MuteCmd.ts    | 19 ++++++++++--------
 .../plugins/ModActions/commands/UnmuteCmd.ts  | 20 +++++++++++--------
 2 files changed, 23 insertions(+), 16 deletions(-)

diff --git a/backend/src/plugins/ModActions/commands/MuteCmd.ts b/backend/src/plugins/ModActions/commands/MuteCmd.ts
index ada23e2f..3aa96c38 100644
--- a/backend/src/plugins/ModActions/commands/MuteCmd.ts
+++ b/backend/src/plugins/ModActions/commands/MuteCmd.ts
@@ -6,7 +6,7 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach
 import { CasesPlugin } from "../../Cases/CasesPlugin";
 import { LogType } from "../../../data/LogType";
 import { CaseTypes } from "../../../data/CaseTypes";
-import { errorMessage, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
+import { errorMessage, noop, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
 import { isBanned } from "../functions/isBanned";
 import { waitForReaction } from "knub/dist/helpers";
 import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
@@ -59,15 +59,18 @@ export const MuteCmd = modActionsCmd({
           msg.channel,
           `User is banned. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
         );
+        return;
       } else {
-        sendErrorMessage(
-          pluginData,
-          msg.channel,
-          `User is not on the server. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
-        );
-      }
+        // Ask the mod if we should upgrade to a forcemute as the user is not on the server
+        const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forcemute instead?");
+        const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id);
 
-      return;
+        notOnServerMsg.delete().catch(noop);
+        if (!reply || reply.name === "❌") {
+          sendErrorMessage(pluginData, msg.channel, "User not on server, mute cancelled by moderator");
+          return;
+        }
+      }
     }
 
     // Make sure we're allowed to mute this member
diff --git a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
index f6efdd39..c294517c 100644
--- a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
+++ b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
@@ -1,10 +1,11 @@
 import { modActionsCmd } from "../types";
 import { commandTypeHelpers as ct } from "../../../commandTypes";
 import { canActOn, sendErrorMessage } from "../../../pluginUtils";
-import { resolveUser, resolveMember } from "../../../utils";
+import { resolveUser, resolveMember, noop } from "../../../utils";
 import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin";
 import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
 import { isBanned } from "../functions/isBanned";
+import { waitForReaction } from "knub/dist/helpers";
 
 const opts = {
   mod: ct.member({ option: true }),
@@ -57,15 +58,18 @@ export const UnmuteCmd = modActionsCmd({
           msg.channel,
           `User is banned. Use \`${prefix}forceunmute\` to unmute them anyway.`,
         );
+        return;
       } else {
-        sendErrorMessage(
-          pluginData,
-          msg.channel,
-          `User is not on the server. Use \`${prefix}forceunmute\` to unmute them anyway.`,
-        );
-      }
+        // Ask the mod if we should upgrade to a forceunmute as the user is not on the server
+        const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forceunmute instead?");
+        const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id);
 
-      return;
+        notOnServerMsg.delete().catch(noop);
+        if (!reply || reply.name === "❌") {
+          sendErrorMessage(pluginData, msg.channel, "User not on server, unmute cancelled by moderator");
+          return;
+        }
+      }
     }
 
     // Make sure we're allowed to unmute this member