Repository: TheOdinProject/odin-bot-v2
Pull request #570 (2024-07-15 07:30:22)

Bump prettier from 3.3.1 to 3.3.3
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.1 to 3.3.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/prettier/prettier/releases">prettier's releases</a>.</em></p>
<blockquote>
<h2>3.3.3</h2>
<p>🔗 <a href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#333">Changelog</a></p>
<h2>3.3.2</h2>
<p>🔗 <a href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#332">Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's changelog</a>.</em></p>
<blockquote>
<h1>3.3.3</h1>
<p><a href="https://github.com/prettier/prettier/compare/3.3.2...3.3.3">diff</a></p>
<h4>Add parentheses for nullish coalescing in ternary (<a href="https://redirect.github.com/prettier/prettier/pull/16391">#16391</a> by <a href="https://github.com/cdignam-segment"><code>@​cdignam-segment</code></a>)</h4>
<p>This change adds clarity to operator precedence.</p>
<!-- raw HTML omitted -->
<pre lang="js"><code>// Input
foo ? bar ?? foo : baz;
foo ?? bar ? a : b;
a ? b : foo ?? bar;
<p>// Prettier 3.3.2<br />
foo ? bar ?? foo : baz;<br />
foo ?? bar ? a : b;<br />
a ? b : foo ?? bar;</p>
<p>// Prettier 3.3.3<br />
foo ? (bar ?? foo) : baz;<br />
(foo ?? bar) ? a : b;<br />
a ? b : (foo ?? bar);<br />
</code></pre></p>
<h4>Add parentheses for decorator expressions (<a href="https://redirect.github.com/prettier/prettier/pull/16458">#16458</a> by <a href="https://github.com/y-schneider"><code>@​y-schneider</code></a>)</h4>
<p>Prevent parentheses around member expressions or tagged template literals from being removed to follow the stricter parsing rules of TypeScript 5.5.</p>
<!-- raw HTML omitted -->
<pre lang="ts"><code>// Input
@(foo`tagged template`)
class X {}
<p>// Prettier 3.3.2<br />
<a href="https://github.com/foo"><code>@​foo</code></a><code>tagged template</code><br />
class X {}</p>
<p>// Prettier 3.3.3<br />
@(foo<code>tagged template</code>)<br />
class X {}<br />
</code></pre></p>
<h4>Support <code>@let</code> declaration syntax (<a href="https://redirect.github.com/prettier/prettier/pull/16474">#16474</a> by <a href="https://github.com/sosukesuzuki"><code>@​sosukesuzuki</code></a>)</h4>
<p>Adds support for Angular v18 <code>@let</code> declaration syntax.</p>
<p>Please see the following code example. The <code>@let</code> declaration allows you to define local variables within the template:</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/prettier/prettier/commit/52829385bcc4d785e58ae2602c0b098a643523c9"><code>5282938</code></a> Release 3.3.3</li>
<li><a href="https://github.com/prettier/prettier/commit/9102b73d8cd01795e7ba5af8a476d500c5dbcf2c"><code>9102b73</code></a> Add parentheses for decorator expressions (<a href="https://redirect.github.com/prettier/prettier/issues/16458">#16458</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/6bbd46194e4db25025afd6fe2f8506a8a2d1e47e"><code>6bbd461</code></a> chore(deps): update eslint related dependencies (<a href="https://redirect.github.com/prettier/prettier/issues/16478">#16478</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/04b560fc4386ee2ba1fc7c611c7a78655d6e60da"><code>04b560f</code></a> chore(deps): update dependency browserslist to v4.23.2 (<a href="https://redirect.github.com/prettier/prettier/issues/16475">#16475</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/614f070ac5f8d30628cc6cbbca1dda0e080e41d9"><code>614f070</code></a> chore(deps): update typescript-eslint to v8.0.0-alpha.41 (<a href="https://redirect.github.com/prettier/prettier/issues/16477">#16477</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/d59a15ded901c6dd66c07cf508e14646a6f12cd9"><code>d59a15d</code></a> chore(deps): update dependency npm-run-all2 to v6.2.2 (<a href="https://redirect.github.com/prettier/prettier/issues/16476">#16476</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/5ca2fc9be8369bf8c96899e2e5e8f3264086645a"><code>5ca2fc9</code></a> Support Angular <code>@let</code> declaration syntax (<a href="https://redirect.github.com/prettier/prettier/issues/16474">#16474</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/bfcfdccdf892141ce9cfbc179493557157c24e4c"><code>bfcfdcc</code></a> chore(deps): update babel to v7.24.8 (<a href="https://redirect.github.com/prettier/prettier/issues/16472">#16472</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/977c161924efa328660fe1802b604de2bc2527ea"><code>977c161</code></a> chore(deps): update dependency acorn to v8.12.1 (<a href="https://redirect.github.com/prettier/prettier/issues/16464">#16464</a>)</li>
<li><a href="https://github.com/prettier/prettier/commit/550606edf9f3c2e2d2c75f37f627c42030de34a3"><code>550606e</code></a> chore(deps): update babel to v7.24.8 (<a href="https://redirect.github.com/prettier/prettier/issues/16470">#16470</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/prettier/prettier/compare/3.3.1...3.3.3">compare view</a></li>
</ul>
</details>
<br />


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

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually 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)


</details>


Patch:
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/config.js b/config.js
index 5970139..880b12a 100644
--- a/config.js
+++ b/config.js
@@ -13,6 +13,9 @@ const config = {
     gettingHiredChannelId: '669547324707569665',
     botSpamPlaygroundChannelId: '513125912070455296',
     FAQChannelId: '823266307293839401',
+    WSLChannelId: '1179839248803844117',
+    ContactModeratorsChannelId: '1059513837197459547',
+    rulesChannelId: '693244715839127653',
   },
   roles: {
     NOBOTRoleId: '783764176178774036',
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
index 553d45d..fe4e554 100644
--- a/lib/commandsContent.js
+++ b/lib/commandsContent.js
@@ -1,12 +1,10 @@
-const { color } = require('../config');
-
+const { channels, color } = require('../config');
 
 const commandContents = {
-    code: {
-        color,
-        title: "How to share your code",
-        description:
-`
+  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:
@@ -26,18 +24,22 @@ 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
-`
-    },
-    os: {
-        color,
-        title: "Windows",
-        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>",
-        url: "https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/installation-overview#os-options",
-    },
-    question: {
-        color,
-        title: 'Asking Great Questions',
-        description: `
+`,
+  },
+  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:**
@@ -48,25 +50,25 @@ Asking context-rich questions makes it easy to receive help, and makes it easy f
 **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: <#1179839248803844117>"
-    },
-    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."
-    }
-
-
-}
+        `,
+  },
+  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/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 7dd06df..55483cb 100644
--- a/new-era-commands/slash/help.js
+++ b/new-era-commands/slash/help.js
@@ -1,4 +1,7 @@
 const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
+const config = require('../../config');
+
+const { channels } = config;
 
 module.exports = {
   data: new SlashCommandBuilder()
@@ -18,18 +21,17 @@ module.exports = {
       .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 <@575252669443211264>, as well as to ask any rule-related questions. Modmail How-To - <#1059513837197459547>',
+          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 - <#823266307293839401>!',
+          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 - <#513125912070455296>.',
+            `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",
diff --git a/package-lock.json b/package-lock.json
index 29f1266..7833dc0 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"
       },
@@ -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",
diff --git a/package.json b/package.json
index a9ead62..2055a37 100644
--- a/package.json
+++ b/package.json
@@ -20,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__/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/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);
+  });
+});