35 |
100 Days of Code Bot
36 |
37 | Documentation for 100 Days of Code bot in the freeCodeCamp Discord server.
38 |
39 |
40 |
41 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # 100 Days of Code Bot
2 |
3 | This is the documentation for the 100 Days of Code bot in the freeCodeCamp Discord server.
4 |
5 | Use the sidebar to navigate the available commands.
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "100-days-of-code-bot",
3 | "version": "2.0.0",
4 | "description": "A bot for tracking 100 Days of Code progress in Discord.",
5 | "main": "./prod/index.js",
6 | "scripts": {
7 | "prebuild": "rm -rf ./prod",
8 | "build": "tsc",
9 | "lint": "eslint ./src",
10 | "start": "node -r dotenv/config ./prod/index.js",
11 | "test": "echo 'No tests yet'.",
12 | "docs": "docsify serve ./docs -o --port 3200"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/nhcarrigan/100-days-of-code-bot.git"
17 | },
18 | "author": "Nicholas Carrigan",
19 | "license": "AGPL-3.0-or-later",
20 | "bugs": {
21 | "url": "https://github.com/nhcarrigan/100-days-of-code-bot/issues"
22 | },
23 | "homepage": "https://github.com/nhcarrigan/100-days-of-code-bot#readme",
24 | "engines": {
25 | "node": "16.15.1",
26 | "npm": "8.12.1"
27 | },
28 | "devDependencies": {
29 | "@types/node": "16.11.41",
30 | "@typescript-eslint/eslint-plugin": "5.20.0",
31 | "@typescript-eslint/parser": "5.20.0",
32 | "eslint": "8.17.0",
33 | "eslint-config-prettier": "8.5.0",
34 | "eslint-plugin-prettier": "4.0.0",
35 | "prettier": "2.6.2",
36 | "typescript": "4.7.4"
37 | },
38 | "dependencies": {
39 | "@discordjs/builders": "0.13.0",
40 | "@discordjs/rest": "0.4.1",
41 | "@sentry/integrations": "6.19.7",
42 | "@sentry/node": "6.19.7",
43 | "discord-api-types": "0.33.5",
44 | "discord.js": "13.7.0",
45 | "docsify-cli": "4.4.4",
46 | "dotenv": "16.0.1",
47 | "mongoose": "6.2.10",
48 | "winston": "3.7.2"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["github>freecodecamp/renovate-config"],
3 | "prCreation": "not-pending",
4 | "rangeStrategy": "pin"
5 | }
6 |
--------------------------------------------------------------------------------
/sample.env:
--------------------------------------------------------------------------------
1 | # Discord
2 | BOT_TOKEN=""
3 | GUILD_ID=""
4 |
5 | # Database
6 | MONGO_URI=""
7 |
8 | # Logging
9 | SENTRY_DSN=""
--------------------------------------------------------------------------------
/src/commands/_CommandList.ts:
--------------------------------------------------------------------------------
1 | import { CommandInt } from "../interfaces/CommandInt";
2 | import { edit } from "./edit";
3 | import { help } from "./help";
4 | import { oneHundred } from "./oneHundred";
5 | import { reset } from "./reset";
6 | import { view } from "./view";
7 |
8 | export const CommandList: CommandInt[] = [oneHundred, view, help, edit, reset];
9 |
--------------------------------------------------------------------------------
/src/commands/edit.ts:
--------------------------------------------------------------------------------
1 | import { SlashCommandBuilder } from "@discordjs/builders";
2 | import { CommandInt } from "../interfaces/CommandInt";
3 | import { errorHandler } from "../utils/errorHandler";
4 |
5 | export const edit: CommandInt = {
6 | data: new SlashCommandBuilder()
7 | .setName("edit")
8 | .setDescription("Edit a previous 100 days of code post.")
9 | .addStringOption((option) =>
10 | option
11 | .setName("embed-id")
12 | .setDescription("ID of the message to edit.")
13 | .setRequired(true)
14 | )
15 | .addStringOption((option) =>
16 | option
17 | .setName("message")
18 | .setDescription("The message to go in your 100 Days of Code update.")
19 | .setRequired(true)
20 | ) as SlashCommandBuilder,
21 | run: async (interaction) => {
22 | try {
23 | await interaction.deferReply();
24 | const { channel, user } = interaction;
25 | const targetId = interaction.options.getString("embed-id");
26 | const text = interaction.options.getString("message");
27 |
28 | if (!text || !targetId || !channel) {
29 | await interaction.editReply({
30 | content: "Missing required parameters...",
31 | });
32 | return;
33 | }
34 |
35 | const targetMessage = await channel.messages.fetch(targetId);
36 |
37 | if (!targetMessage) {
38 | await interaction.editReply({
39 | content:
40 | "That does not appear to be a valid message ID. Be sure that the message is in the same channel you are using this command.",
41 | });
42 | return;
43 | }
44 |
45 | const targetEmbed = targetMessage.embeds[0];
46 |
47 | if (
48 | targetEmbed.author?.name !==
49 | user.username + "#" + user.discriminator
50 | ) {
51 | await interaction.editReply(
52 | "This does not appear to be your 100 Days of Code post. You cannot edit it."
53 | );
54 | return;
55 | }
56 |
57 | targetEmbed.setDescription(text);
58 |
59 | await targetMessage.edit({ embeds: [targetEmbed] });
60 | await interaction.editReply({ content: "Updated!" });
61 | } catch (err) {
62 | errorHandler("edit command", err);
63 | }
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/src/commands/help.ts:
--------------------------------------------------------------------------------
1 | import { SlashCommandBuilder } from "@discordjs/builders";
2 | import { MessageEmbed } from "discord.js";
3 | import { CommandInt } from "../interfaces/CommandInt";
4 | import { errorHandler } from "../utils/errorHandler";
5 |
6 | export const help: CommandInt = {
7 | data: new SlashCommandBuilder()
8 | .setName("help")
9 | .setDescription("Provides information on using this bot."),
10 | run: async (interaction) => {
11 | try {
12 | await interaction.deferReply();
13 | const helpEmbed = new MessageEmbed();
14 | helpEmbed.setTitle("100 Days of Code Bot!");
15 | helpEmbed.setDescription(
16 | "This discord bot is designed to help you track and share your 100 Days of Code progress."
17 | );
18 | helpEmbed.addField(
19 | "Create today's update",
20 | "Use the `/100` command to create your update for today. The `message` will be displayed in your embed."
21 | );
22 | helpEmbed.addField(
23 | "Edit today's update",
24 | "Do you see a typo in your embed? Right click it and copy the ID (you may need developer mode on for this), and use the `/edit` command to update that embed with a new message."
25 | );
26 | helpEmbed.addField(
27 | "Show your progress",
28 | "To see your current progress in the challenge, and the day you last checked in, use `/view`."
29 | );
30 | helpEmbed.setFooter(`Version ${process.env.npm_package_version}`);
31 | await interaction.editReply({ embeds: [helpEmbed] });
32 | return;
33 | } catch (err) {
34 | errorHandler("help command", err);
35 | }
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/src/commands/oneHundred.ts:
--------------------------------------------------------------------------------
1 | import { SlashCommandBuilder } from "@discordjs/builders";
2 | import { MessageEmbed } from "discord.js";
3 | import { CommandInt } from "../interfaces/CommandInt";
4 | import { getCamperData } from "../modules/getCamperData";
5 | import { updateCamperData } from "../modules/updateCamperData";
6 | import { errorHandler } from "../utils/errorHandler";
7 |
8 | export const oneHundred: CommandInt = {
9 | data: new SlashCommandBuilder()
10 | .setName("100")
11 | .setDescription("Check in for the 100 Days of Code challenge.")
12 | .addStringOption((option) =>
13 | option
14 | .setName("message")
15 | .setDescription("The message to go in your 100 Days of Code update.")
16 | .setRequired(true)
17 | ) as SlashCommandBuilder,
18 | run: async (interaction) => {
19 | try {
20 | await interaction.deferReply();
21 | const { user } = interaction;
22 | const text = interaction.options.getString("message");
23 |
24 | if (!text) {
25 | await interaction.editReply({
26 | content: "The message argument is required.",
27 | });
28 | return;
29 | }
30 | const targetCamper = await getCamperData(user.id);
31 |
32 | if (!targetCamper) {
33 | await interaction.editReply({
34 | content:
35 | "There is an error with the database lookup. Please try again later.",
36 | });
37 | return;
38 | }
39 |
40 | const updatedCamper = await updateCamperData(targetCamper);
41 |
42 | if (!updatedCamper) {
43 | await interaction.editReply({
44 | content:
45 | "There is an error with the database update. Please try again later.",
46 | });
47 | return;
48 | }
49 |
50 | const oneHundredEmbed = new MessageEmbed();
51 | oneHundredEmbed.setTitle("100 Days of Code");
52 | oneHundredEmbed.setDescription(text);
53 | oneHundredEmbed.setAuthor(
54 | user.username + "#" + user.discriminator,
55 | user.displayAvatarURL()
56 | );
57 | oneHundredEmbed.addField("Round", updatedCamper.round.toString(), true);
58 | oneHundredEmbed.addField("Day", updatedCamper.day.toString(), true);
59 | oneHundredEmbed.setFooter(
60 | "Day completed: " +
61 | new Date(updatedCamper.timestamp).toLocaleDateString()
62 | );
63 |
64 | await interaction.editReply({ embeds: [oneHundredEmbed] });
65 | } catch (err) {
66 | errorHandler("100 command", err);
67 | }
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/src/commands/reset.ts:
--------------------------------------------------------------------------------
1 | import { SlashCommandBuilder } from "@discordjs/builders";
2 | import { Message, MessageActionRow, MessageButton } from "discord.js";
3 | import { CommandInt } from "../interfaces/CommandInt";
4 | import { getCamperData } from "../modules/getCamperData";
5 | import { errorHandler } from "../utils/errorHandler";
6 |
7 | export const reset: CommandInt = {
8 | data: new SlashCommandBuilder()
9 | .setName("reset")
10 | .setDescription("Reset your 100 Days of Code progress."),
11 | run: async (interaction) => {
12 | try {
13 | await interaction.deferReply();
14 | const { user } = interaction;
15 | const confirmButton = new MessageButton()
16 | .setCustomId("confirm")
17 | .setLabel("Confirm")
18 | .setEmoji("✅")
19 | .setStyle("PRIMARY");
20 | const row = new MessageActionRow().addComponents([confirmButton]);
21 | const response = (await interaction.editReply({
22 | content:
23 | "This will delete all of your stored 100 Days of Code progress. You will start at Round 1 Day 1. If you are sure this is what you want to do, click the button below.",
24 | components: [row],
25 | })) as Message;
26 |
27 | const collector = response.createMessageComponentCollector({
28 | time: 30000,
29 | filter: (click) => click.user.id === user.id,
30 | max: 1,
31 | });
32 |
33 | collector.on("collect", async (click) => {
34 | await click.deferUpdate();
35 | const camperData = await getCamperData(user.id);
36 |
37 | if (!camperData) {
38 | await interaction.editReply({
39 | content: "There was an error fetching your data.",
40 | });
41 | return;
42 | }
43 |
44 | await camperData.delete();
45 |
46 | await interaction.editReply({
47 | content: "Your 100 Days of Code progress has been reset.",
48 | });
49 | return;
50 | });
51 |
52 | collector.on("end", async () => {
53 | const disabledButton = confirmButton.setDisabled(true);
54 | const row = new MessageActionRow().addComponents([disabledButton]);
55 | await interaction.editReply({
56 | components: [row],
57 | });
58 | });
59 | } catch (err) {
60 | errorHandler("reset command", err);
61 | }
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/src/commands/view.ts:
--------------------------------------------------------------------------------
1 | import { SlashCommandBuilder } from "@discordjs/builders";
2 | import { MessageEmbed } from "discord.js";
3 | import { CommandInt } from "../interfaces/CommandInt";
4 | import { getCamperData } from "../modules/getCamperData";
5 | import { errorHandler } from "../utils/errorHandler";
6 |
7 | export const view: CommandInt = {
8 | data: new SlashCommandBuilder()
9 | .setName("view")
10 | .setDescription("Shows your latest 100 days of code check in."),
11 | run: async (interaction) => {
12 | try {
13 | await interaction.deferReply();
14 | const { user } = interaction;
15 | const targetCamper = await getCamperData(user.id);
16 |
17 | if (!targetCamper) {
18 | await interaction.editReply({
19 | content:
20 | "There was an error with the database lookup. Please try again later.",
21 | });
22 | return;
23 | }
24 |
25 | if (!targetCamper.day) {
26 | await interaction.editReply({
27 | content:
28 | "It looks like you have not started the 100 Days of Code challenge yet. Use `/100` and add your message to report your first day!",
29 | });
30 | return;
31 | }
32 |
33 | const camperEmbed = new MessageEmbed();
34 | camperEmbed.setTitle("My 100DoC Progress");
35 | camperEmbed.setDescription(
36 | `Here is my 100 Days of Code progress. I last reported an update on ${new Date(
37 | targetCamper.timestamp
38 | ).toLocaleDateString()}.`
39 | );
40 | camperEmbed.addField("Round", targetCamper.round.toString(), true);
41 | camperEmbed.addField("Day", targetCamper.day.toString(), true);
42 | camperEmbed.setAuthor(
43 | user.username + "#" + user.discriminator,
44 | user.displayAvatarURL()
45 | );
46 |
47 | await interaction.editReply({ embeds: [camperEmbed] });
48 | } catch (err) {
49 | errorHandler("view command", err);
50 | }
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/src/config/IntentOptions.ts:
--------------------------------------------------------------------------------
1 | import { IntentsString } from "discord.js";
2 |
3 | export const IntentOptions: IntentsString[] = ["GUILDS", "GUILD_MESSAGES"];
4 |
--------------------------------------------------------------------------------
/src/database/connectDatabase.ts:
--------------------------------------------------------------------------------
1 | import { connect } from "mongoose";
2 | import { errorHandler } from "../utils/errorHandler";
3 | import { logHandler } from "../utils/logHandler";
4 |
5 | export const connectDatabase = async (): Promise