Repository: TheOdinProject/odin-bot-v2
Pull request #551 (2024-06-19 02:33:40)

Bump ws from 8.17.0 to 8.17.1
Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/websockets/ws/releases">ws's releases</a>.</em></p>
<blockquote>
<h2>8.17.1</h2>
<h1>Bug fixes</h1>
<ul>
<li>Fixed a DoS vulnerability (<a href="https://redirect.github.com/websockets/ws/issues/2231">#2231</a>).</li>
</ul>
<p>A request with a number of headers exceeding the[<code>server.maxHeadersCount</code>][]
threshold could be used to crash a ws server.</p>
<pre lang="js"><code>const http = require('http');
const WebSocket = require('ws');
<p>const wss = new WebSocket.Server({ port: 0 }, function () {
const chars = &quot;!#$%&amp;'*+-.0123456789abcdefghijklmnopqrstuvwxyz^_`|~&quot;.split('');
const headers = {};
let count = 0;</p>
<p>for (let i = 0; i &lt; chars.length; i++) {
if (count === 2000) break;</p>
<pre><code>for (let j = 0; j &amp;lt; chars.length; j++) {
  const key = chars[i] + chars[j];
  headers[key] = 'x';

  if (++count === 2000) break;
}
</code></pre>
<p>}</p>
<p>headers.Connection = 'Upgrade';
headers.Upgrade = 'websocket';
headers['Sec-WebSocket-Key'] = 'dGhlIHNhbXBsZSBub25jZQ==';
headers['Sec-WebSocket-Version'] = '13';</p>
<p>const request = http.request({
headers: headers,
host: '127.0.0.1',
port: wss.address().port
});</p>
<p>request.end();
});
</code></pre></p>
<p>The vulnerability was reported by <a href="https://github.com/rrlapointe">Ryan LaPointe</a> in <a href="https://redirect.github.com/websockets/ws/issues/2230">websockets/ws#2230</a>.</p>
<p>In vulnerable versions of ws, the issue can be mitigated in the following ways:</p>
<ol>
<li>Reduce the maximum allowed length of the request headers using the
[<code>--max-http-header-size=size</code>][] and/or the [<code>maxHeaderSize</code>][] options so
that no more headers than the <code>server.maxHeadersCount</code> limit can be sent.</li>
</ol>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/websockets/ws/commit/3c56601092872f7d7566989f0e379271afd0e4a1"><code>3c56601</code></a> [dist] 8.17.1</li>
<li><a href="https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c"><code>e55e510</code></a> [security] Fix crash when the Upgrade header cannot be read (<a href="https://redirect.github.com/websockets/ws/issues/2231">#2231</a>)</li>
<li><a href="https://github.com/websockets/ws/commit/6a00029edd924499f892aed8003cef1fa724cfe5"><code>6a00029</code></a> [test] Increase code coverage</li>
<li><a href="https://github.com/websockets/ws/commit/ddfe4a804d79e7788ab136290e609f91cf68423f"><code>ddfe4a8</code></a> [perf] Reduce the amount of <code>crypto.randomFillSync()</code> calls</li>
<li>See full diff in <a href="https://github.com/websockets/ws/compare/8.17.0...8.17.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=8.17.0&new-version=8.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/TheOdinProject/odin-bot-v2/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.



Patch:
diff --git a/.env.sample b/.env.sample
index 693fbd2..bef4dd8 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,7 +1,6 @@
 POINTSBOT_TOKEN=xxxxx
 DISCORD_API_KEY=xxxxxxxx
 DISCORD_GUILD_ID=xxxxxxx
-DISCORD_GETTING_HIRED_CHANNEL_ID=xxxxxxxxxx
 DISCORD_CLIENT_ID=xxxxx
 
 REDIS_URL=redis://localhost:6379
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..f143442
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,16 @@
+{
+  "arrowParens": "always",
+  "bracketSpacing": true,
+  "bracketSameLine": false,
+  "endOfLine": "lf",
+  "htmlWhitespaceSensitivity": "css",
+  "insertPragma": false,
+  "printWidth": 80,
+  "proseWrap": "preserve",
+  "quoteProps": "as-needed",
+  "semi": true,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "useTabs": false
+}
diff --git a/botCommands/code-legacy.js b/botCommands/code-legacy.js
index 55d7ae0..d55ddbf 100644
--- a/botCommands/code-legacy.js
+++ b/botCommands/code-legacy.js
@@ -1,7 +1,10 @@
 const Discord = require("discord.js");
 const { registerBotCommand } = require("../botEngine");
+const { code } = require("../lib/commandsContent")
 
-const code = {
+const { color, title, description } = code;
+
+const command = {
   regex: /(?<!\S)!code(?!\S)/,
   cb: ({ mentions }) => {
     let users = "";
@@ -12,40 +15,17 @@ const code = {
     }
 
     const codeCommandEmbed = new Discord.EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('How to share your code')
-      .setDescription(`
-**Codeblocks:**        
-
-To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
-
-\\\`\\\`\\\`js
-// your JavaScript code goes here
-\\\`\\\`\\\`
-
-**Inline code:**
-
-For \`inline code\` use one backtick (no syntax highlighting):
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
-\\\`code here!\\\`
-
-**Websites:**
-
-- [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
-- [Repl.it](https://replit.com/) for JavaScript/Ruby projects
-- [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      `
-      );
-
-    return { 
+    return {
       content: users ? `${users.trim()}` : '',
-      embeds: [codeCommandEmbed] 
+      embeds: [codeCommandEmbed]
     };
   },
 };
 
-registerBotCommand(code.regex, code.cb);
+registerBotCommand(command.regex, command.cb);
 
-module.exports = {
-  code,
-};
+module.exports = command;
diff --git a/botCommands/os.js b/botCommands/os.js
index b80f99a..1231231 100644
--- a/botCommands/os.js
+++ b/botCommands/os.js
@@ -1,18 +1,17 @@
 const Discord = require("discord.js");
 const { registerBotCommand } = require("../botEngine");
+const { os } = require("../lib/commandsContent");
+
+const { color, title, description, url } = os;
 
 const command = {
   regex: /(?<!\S)!os(?!\S)/,
   cb: () => {
     const osEmbed = new Discord.EmbedBuilder()
-      .setColor("#cc9543")
-      .setTitle("Windows")
-      .setDescription(
-        "**The Odin Project does not support Windows or any other OS outside of our recommendations**. We are happy to assist with any questions about installing a VM, using WSL, or dual booting Linux. <https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options>"
-      )
-      .setURL(
-        "https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options"
-      );
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description)
+      .setURL(url);
 
     return { embeds: [osEmbed] };
   },
diff --git a/botCommands/points.js b/botCommands/points.js
index 6337ed4..7262b91 100644
--- a/botCommands/points.js
+++ b/botCommands/points.js
@@ -3,6 +3,8 @@ const config = require('../config');
 const { registerBotCommand } = require('../botEngine');
 const club40Gifs = require('./club_40_gifs.json');
 
+const { isAdmin } = require('../utils/is-admin');
+
 axios.default.defaults.headers.common.Authorization = `Token ${config.pointsbot.token}`;
 
 function gifPicker(gifContainer, clubChannel) {
@@ -11,7 +13,15 @@ function gifPicker(gifContainer, clubChannel) {
   clubChannel.send(`Gif by ${gifContainer[choice].author}`);
 }
 
-function getUserIdsFromMessage(client, author, guild, text, regex, authorMember, channel) {
+function getUserIdsFromMessage(
+  client,
+  author,
+  guild,
+  text,
+  regex,
+  authorMember,
+  channel,
+) {
   const matches = [];
   const processedIDs = [];
   let match = regex.exec(text);
@@ -19,22 +29,19 @@ function getUserIdsFromMessage(client, author, guild, text, regex, authorMember,
   while (match !== null) {
     const userID = match[1].replace('!', '');
     if (match[2] === '?++') {
-      let isAdmin = false;
-      authorMember.roles.cache.forEach((value) => {
-        if (config.roles.adminRolesName.includes(value.name)) {
-          isAdmin = true;
-        }
-      });
-
-      if (isAdmin) {
+      if (isAdmin(authorMember)) {
         matches.push([userID, 2]);
       } else {
-        channel.send('Only maintainers or core members can give double points!');
+        channel.send(
+          'Only maintainers or core members can give double points!',
+        );
       }
       match = regex.exec(text);
     } else {
       if (processedIDs.includes(userID)) {
-        channel.send('Only maintainers or core members can give double points!');
+        channel.send(
+          'Only maintainers or core members can give double points!',
+        );
       } else {
         processedIDs.push(userID);
         matches.push([userID, 1]);
@@ -47,7 +54,8 @@ function getUserIdsFromMessage(client, author, guild, text, regex, authorMember,
 
 const deductPoints = {
   regex: /(?<!\S)<@!?(\d+)>\s?(--)(?!\S)/,
-  cb: () => 'http://media.riffsy.com/images/636a97aa416ad674eb2b72d4a6e9ad6c/tenor.gif',
+  cb: () =>
+    'http://media.riffsy.com/images/636a97aa416ad674eb2b72d4a6e9ad6c/tenor.gif',
 };
 
 registerBotCommand(deductPoints.regex, deductPoints.cb);
@@ -163,26 +171,29 @@ const awardPoints = {
           if (user) {
             const recipientMember = await guild.members.fetch(user);
             if (
-              recipientMember
-              && !recipientMember.roles.cache.find((r) => r.name === 'club-40')
-              && pointsUser.points > 39
+              recipientMember &&
+              !recipientMember.roles.cache.find((r) => r.name === 'club-40') &&
+              pointsUser.points > 39
             ) {
               const pointsRole = guild.roles.cache.find(
                 (r) => r.name === 'club-40',
               );
               recipientMember.roles.add(pointsRole);
-              const clubChannel = client.channels.cache.get(
-                '707225752608964628',
-              );
+              const clubChannel =
+                client.channels.cache.get('707225752608964628');
 
               if (clubChannel) {
-                const welcomeMessage = (previousPoints < 40) ? `HEYYY EVERYONE SAY HI TO ${user} the newest member of CLUB 40. Please check the pins at the top right!` : `WELCOME BACK TO CLUB 40 ${user}!! Please review the pins at the top right!`;
+                const welcomeMessage =
+                  previousPoints < 40
+                    ? `HEYYY EVERYONE SAY HI TO ${user} the newest member of CLUB 40. Please check the pins at the top right!`
+                    : `WELCOME BACK TO CLUB 40 ${user}!! Please review the pins at the top right!`;
                 clubChannel.send(welcomeMessage);
                 gifPicker(club40Gifs, clubChannel);
               }
             }
             channel.send(
-              `${exclamation(pointsUser.points, isGoodQuestion)} ${user} now has ${pointsUser.points
+              `${exclamation(pointsUser.points, isGoodQuestion)} ${user} now has ${
+                pointsUser.points
               } ${plural(pointsUser.points)}`,
             );
           }
diff --git a/botCommands/top-legacy.js b/botCommands/top-legacy.js
index 3dbfce4..2a95d06 100644
--- a/botCommands/top-legacy.js
+++ b/botCommands/top-legacy.js
@@ -1,15 +1,17 @@
 const Discord = require('discord.js');
 const { registerBotCommand } = require('../botEngine');
+const { top } = require('../lib/commandsContent');
+
+const { color, title, description } = top;
 
 const command = {
   regex: /(?<!\S)!top(?!\S)/,
   cb: () => {
     const topLegacyEmbed = new Discord.EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('The Odin Project').setDescription(`
-For more information about The Odin Project, visit our site:
-[Your Career in Web Development Starts Here](https://www.theodinproject.com/)    
-    `);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
+
     return { embeds: [topLegacyEmbed] };
   },
 };
diff --git a/botCommands/wsl.js b/botCommands/wsl.js
index c217d98..8cf82d0 100644
--- a/botCommands/wsl.js
+++ b/botCommands/wsl.js
@@ -1,15 +1,16 @@
 const Discord = require("discord.js");
 const { registerBotCommand } = require("../botEngine");
+const { wsl } = require("../lib/commandsContent");
+
+const { color, title, description } = wsl;
 
 const command = {
   regex: /(?<!\S)!wsl(?!\S)/,
   cb: () => {
     const wslEmbed = new Discord.EmbedBuilder()
-      .setColor("#cc9543")
-      .setTitle("WSL")
-      .setDescription(
-        "For help with using WSL please use the WSL channel: <#1179839248803844117>"
-      );
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
     return { embeds: [wslEmbed] };
   },
diff --git a/botCommands/xy-legacy.js b/botCommands/xy-legacy.js
index a0a709b..e38ce73 100644
--- a/botCommands/xy-legacy.js
+++ b/botCommands/xy-legacy.js
@@ -1,13 +1,15 @@
 const Discord = require('discord.js');
 const { registerBotCommand } = require('../botEngine');
+const { xy } = require('../lib/commandsContent');
 
+const {color, title, description} = xy;
 const command = {
   regex: /(?<!\S)!xy(?!\S)/,
   cb: () => {
     const xyEmbed = new Discord.EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('This could very well be an xy problem')
-      .setDescription(`What problem are you *really* trying to solve? Check out [this article about xy problems](https://xyproblem.info/) to help others better understand your question.`);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
     return { embeds: [xyEmbed] };
   },
 };
diff --git a/config.js b/config.js
index 19ad200..880b12a 100644
--- a/config.js
+++ b/config.js
@@ -10,9 +10,12 @@ const config = {
   channels: {
     noPointsChannelIds: ['513125912070455296', '948409662255026227'],
     introductionsChannelId: '690618925494566912',
-    gettingHiredChannelId: process.env.DISCORD_GETTING_HIRED_CHANNEL_ID,
+    gettingHiredChannelId: '669547324707569665',
     botSpamPlaygroundChannelId: '513125912070455296',
     FAQChannelId: '823266307293839401',
+    WSLChannelId: '1179839248803844117',
+    ContactModeratorsChannelId: '1059513837197459547',
+    rulesChannelId: '693244715839127653',
   },
   roles: {
     NOBOTRoleId: '783764176178774036',
@@ -20,6 +23,7 @@ const config = {
     backer: '1134874972339327087',
   },
   modmailUserId: '575252669443211264',
+  color: '#cc9543',
 };
 
 module.exports = config;
diff --git a/events/message-create.js b/events/message-create.js
index aab0b2f..2b85866 100644
--- a/events/message-create.js
+++ b/events/message-create.js
@@ -1,13 +1,15 @@
 const { Events } = require('discord.js');
 const GettingHiredMessageService = require('../services/getting-hired-message.service');
 const config = require('../config');
+const { isAdmin } = require('../utils/is-admin');
 
 const botCommands = [];
 
 let authorBuffer = [];
 
 let currentIntroductionsMessage = null;
-const introductionsWelcomeMessage = 'Welcome to The Odin Project! Take a moment to survey all of the channels on the sidebar, especially the <#823266307293839401> channel for answers to commonly asked questions. We\'re excited for you to join us on your programming journey. Happy learning!';
+
+const introductionsWelcomeMessage = `Welcome to The Odin Project! Take a moment to survey all of the channels on the sidebar, especially the <#${config.channels.FAQChannelId}> channel for answers to commonly asked questions. We're excited for you to join us on your programming journey. Happy learning!`;
 
 function createAuthorEntry(message) {
   const entry = {
@@ -44,23 +46,11 @@ module.exports = {
      * based on the admin (core, maintainer) status of the member in the Discord.
      * Sets the flag for later use.
      */
-    let isAdminMessage = false;
-    try {
-      isAdminMessage = message.member.roles.cache.some(
-        (r) => config.roles.adminRolesName.includes(r.name),
-      );
-    } catch (e) {
-      //  The only 'con' is a command or message gets ignored.
-    }
 
-    let isMessageAuthorNobot = false;
-    try {
-      isMessageAuthorNobot = message.member.roles.cache.has(
-        config.roles.NOBOTRoleId,
-      );
-    } catch (err) {
-      // message.member can be null
-    }
+    const isAdminMessage = isAdmin(message.member);
+    const isMessageAuthorNobot = message.member?.roles.cache.has(
+      config.roles.NOBOTRoleId,
+    );
 
     // can't bot if user is NOBOT
     if (isMessageAuthorNobot) {
@@ -121,8 +111,8 @@ module.exports = {
       // introductions
       if (!isAdminMessage) {
         if (
-          currentIntroductionsMessage
-          && currentIntroductionsMessage.content === introductionsWelcomeMessage
+          currentIntroductionsMessage &&
+          currentIntroductionsMessage.content === introductionsWelcomeMessage
         ) {
           currentIntroductionsMessage.delete();
         }
diff --git a/lib/commandsContent.js b/lib/commandsContent.js
new file mode 100644
index 0000000..fe4e554
--- /dev/null
+++ b/lib/commandsContent.js
@@ -0,0 +1,74 @@
+const { channels, color } = require('../config');
+
+const commandContents = {
+  code: {
+    color,
+    title: 'How to share your code',
+    description: `
+**Codeblocks:**
+
+To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
+
+\\\`\\\`\\\`js
+// your JavaScript code goes here
+\\\`\\\`\\\`
+
+**Inline code:**
+
+For \`inline code\` use one backtick (no syntax highlighting):
+
+\\\`code here!\\\`
+
+**Websites:**
+
+- [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
+- [Repl.it](https://replit.com/) for JavaScript/Ruby projects
+- [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
+`,
+  },
+  os: {
+    color,
+    title: 'Windows',
+    description: `**The Odin Project is not designed for and does not support Windows or any other OS outside of our recommendations**. We are happy to assist with any questions about installing a VM, using WSL, or dual booting Linux.
+
+For more info on Windows, check out this exhaustive list on [why we do not support Windows](<https://github.com/TheOdinProject/blog/wiki/Why-We-Do-Not-Support-Windows>).
+
+[You can find our list of OS options in the Installation Overview lesson](<https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options>)`,
+    url: 'https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options',
+  },
+  question: {
+    color,
+    title: 'Asking Great Questions',
+    description: `
+Asking context-rich questions makes it easy to receive help, and makes it easy for others to help you quickly! Great engineers ask great questions, and the prompt below is an invitation to improve your skills and set yourself up for success in the workplace.
+
+**Project/Exercise:**
+**Lesson link:**
+**Code:** [code sandbox like replit or codepen]
+**Issue/Problem:** [screenshots if applicable]
+**What I expected:**
+**What I've tried:**
+
+For even more context around on how to hone your question-asking skills, give this a read: https://www.theodinproject.com/guides/community/how_to_ask
+        `,
+  },
+  top: {
+    color,
+    title: 'The Odin Project',
+    description:
+      'For more information about The Odin Project, visit our site:\n[Your Career in Web Development Starts Here](https://www.theodinproject.com/)',
+  },
+  wsl: {
+    color,
+    title: 'WSL',
+    description: `For help with using WSL please use the WSL channel: <#${channels.WSLChannelId}>`,
+  },
+  xy: {
+    color,
+    title: 'This could very well be an xy problem',
+    description:
+      'What problem are you *really* trying to solve? Check out [this article about xy problems](https://xyproblem.info/) to help others better understand your question.',
+  },
+};
+
+module.exports = commandContents;
diff --git a/new-era-commands/slash/bothelp.js b/new-era-commands/slash/bothelp.js
index 1ec939e..b678f08 100644
--- a/new-era-commands/slash/bothelp.js
+++ b/new-era-commands/slash/bothelp.js
@@ -1,31 +1,33 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { channels } = require('../../config');
 
 module.exports = {
   data: new SlashCommandBuilder()
     .setName('bothelp')
     .setDescription('Information about the Odin bot')
-    .addUserOption((option) => option.setName('user').setDescription('user to ping')),
+    .addUserOption((option) =>
+      option.setName('user').setDescription('user to ping'),
+    ),
   execute: async (interaction) => {
     const userId = interaction.options.getUser('user');
 
     const botHelpEmbed = new EmbedBuilder()
       .setColor('#cc9543')
-      .setTitle('Odin Bot Information')
-      .setDescription(`
-**Slash Commands** 
+      .setTitle('Odin Bot Information').setDescription(`
+**Slash Commands**
 Type a slash \`/\` to see a list of all the available slash commands you can use. Most commands offer extra options, such as pinging a user or specifying additional variables.
 
-**Text-Based Commands** 
-We also have these text-based commands which you can use in a sentence:      
+**Text-Based Commands**
+We also have these text-based commands which you can use in a sentence:
 \`!xy\`, \`!code\`, \`!question\`, \`!google\`, \`!searchtop\` and \`!top\`
 
-**Bot Playground** 
-Experiment with our bot in the <#513125912070455296> channel to try out different commands and features. It's a safe space to explore without disturbing the main channels.
+**Bot Playground**
+Experiment with our bot in the <#${channels.botSpamPlaygroundChannelId}> channel to try out different commands and features. It's a safe space to explore without disturbing the main channels.
 
 **Repository**
 Feel free to check out or contribute to the Odin bot's code in the [odin-bot Repository](https://github.com/TheOdinProject/odin-bot-v2)
 
-**Party Parrot** 
+**Party Parrot**
 Fun fact: Odin-bot loves the Party Parrot! 🦜
       `);
 
diff --git a/new-era-commands/slash/code.js b/new-era-commands/slash/code.js
index 81cadb1..9247d36 100644
--- a/new-era-commands/slash/code.js
+++ b/new-era-commands/slash/code.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { code } = require('../../lib/commandsContent');
+
+const { color, title, description } = code;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -9,29 +12,9 @@ module.exports = {
     const userId = interaction.options.getUser('user');
 
     const codeEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('How to share your code')
-      .setDescription(`
-**Codeblocks:**
-
-To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
-
-\\\`\\\`\\\`js
-// your JavaScript code goes here
-\\\`\\\`\\\`
-
-**Inline code:**
-
-For \`inline code\` use one backtick (no syntax highlighting):
-
-\\\`code here!\\\`
-
-**Websites:**
-
-- [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
-- [Repl.it](https://replit.com/) for JavaScript/Ruby projects
-- [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      `);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
     await interaction.reply({
       content: userId ? `${userId}` : '',
diff --git a/new-era-commands/slash/faq.js b/new-era-commands/slash/faq.js
index d444590..3895898 100644
--- a/new-era-commands/slash/faq.js
+++ b/new-era-commands/slash/faq.js
@@ -1,17 +1,22 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { channels } = require('../../config');
 
 module.exports = {
   data: new SlashCommandBuilder()
     .setName('faq')
     .setDescription('Provides a link to our FAQ channel')
-    .addUserOption((option) => option.setName('user').setDescription('user to ping')),
+    .addUserOption((option) =>
+      option.setName('user').setDescription('user to ping'),
+    ),
   execute: async (interaction) => {
     const userId = interaction.options.getUser('user');
 
     const faqEmbed = new EmbedBuilder()
       .setColor('#cc9543')
       .setTitle('TOP FAQ')
-      .setDescription('We have a channel with frequently asked questions - <#823266307293839401>!');
+      .setDescription(
+        `We have a channel with frequently asked questions - <#${channels.FAQChannelId}>!`,
+      );
 
     await interaction.reply({
       content: userId ? `${userId}` : '',
diff --git a/new-era-commands/slash/help.js b/new-era-commands/slash/help.js
index f31a7e7..25907e1 100644
--- a/new-era-commands/slash/help.js
+++ b/new-era-commands/slash/help.js
@@ -1,22 +1,47 @@
-const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
+const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const config = require('../../config');
+
+const { channels } = config;
 
 module.exports = {
   data: new SlashCommandBuilder()
-    .setName("help")
-    .setDescription("Help")
+    .setName('help')
+    .setDescription('Help')
     .addUserOption((option) =>
-      option.setName("user").setDescription("user to ping")
+      option.setName('user').setDescription('user to ping'),
     ),
   execute: async (interaction) => {
-    const userId = interaction.options.getUser("user");
+    const userId = interaction.options.getUser('user');
 
     // helpEmbed would make a good candidate for a general /help info command (a default command like there is for /points) when we implement subcommands for /help
 
     const helpEmbed = new EmbedBuilder()
-      .setColor("#cc9543")
+      .setColor('#cc9543')
       .setTitle("Get help on The Odin Project's Discord Server")
-      .setDescription(
-        "**Server Conduct**\nAll conduct must be within our [rules](https://www.theodinproject.com/guides/community/rules), and [community expectations](https://www.theodinproject.com/guides/community/expectations).\nPlease report issues/misconduct to <@575252669443211264>, as well as to ask any rule-related questions. Modmail How-To - <#1059513837197459547>\n\n **Just the FAQs**\n We have a [list of frequently asked questions - <#823266307293839401>!\n\n**Helpful bot commands**\nType `/` in the message box to see the full list of commands! Explore them in our bot channel - <#513125912070455296>. \n\n**It's TOP o'Clock, 24/7/365**\n This server is for supporting learners working through our curriculum.\nIf your question is not about TOP's curriculum or the projects it contains, try another discord: [Discord Server Search](https://disboard.org/servers/tag/coding).\n\n**TOP is community-driven**\nThe Odin Project is open source and independent. Donate here: [Open Collective](https://opencollective.com/theodinproject). To learn more, use the `/contribute` or `/support` commands."
+      .addFields(
+        {
+          name: 'Server Conduct',
+          value: `All conduct must be within our [rules](https://www.theodinproject.com/guides/community/rules), and [community expectations](https://www.theodinproject.com/guides/community/expectations).\nPlease report issues/misconduct to <@${config.modmailUserId}>, as well as to ask any rule-related questions. Modmail How-To - <#${channels.ContactModeratorsChannelId}>`,
+        },
+        {
+          name: 'Just the FAQs',
+          value: `We have a list of frequently asked questions - <#${channels.FAQChannelId}>!`,
+        },
+        {
+          name: 'Helpful bot commands',
+          value:
+            `Type \`/\` in the message box to see the full list of commands! Explore them in our bot channel - <#${channels.botSpamPlaygroundChannelId}>.`,
+        },
+        {
+          name: "It's TOP o'Clock, 24/7/365",
+          value:
+            "This server is for supporting learners working through our curriculum.\nIf your question is not about TOP's curriculum or the projects it contains, try another discord: [Discord Server Search](https://disboard.org/servers/tag/coding).",
+        },
+        {
+          name: 'TOP is community-driven',
+          value:
+            'The Odin Project is open source and independent. Donate here: [Open Collective](https://opencollective.com/theodinproject). To learn more, use the `/contribute` or `/support` commands.',
+        },
       );
 
     await interaction.reply({
diff --git a/new-era-commands/slash/os.js b/new-era-commands/slash/os.js
index f189511..ab1b118 100644
--- a/new-era-commands/slash/os.js
+++ b/new-era-commands/slash/os.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { os } = require('../../lib/commandsContent');
+
+const { color, title, description } = os;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -9,10 +12,9 @@ module.exports = {
     const userId = interaction.options.getUser('user')?.id;
 
     const osEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('Windows')
-      .setDescription('**The Odin Project does not support Windows or any other OS outside of our recommendations**. We are happy to assist with any questions about installing a VM, using WSL, or dual booting Linux. <https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options>')
-      .setURL('https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options');
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description)
 
     await interaction.reply({
       content: userId ? `<@${userId}>` : '',
diff --git a/new-era-commands/slash/question.js b/new-era-commands/slash/question.js
index 5a717d8..6ab2516 100644
--- a/new-era-commands/slash/question.js
+++ b/new-era-commands/slash/question.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { question } = require('../../lib/commandsContent');
+
+const { color, title, description } = question;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -9,20 +12,9 @@ module.exports = {
     const userId = interaction.options.getUser('user');
 
     const questionEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('Asking Great Questions')
-      .setDescription(`
-Asking context-rich questions makes it easy to receive help, and makes it easy for others to help you quickly! Great engineers ask great questions, and the prompt below is an invitation to improve your skills and set yourself up for success in the workplace.
-
-**Project/Exercise:**
-**Lesson link:**
-**Code:** [code sandbox like replit or codepen]
-**Issue/Problem:** [screenshots if applicable]
-**What I expected:**
-**What I've tried:**
-
-For even more context around on how to hone your question-asking skills, give this a read: https://www.theodinproject.com/guides/community/how_to_ask
-              `);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
     await interaction.reply({
       content: userId ? `${userId}` : '',
diff --git a/new-era-commands/slash/support.js b/new-era-commands/slash/support.js
index 889ae03..5c19861 100644
--- a/new-era-commands/slash/support.js
+++ b/new-era-commands/slash/support.js
@@ -1,5 +1,9 @@
 const {
-  SlashCommandBuilder, EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder,
+  SlashCommandBuilder,
+  EmbedBuilder,
+  ButtonBuilder,
+  ButtonStyle,
+  ActionRowBuilder,
 } = require('discord.js');
 
 module.exports = {
@@ -16,15 +20,19 @@ module.exports = {
       .setLabel('Donate Now')
       .setURL('https://opencollective.com/theodinproject/donate?amount=5')
       .setStyle(ButtonStyle.Link)
-      .setEmoji('💰');
+      .setEmoji('1270559118368964710');
 
-    const buttons = new ActionRowBuilder()
-      .addComponents(learnMoreButton, donateNowButton);
+    const buttons = new ActionRowBuilder().addComponents(
+      learnMoreButton,
+      donateNowButton,
+    );
 
     const supportEmbed = new EmbedBuilder()
       .setColor('#cc9543')
       .setTitle('Support The Project')
-      .setDescription('From the very beginning, The Odin Project has been committed to providing a world-class and completely free coding curriculum for anyone and everyone eager to learn. **With your generous donations**, we can continue to inspire thousands of aspiring developers, irrespective of their background or financial status.');
+      .setDescription(
+        'From the very beginning, The Odin Project has been committed to providing a world-class and completely free coding curriculum for anyone and everyone eager to learn. **With your generous donations**, we can continue to inspire thousands of aspiring developers, irrespective of their background or financial status.',
+      );
 
     await interaction.reply({ embeds: [supportEmbed], components: [buttons] });
   },
diff --git a/new-era-commands/slash/top.js b/new-era-commands/slash/top.js
index 6dd2a0f..276237e 100644
--- a/new-era-commands/slash/top.js
+++ b/new-era-commands/slash/top.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { top } = require('../../lib/commandsContent');
+
+const { color, title, description } = top;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -9,12 +12,9 @@ module.exports = {
     const userId = interaction.options.getUser('user');
 
     const topEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('The Odin Project')
-      .setDescription(`
-For more information about The Odin Project, visit our site:
-[Your Career in Web Development Starts Here](https://www.theodinproject.com/)
-    `);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
     await interaction.reply({
       content: userId ? `${userId}` : '',
diff --git a/new-era-commands/slash/wsl.js b/new-era-commands/slash/wsl.js
index 9cdf2a1..82ca4aa 100644
--- a/new-era-commands/slash/wsl.js
+++ b/new-era-commands/slash/wsl.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const { wsl } = require('../../lib/commandsContent');
+
+const { color, title, description } = wsl;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -9,9 +12,9 @@ module.exports = {
     const userId = interaction.options.getUser('user')?.id;
 
     const wslEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('WSL')
-      .setDescription('For help with using WSL please use the WSL channel: <#1179839248803844117>')
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description)
 
     await interaction.reply({
       content: userId ? `<@${userId}>` : '',
diff --git a/new-era-commands/slash/xy.js b/new-era-commands/slash/xy.js
index b4c84e1..afcf4ca 100644
--- a/new-era-commands/slash/xy.js
+++ b/new-era-commands/slash/xy.js
@@ -1,5 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const {xy} = require('../../lib/commandsContent');
 
+const {color, title, description} = xy;
 module.exports = {
   data: new SlashCommandBuilder()
     .setName('xy')
@@ -9,12 +11,9 @@ module.exports = {
     const userId = interaction.options.getUser('user');
 
     const xyEmbed = new EmbedBuilder()
-      .setColor('#cc9543')
-      .setTitle('This could very well be an xy problem')
-      .setDescription(`
-What problem are you *really* trying to solve?
-Check out [this article about xy problems](https://xyproblem.info/) to help others better understand your question.
-    `);
+      .setColor(color)
+      .setTitle(title)
+      .setDescription(description);
 
     await interaction.reply({
       content: userId ? `${userId}` : '',
diff --git a/package-lock.json b/package-lock.json
index 29f1266..fe1fed8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
         "dotenv": "^16.4.5",
         "glob": "^10.4.1",
         "ioredis": "^5.4.1",
-        "prettier": "^3.3.1"
+        "prettier": "^3.3.3"
       },
       "devDependencies": {
         "@babel/eslint-parser": "^7.24.7",
@@ -1977,6 +1977,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.0.tgz",
       "integrity": "sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=18"
       },
@@ -7296,9 +7297,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz",
-      "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==",
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+      "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
       "bin": {
         "prettier": "bin/prettier.cjs"
       },
@@ -8504,9 +8505,9 @@
       }
     },
     "node_modules/ws": {
-      "version": "8.17.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
-      "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
       "engines": {
         "node": ">=10.0.0"
       },
@@ -13844,9 +13845,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz",
-      "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg=="
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+      "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew=="
     },
     "pretty-format": {
       "version": "29.7.0",
@@ -14701,9 +14702,9 @@
       }
     },
     "ws": {
-      "version": "8.17.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
-      "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
       "requires": {}
     },
     "y18n": {
diff --git a/package.json b/package.json
index dc68a83..2055a37 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
     "start": "node index.js",
     "dev": "nodemon index.js",
     "test": "NODE_OPTIONS=--experimental-vm-modules jest",
+    "format": "prettier --cache --write",
     "eslint": "eslint '**/*.js'"
   },
   "author": "",
@@ -19,7 +20,7 @@
     "dotenv": "^16.4.5",
     "glob": "^10.4.1",
     "ioredis": "^5.4.1",
-    "prettier": "^3.3.1"
+    "prettier": "^3.3.3"
   },
   "devDependencies": {
     "@babel/eslint-parser": "^7.24.7",
diff --git a/services/getting-hired-message.service.js b/services/getting-hired-message.service.js
index 6eaabf5..649ab22 100644
--- a/services/getting-hired-message.service.js
+++ b/services/getting-hired-message.service.js
@@ -24,7 +24,8 @@ class GettingHiredMessageService {
   }
 
   static async sendIntroMessage(message) {
-    const welcomeMessage = 'Welcome to the channel for the **Getting Hired** part of the curriculum. Please ensure you have **completed the Getting Hired course** and **read all of the pins** prior to engaging in this channel for resume review, interview help, or anything else covered in that section!';
+    const welcomeMessage =
+      'Welcome to the channel for the **Getting Hired** part of the curriculum. Please ensure you have **completed the Getting Hired course** and **read all of the pins** prior to engaging in this channel for resume review, interview help, or anything else covered in that section!';
 
     try {
       await message.author.send(welcomeMessage);
diff --git a/services/moderation-message-delete.service.js b/services/moderation-message-delete.service.js
index e7f0818..be912e1 100644
--- a/services/moderation-message-delete.service.js
+++ b/services/moderation-message-delete.service.js
@@ -1,26 +1,32 @@
 const { RESTJSONErrorCodes } = require('discord-api-types/v10');
 const { EmbedBuilder } = require('discord.js');
 const ThreadCreator = require('../utils/thread-creator');
+const { modmailUserId, channels } = require('../config');
 
 class ModerationMessageDeleteService {
   static async handleInteraction(interaction) {
     const message = interaction.options.getMessage('message');
 
     await ModerationMessageDeleteService.#deleteMessage(message);
-    await ModerationMessageDeleteService.#interactionReply(interaction, { content: 'Message deleted.', ephemeral: true });
+    await ModerationMessageDeleteService.#interactionReply(interaction, {
+      content: 'Message deleted.',
+      ephemeral: true,
+    });
 
     if (message.author.bot) return;
 
-    const messageEmbed = await ModerationMessageDeleteService.#messageBuilder(message);
+    const messageEmbed =
+      await ModerationMessageDeleteService.#messageBuilder(message);
 
     try {
-      await ModerationMessageDeleteService.#sendMessageToUser(
-        message.author,
-        { embeds: [messageEmbed] },
-      );
+      await ModerationMessageDeleteService.#sendMessageToUser(message.author, {
+        embeds: [messageEmbed],
+      });
     } catch (error) {
       if (error.code === RESTJSONErrorCodes.CannotSendMessagesToThisUser) {
-        await ModerationMessageDeleteService.#handleThread(message, { embeds: [messageEmbed] });
+        await ModerationMessageDeleteService.#handleThread(message, {
+          embeds: [messageEmbed],
+        });
       } else {
         console.log(error);
       }
@@ -67,8 +73,9 @@ class ModerationMessageDeleteService {
   static async #messageBuilder(message) {
     return new EmbedBuilder()
       .setTitle('Message Deleted')
-      .setDescription(`
-      Hi <@${message.author.id}>, your message in <#${message.channel.id}> was deleted by a moderator. 
+      .setDescription(
+        `
+      Hi <@${message.author.id}>, your message in <#${message.channel.id}> was deleted by a moderator.
 
 Some common reasons for post removal include but isn't limited to;
 - the post was not in line with our discord rules, channel description or community expectations
@@ -77,15 +84,14 @@ Some common reasons for post removal include but isn't limited to;
 - a link was shared without context
 - the post was shared in multiple channels within a short timeframe (cross-posting)
 
-If further action is deemed necessary, a moderator will follow up on this message in a private thread. 
+If further action is deemed necessary, a moderator will follow up on this message in a private thread.
 
-Please make sure to check the rules of the server and the description of the channel you’ve posted in. 
-You can find our rules including a link to the additional community expectations here <#693244715839127653>
+Please make sure to check the rules of the server and the description of the channel you've posted in.
+You can find our rules including a link to the additional community expectations here <#${channels.rulesChannelId}>
 
-If after reading the rules, channel description or community expectations, you feel this deletion was made in error, feel free to send a dm to <@575252669443211264>`)
-      .setFields([
-        { name: 'Message:', value: message.content },
-      ]);
+If after reading the rules, channel description or community expectations, you feel this deletion was made in error, feel free to send a dm to <@${modmailUserId}>`,
+      )
+      .setFields([{ name: 'Message:', value: message.content }]);
   }
 }
 
diff --git a/utils/is-admin.js b/utils/is-admin.js
new file mode 100644
index 0000000..35942ed
--- /dev/null
+++ b/utils/is-admin.js
@@ -0,0 +1,9 @@
+const { roles } = require('../config');
+
+function isAdmin(member) {
+  return member?.roles.cache.some((role) =>
+    roles.adminRolesName.includes(role.name),
+  );
+}
+
+module.exports = { isAdmin };


Test patch:
diff --git a/botCommands/__snapshots__/code-legacy.test.js.snap b/botCommands/__snapshots__/code-legacy.test.js.snap
index e65a6b5..b211338 100644
--- a/botCommands/__snapshots__/code-legacy.test.js.snap
+++ b/botCommands/__snapshots__/code-legacy.test.js.snap
@@ -7,7 +7,7 @@ exports[`!code snapshot returns correct output 1`] = `
     {
       "color": 13407555,
       "description": "
-**Codeblocks:**        
+**Codeblocks:**
 
 To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
 
@@ -26,7 +26,7 @@ For \`inline code\` use one backtick (no syntax highlighting):
 - [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
 - [Repl.it](https://replit.com/) for JavaScript/Ruby projects
 - [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      ",
+",
       "title": "How to share your code",
     },
   ],
@@ -40,7 +40,7 @@ exports[`!code snapshot returns correct output 2`] = `
     {
       "color": 13407555,
       "description": "
-**Codeblocks:**        
+**Codeblocks:**
 
 To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
 
@@ -59,7 +59,7 @@ For \`inline code\` use one backtick (no syntax highlighting):
 - [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
 - [Repl.it](https://replit.com/) for JavaScript/Ruby projects
 - [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      ",
+",
       "title": "How to share your code",
     },
   ],
@@ -73,7 +73,7 @@ exports[`!code snapshot returns correct output 3`] = `
     {
       "color": 13407555,
       "description": "
-**Codeblocks:**        
+**Codeblocks:**
 
 To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
 
@@ -92,7 +92,7 @@ For \`inline code\` use one backtick (no syntax highlighting):
 - [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
 - [Repl.it](https://replit.com/) for JavaScript/Ruby projects
 - [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      ",
+",
       "title": "How to share your code",
     },
   ],
@@ -106,7 +106,7 @@ exports[`!code snapshot returns correct output 4`] = `
     {
       "color": 13407555,
       "description": "
-**Codeblocks:**        
+**Codeblocks:**
 
 To write multiple lines of code with language syntax highlighting, use three [backticks](https://i.stack.imgur.com/ETTnT.jpg) followed by the language:
 
@@ -125,7 +125,7 @@ For \`inline code\` use one backtick (no syntax highlighting):
 - [Code Sandbox](https://codesandbox.io/) for Webpack/React projects
 - [Repl.it](https://replit.com/) for JavaScript/Ruby projects
 - [Codepen](https://codepen.io/) for basic HTML/CSS/Javascript
-      ",
+",
       "title": "How to share your code",
     },
   ],
diff --git a/botCommands/__snapshots__/os.test.js.snap b/botCommands/__snapshots__/os.test.js.snap
index 8cf5aa1..067e48d 100644
--- a/botCommands/__snapshots__/os.test.js.snap
+++ b/botCommands/__snapshots__/os.test.js.snap
@@ -5,7 +5,11 @@ exports[`!os callback returns correct output 1`] = `
   "embeds": [
     {
       "color": 13407555,
-      "description": "**The Odin Project does not support Windows or any other OS outside of our recommendations**. We are happy to assist with any questions about installing a VM, using WSL, or dual booting Linux. <https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options>",
+      "description": "**The Odin Project is not designed for and does not support Windows or any other OS outside of our recommendations**. We are happy to assist with any questions about installing a VM, using WSL, or dual booting Linux.
+
+For more info on Windows, check out this exhaustive list on [why we do not support Windows](<https://github.com/TheOdinProject/blog/wiki/Why-We-Do-Not-Support-Windows>).
+
+[You can find our list of OS options in the Installation Overview lesson](<https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options>)",
       "title": "Windows",
       "url": "https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options",
     },
diff --git a/botCommands/__snapshots__/top-legacy.test.js.snap b/botCommands/__snapshots__/top-legacy.test.js.snap
index 4e33616..b829988 100644
--- a/botCommands/__snapshots__/top-legacy.test.js.snap
+++ b/botCommands/__snapshots__/top-legacy.test.js.snap
@@ -5,10 +5,8 @@ exports[`!top callback returns correct output 1`] = `
   "embeds": [
     {
       "color": 13407555,
-      "description": "
-For more information about The Odin Project, visit our site:
-[Your Career in Web Development Starts Here](https://www.theodinproject.com/)    
-    ",
+      "description": "For more information about The Odin Project, visit our site:
+[Your Career in Web Development Starts Here](https://www.theodinproject.com/)",
       "title": "The Odin Project",
     },
   ],
diff --git a/botCommands/code-legacy.test.js b/botCommands/code-legacy.test.js
index 0a17632..56cd68d 100644
--- a/botCommands/code-legacy.test.js
+++ b/botCommands/code-legacy.test.js
@@ -1,4 +1,4 @@
-const commands = require('./code-legacy');
+const command = require('./code-legacy');
 const { generateMentions } = require('./mockData');
 
 describe('!code', () => {
@@ -9,7 +9,7 @@ describe('!code', () => {
     [' !code @odin-bot'],
     ['!code fmdkslafmksa'],
   ])("'%s' triggers the callback", (string) => {
-    expect(commands.code.regex.test(string)).toBeTruthy();
+    expect(command.regex.test(string)).toBeTruthy();
   });
 
   it.each([
@@ -24,7 +24,7 @@ describe('!code', () => {
     ['!c*od&e^'],
     ['!^code'],
   ])("'%s' does not trigger the callback", (string) => {
-    expect(commands.code.regex.test(string)).toBeFalsy();
+    expect(command.regex.test(string)).toBeFalsy();
   });
 
   it.each([
@@ -33,7 +33,7 @@ describe('!code', () => {
     ['hey @odin-bot, teach me to !code'],
     ['!@odin-bot ^ !me !code !test$*'],
   ])("'%s' - the command can be anywhere in the string", (string) => {
-    expect(commands.code.regex.test(string)).toBeTruthy();
+    expect(command.regex.test(string)).toBeTruthy();
   });
 
   it.each([
@@ -47,16 +47,16 @@ describe('!code', () => {
   ])(
     "'%s' - command should be its own word!group - no leading or trailing characters",
     (string) => {
-      expect(commands.code.regex.test(string)).toBeFalsy();
+      expect(command.regex.test(string)).toBeFalsy();
     },
   );
 });
 
 describe('!code snapshot', () => {
   it('returns correct output', () => {
-    expect(commands.code.cb(generateMentions(0))).toMatchSnapshot();
-    expect(commands.code.cb(generateMentions(1))).toMatchSnapshot();
-    expect(commands.code.cb(generateMentions(2))).toMatchSnapshot();
-    expect(commands.code.cb(generateMentions(3))).toMatchSnapshot();
+    expect(command.cb(generateMentions(0))).toMatchSnapshot();
+    expect(command.cb(generateMentions(1))).toMatchSnapshot();
+    expect(command.cb(generateMentions(2))).toMatchSnapshot();
+    expect(command.cb(generateMentions(3))).toMatchSnapshot();
   });
 });
diff --git a/botCommands/points.test.js b/botCommands/points.test.js
index 1b6a796..a871526 100644
--- a/botCommands/points.test.js
+++ b/botCommands/points.test.js
@@ -1,6 +1,11 @@
 const axios = require('axios');
 const {
-  Guild, Channel, Client, User, Member,
+  Guild,
+  Channel,
+  Client,
+  Collection,
+  User,
+  Member,
 } = require('discord.js');
 const commands = require('./points');
 
@@ -23,6 +28,7 @@ jest.mock('../botEngine.js', () => ({
 }));
 
 jest.mock('discord.js', () => ({
+  ...jest.requireActual('discord.js'),
   Client: jest.fn().mockImplementation((users, channel, user) => ({
     channels: {
       cache: {
@@ -31,7 +37,8 @@ jest.mock('discord.js', () => ({
     },
     users: {
       cache: {
-        get: (userId) => users.filter((filteredUser) => `<@${userId}>` === filteredUser.id)[0],
+        get: (userId) =>
+          users.filter((filteredUser) => `<@${userId}>` === filteredUser.id)[0],
       },
     },
     user,
@@ -305,13 +312,7 @@ describe('callback', () => {
     const mentionedUser3 = User([], 4, 2);
     const mentionedUser4 = User([], 5, 0);
     const client = Client(
-      [
-        author,
-        mentionedUser1,
-        mentionedUser2,
-        mentionedUser3,
-        mentionedUser4,
-      ],
+      [author, mentionedUser1, mentionedUser2, mentionedUser3, mentionedUser4],
       channel,
     );
 
@@ -367,31 +368,21 @@ describe('callback', () => {
   describe('where one user is mentioned more than once', () => {
     it('returns correct output for only 1 user mentioned twice', async () => {
       const mentionedUser1 = User([], 2, 5);
-      const client = Client(
-        [
-          author,
-          mentionedUser1,
-        ],
-        channel,
-      );
+      const client = Client([author, mentionedUser1], channel);
       const data = {
         author,
         content: `${mentionedUser1.id} ++ ${mentionedUser1.id} ++`,
         channel,
         client,
-        guild: Guild([
-          author,
-          mentionedUser1,
-        ]),
+        guild: Guild([author, mentionedUser1]),
       };
 
-      axios.post
-        .mockResolvedValueOnce({
-          data: {
-            ...mentionedUser1,
-            points: (mentionedUser1.points += 1),
-          },
-        });
+      axios.post.mockResolvedValueOnce({
+        data: {
+          ...mentionedUser1,
+          points: (mentionedUser1.points += 1),
+        },
+      });
 
       await commands.awardPoints.cb(data);
 
@@ -402,32 +393,22 @@ describe('callback', () => {
 
     it('returns correct output for only 1 user mentioned more than 5 times', async () => {
       const mentionedUser1 = User([], 2, 5);
-      const client = Client(
-        [
-          author,
-          mentionedUser1,
-        ],
-        channel,
-      );
+      const client = Client([author, mentionedUser1], channel);
 
       const data = {
         author,
         content: `${mentionedUser1.id} ++ ${mentionedUser1.id} ++ ${mentionedUser1.id} ++ ${mentionedUser1.id} ++ ${mentionedUser1.id} ++ ${mentionedUser1.id} ++`,
         channel,
         client,
-        guild: Guild([
-          author,
-          mentionedUser1,
-        ]),
+        guild: Guild([author, mentionedUser1]),
       };
 
-      axios.post
-        .mockResolvedValueOnce({
-          data: {
-            ...mentionedUser1,
-            points: (mentionedUser1.points += 1),
-          },
-        });
+      axios.post.mockResolvedValueOnce({
+        data: {
+          ...mentionedUser1,
+          points: (mentionedUser1.points += 1),
+        },
+      });
 
       await commands.awardPoints.cb(data);
 
@@ -443,23 +424,14 @@ describe('callback', () => {
     it('returns correct output for 1 user mentioned more than once with another user', async () => {
       const mentionedUser1 = User([], 2, 21);
       const mentionedUser2 = User([], 3, 23);
-      const client = Client(
-        [
-          author,
-          mentionedUser1,
-        ],
-        channel,
-      );
+      const client = Client([author, mentionedUser1], channel);
 
       const data = {
         author,
         content: `${mentionedUser1.id} ++ ${mentionedUser1.id} ++ ${mentionedUser2.id} ++`,
         channel,
         client,
-        guild: Guild([
-          author,
-          mentionedUser1,
-        ]),
+        guild: Guild([author, mentionedUser1]),
       };
 
       axios.post
@@ -629,9 +601,7 @@ describe('callback', () => {
 
     await commands.awardPoints.cb(botSpamChannelData);
     expect(botSpamChannelData.channel.send).toHaveBeenCalled();
-    expect(
-      botSpamChannelData.channel.send.mock.calls[0][0],
-    ).toMatchSnapshot();
+    expect(botSpamChannelData.channel.send.mock.calls[0][0]).toMatchSnapshot();
 
     await commands.awardPoints.cb(bannedChannelData);
     expect(bannedChannelData.channel.send).toHaveBeenCalled();
@@ -645,9 +615,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a user who does not have an admin role', async () => {
     const mentionedUser = User([], 2, 20);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: '@everyone' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: '@everyone' });
+    const member = Member(memberCollection);
     // users must be passed in as an array
     const client = Client([author, mentionedUser], channel);
     const data = {
@@ -673,9 +643,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a single user w/o club-40', async () => {
     const mentionedUser = User([], 2, 20);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     // users must be passed in as an array
     const client = Client([author, mentionedUser], channel);
     const data = {
@@ -701,9 +671,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a single user entering club-40', async () => {
     const mentionedUser = User([], 2, 39);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const client = Client([author, mentionedUser], channel);
     const data = {
       author,
@@ -731,9 +701,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a single user re-entering club-40', async () => {
     const mentionedUser = User([], 2, 40);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const client = Client([author, mentionedUser], channel);
     const data = {
       author,
@@ -764,17 +734,11 @@ describe('?++ callback', () => {
     const mentionedUser2 = User([], 3, 21);
     const mentionedUser3 = User([], 4, 2);
     const mentionedUser4 = User([], 5, 0);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const client = Client(
-      [
-        author,
-        mentionedUser1,
-        mentionedUser2,
-        mentionedUser3,
-        mentionedUser4,
-      ],
+      [author, mentionedUser1, mentionedUser2, mentionedUser3, mentionedUser4],
       channel,
     );
 
@@ -833,9 +797,9 @@ describe('?++ callback', () => {
     const mentionedUser3 = User([], 4, 1);
     const mentionedUser4 = User([], 5, 0);
     const mentionedUser5 = User([], 6, 21);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const client = Client(
       [
         author,
@@ -908,9 +872,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a user mentioning themselves', async () => {
     const client = Client([author], channel);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const data = {
       author,
       content: `${author.id} ?++`,
@@ -935,9 +899,9 @@ describe('?++ callback', () => {
 
   it('returns correct output for a user mentioning Odin Bot', async () => {
     const odinBot = User([], 0, 0);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const client = Client([author, odinBot], channel, odinBot);
     const data = {
       author,
@@ -962,9 +926,9 @@ describe('?++ callback', () => {
       { virtual: true },
     );
     const mentionedUser = User([], 2, 20);
-    const memberMap = new Map();
-    memberMap.set('role-1', { name: 'core' });
-    const member = Member(memberMap);
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
     const botSpamChannel = Channel('513125912070455296');
     const bannedChannel = Channel('123456789');
     const client = Client([author, mentionedUser], botSpamChannel);
@@ -989,9 +953,7 @@ describe('?++ callback', () => {
 
     await commands.awardPoints.cb(botSpamChannelData);
     expect(botSpamChannelData.channel.send).toHaveBeenCalled();
-    expect(
-      botSpamChannelData.channel.send.mock.calls[0][0],
-    ).toMatchSnapshot();
+    expect(botSpamChannelData.channel.send.mock.calls[0][0]).toMatchSnapshot();
 
     await commands.awardPoints.cb(bannedChannelData);
     expect(bannedChannelData.channel.send).toHaveBeenCalled();
diff --git a/utils/is-admin.test.js b/utils/is-admin.test.js
new file mode 100644
index 0000000..dfca7a2
--- /dev/null
+++ b/utils/is-admin.test.js
@@ -0,0 +1,26 @@
+const { Collection, Member } = require('discord.js');
+const { isAdmin } = require('./is-admin')
+
+jest.mock('discord.js', () => ({
+  ...jest.requireActual('discord.js'),
+  Member: jest.fn().mockImplementation((roles) => ({
+    roles: {
+      cache: roles,
+    },
+  })),
+}));
+
+describe('isAdmin', () => {
+  it('should return true if the user has an admin role', () => {
+    const memberCollection = new Collection();
+    memberCollection.set('role-1', { name: 'core' });
+    const member = Member(memberCollection);
+    expect(isAdmin(member)).toBe(true);
+  });
+
+  it('should return false if the user does not have an admin role', () => {
+    const memberCollection = new Collection();
+    const member = Member(memberCollection);
+    expect(isAdmin(member)).toBe(false);
+  });
+});