Repository: TheOdinProject/odin-bot-v2
Pull request #328 (2023-05-24 13:52:03)

Feature: update faq service


<!-- Thank you for taking the time to contribute to The Odin Project. In order to get this pull request (PR) merged in a reasonable amount of time, you must complete this entire template. -->

## Because
<!-- Summarize the purpose or reasons for this PR, e.g. what problem it solves or what benefit it provides. -->

FAQs in #faq channel keep going out of sync with GitHub's version

## This PR
<!-- A bullet point list of one or more items describing the specific changes. -->

Create a slash command which updates #faq with latest ones in our GitHub

## Issue
<!--
If this PR closes an open issue in this repo, replace the XXXXX below with the issue number, e.g. Closes #2013.

If this PR closes an open issue in another TOP repo, replace the #XXXXX with the URL of the issue, e.g. Closes https://github.com/TheOdinProject/curriculum/issues/XXXXX

If this PR does not close, but is related to another issue or PR, you can link it as above without the 'Closes' keyword, e.g. 'Related to #2013'.
-->
Closes #XXXXX

## Additional Information
<!-- Any other information about this PR, such as a link to a Discord discussion. -->


## Pull Request Requirements
<!-- Replace the whitespace between the square brackets with an 'x', e.g. [x]. After you create the PR, they will become checkboxes that you can click on. -->
-   [ ] I have thoroughly read and understand [The Odin Project Contributing Guide](https://github.com/TheOdinProject/theodinproject/blob/main/CONTRIBUTING.md)
-   [ ] The title of this PR follows the `location of change: brief description of change` format, e.g. `Callbacks command: Update verbiage`
-   [ ] The `Because` section summarizes the reason for this PR
-   [ ] The `This PR` section has a bullet point list describing the changes in this PR
-   [ ] If this PR addresses an open issue, it is linked in the `Issue` section
-   [ ] If this PR adds new features or functionality, I have added new tests
-   [ ] If applicable, I have ensured all tests related to any command files included in this PR pass, and/or all snapshots are up to date



Patch:
diff --git a/config.js b/config.js
index 6576a31..bf9307f 100644
--- a/config.js
+++ b/config.js
@@ -12,6 +12,7 @@ const config = {
     introductionsChannelId: '690618925494566912',
     gettingHiredChannelId: process.env.DISCORD_GETTING_HIRED_CHANNEL_ID,
     botSpamPlaygroundChannelId: '513125912070455296',
+    FAQChannelId: '823266307293839401',
   },
   roles: {
     NOBOTRoleId: '783764176178774036',
diff --git a/new-era-commands/slash/update-faq.js b/new-era-commands/slash/update-faq.js
new file mode 100644
index 0000000..fa49413
--- /dev/null
+++ b/new-era-commands/slash/update-faq.js
@@ -0,0 +1,14 @@
+const {
+  SlashCommandBuilder, PermissionFlagsBits,
+} = require('discord.js');
+const UpdateFAQsService = require('../../services/update-faq/update-faq.service');
+
+module.exports = {
+  data: new SlashCommandBuilder()
+    .setName('updatefaq')
+    .setDescription('update FAQs in the #faq channel')
+    .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
+  execute: async (interaction) => {
+    await UpdateFAQsService.handleInteraction(interaction);
+  },
+};
diff --git a/services/update-faq/index.js b/services/update-faq/index.js
new file mode 100644
index 0000000..ef6ae81
--- /dev/null
+++ b/services/update-faq/index.js
@@ -0,0 +1,3 @@
+const UpdateFAQsService = require('./update-faq.service');
+
+module.exports = UpdateFAQsService;
diff --git a/services/update-faq/update-faq.service.js b/services/update-faq/update-faq.service.js
new file mode 100644
index 0000000..7f005ad
--- /dev/null
+++ b/services/update-faq/update-faq.service.js
@@ -0,0 +1,56 @@
+const { EmbedBuilder } = require('discord.js');
+const config = require('../../config');
+
+class UpdateFAQsService {
+  static EmbedDescriptionCharacterLimit = 4096;
+
+  static async handleInteraction(interaction) {
+    let rawFAQs;
+    try {
+      rawFAQs = await UpdateFAQsService.fetchFAQs();
+    } catch (error) {
+      console.log(error);
+      await interaction.reply('Failed to update FAQs');
+      return;
+    }
+
+    const FAQEmbeds = UpdateFAQsService.createFAQEmbeds(rawFAQs);
+    const FAQChannel = interaction.guild.channels.cache.get(config.channels.FAQChannelId);
+    await UpdateFAQsService.deletePreviousFAQs(FAQChannel);
+    await UpdateFAQsService.sendFAQs(FAQEmbeds, FAQChannel);
+    await interaction.reply('FAQs updated');
+  }
+
+  static async fetchFAQs() {
+    const response = await fetch('https://raw.githubusercontent.com/TheOdinProject/top-meta/main/FAQ.md');
+    const FAQs = await response.text();
+    return FAQs;
+  }
+
+  static createFAQEmbeds(rawFAQs) {
+    return UpdateFAQsService
+      .segments(rawFAQs, UpdateFAQsService.EmbedDescriptionCharacterLimit)
+      .map((chunk) => new EmbedBuilder().setColor('#cc9543').setDescription(chunk));
+  }
+
+  static async sendFAQs(FAQEmbeds, FAQChannel) {
+    await Promise.all(FAQEmbeds.map(async (e) => {
+      await FAQChannel.send({ embeds: [e] });
+    }));
+  }
+
+  static async deletePreviousFAQs(FAQChannel) {
+    const prev = await FAQChannel.messages.fetch({ limit: 10 });
+    prev.forEach((message) => message.delete());
+  }
+
+  static segments(string, length) {
+    const segments = [];
+    for (let i = 0; i < string.length; i += length) {
+      segments.push(string.substring(i, i + length));
+    }
+    return segments;
+  }
+}
+
+module.exports = UpdateFAQsService;


Test patch:
diff --git a/services/update-faq/update-faq.service.test.js b/services/update-faq/update-faq.service.test.js
new file mode 100644
index 0000000..7b25a03
--- /dev/null
+++ b/services/update-faq/update-faq.service.test.js
@@ -0,0 +1,33 @@
+const discordjs = require('discord.js');
+const UpdateFAQsService = require('./update-faq.service');
+
+jest.spyOn(discordjs, 'EmbedBuilder');
+
+describe('UpdateFAQsService', () => {
+  describe('segments', () => {
+    describe('when string size is divisible by segment length', () => {
+      it('segments string correctly', () => {
+        const string = 'The Odin Project';
+        const length = 2;
+        const result = UpdateFAQsService.segments(string, length);
+        expect(result).toEqual(['Th', 'e ', 'Od', 'in', ' P', 'ro', 'je', 'ct']);
+      });
+    });
+    describe('when string size is not divisible by segment length', () => {
+      it('segments string correctly', () => {
+        const string = 'The Odin Project';
+        const length = 3;
+        const result = UpdateFAQsService.segments(string, length);
+        expect(result).toEqual(['The', ' Od', 'in ', 'Pro', 'jec', 't']);
+      });
+    });
+    describe('when string size is 0', () => {
+      it('returns empty array', () => {
+        const string = '';
+        const length = 2;
+        const result = UpdateFAQsService.segments(string, length);
+        expect(result).toEqual([]);
+      });
+    });
+  });
+});