diff --git a/packages/bot/.nvmrc b/packages/bot/.nvmrc
new file mode 100644
index 0000000..0a47c85
--- /dev/null
+++ b/packages/bot/.nvmrc
@@ -0,0 +1 @@
+lts/iron
\ No newline at end of file
diff --git a/packages/bot/Dockerfile b/packages/bot/Dockerfile
new file mode 100644
index 0000000..0d65757
--- /dev/null
+++ b/packages/bot/Dockerfile
@@ -0,0 +1,19 @@
+FROM node:20-alpine AS base
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+WORKDIR /app
+RUN corepack enable
+
+FROM base AS build
+COPY ./packages/bot/package.json ./
+COPY ./packages/bot/src ./
+RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install
+
+FROM build AS dev
+ENTRYPOINT ["pnpm"]
+CMD ["run", "dev"]
+
+FROM build AS run
+ENTRYPOINT ["pnpm"]
+CMD ["start"]
+
diff --git a/packages/bot/components.js b/packages/bot/components.js
new file mode 100644
index 0000000..fec32f5
--- /dev/null
+++ b/packages/bot/components.js
@@ -0,0 +1,106 @@
+const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
+
+const client = new Client({
+ auth: `Bot ${process.env.DISCORD_TOKEN}`,
+ gateway: {
+ intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
+ }
+});
+
+client.on("ready", () => console.log("Ready as", client.user.tag));
+
+client.on("messageCreate", async (msg) => {
+ if(msg.content.includes("!component")) {
+ await client.rest.channels.createMessage(msg.channelID, {
+ content: `Here's some buttons for you, ${msg.author.mention}.`,
+ components: [
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
+ customID: "some-string-you-will-see-later",
+ label: "Click!",
+ disabled: false, // If the button is disabled, false by default.
+ },
+ {
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.PRIMARY,
+ customID: "some-other-string",
+ label: "This Is Disabled",
+ disabled: true
+ },
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.LINK,
+ label: "Open Link",
+ url: "https://docs.oceanic.ws"
+ }
+ ]
+ },
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
+ type: ComponentTypes.STRING_SELECT,
+ customID: "string-select",
+ disabled: false,
+ maxValues: 1, // The maximum number of values that can be selected (default 1)
+ minValues: 1, // The minimum number of values that can be selected (default 1)
+ options: [
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
+ {
+ default: true, // If this option is selected by default
+ description: "The description of the option", // Optional description
+ label: "Option One",
+ value: "value-1"
+ },
+ {
+ label: "Option Two",
+ value: "option-2"
+ }
+ ],
+ placeholder: "Some Placeholder Text"
+ }
+ ]
+ },
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
+ type: ComponentTypes.CHANNEL_SELECT,
+ channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
+ customID: "channel-select",
+ disabled: false,
+ maxValues: 1, // The maximum number of values that can be selected (default 1)
+ minValues: 1, // The minimum number of values that can be selected (default 1)
+ placeholder: "Some Placeholder Text"
+ }
+ ]
+ }
+ ]
+ });
+ }
+});
+
+// An error handler
+client.on("error", (error) => {
+ console.error("Something went wrong:", error);
+});
+
+// Connect to Discord
+client.connect();
\ No newline at end of file
diff --git a/packages/bot/embeds.js b/packages/bot/embeds.js
new file mode 100644
index 0000000..1b6f1b7
--- /dev/null
+++ b/packages/bot/embeds.js
@@ -0,0 +1,94 @@
+const { Client } = require("oceanic.js");
+const { readFileSync } = require("fs");
+
+const client = new Client({
+ auth: `Bot ${process.env.DISCORD_TOKEN}`,
+ gateway: {
+ intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
+ }
+});
+
+client.on("ready", () => console.log("Ready as", client.user.tag));
+
+client.on("messageCreate", async (msg) => {
+ if(msg.content.includes("!embed")) {
+ console.log(`'!embeds' was seen in chat!`)
+ console.log(msg)
+ await client.rest.channels.createMessage(msg.channelID, {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
+ // Up to 10 in one message
+ embeds: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedAuthorOptions.html
+ author: {
+ name: "Author Name",
+ // An image url, or attachment://filename.ext
+ iconURL: "https://i.furry.cool/DonPride.png", // Optional
+ url: "https://docs.oceanic.ws" // Optional
+ },
+ // Array of https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedField.html
+ // Up to 25 in one message
+ fields: [
+ {
+ name: "Field One",
+ value: "Field One Value",
+ inline: true // If this field should be displayed inline (default: false)
+ },
+ {
+ name: "Field Two",
+ value: "Field Two Value",
+ inline: false
+ }
+ ],
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedFooterOptions.html
+ footer: {
+ text: "Footer Text",
+ // An image url, or attachment://filename.ext
+ iconURL: "https://i.furry.cool/DonPride.png" // Optional
+ },
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedImageOptions.html
+ image: {
+ // An image url, or attachment://filename.ext
+ url: "https://i.furry.cool/DonPride.png"
+ },
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedThumbnailOptions.html
+ thumbnail: {
+ // An image url, or attachment://filename.ext
+ url: "https://i.furry.cool/DonPride.png"
+ },
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
+ color: 0xFFA500, // Base-10 color (0x prefix can be used for hex codes)
+ description: "My Cool Embed",
+ timestamp: new Date().toISOString(), // The current time - ISO 8601 format
+ title: "My Amazing Embed",
+ url: "https://docs.oceanic.ws"
+ }
+ ]
+ });
+ } else if(msg.content.includes("!file")) {
+ await client.rest.channels.createMessage(msg.channelID, {
+ embeds: [
+ {
+ image: {
+ // This can also be used for author & footer images
+ url: "attachment://image.png"
+ }
+ }
+ ],
+ files: [
+ {
+ name: "image.png",
+ contents: readFileSync(`${__dirname}/image.png`)
+ }
+ ]
+ });
+ }
+});
+
+// An error handler
+client.on("error", (error) => {
+ console.error("Something went wrong:", error);
+});
+
+// Connect to Discord
+client.connect();
\ No newline at end of file
diff --git a/packages/bot/futurebutt.js b/packages/bot/futurebutt.js
new file mode 100644
index 0000000..b9fe9e2
--- /dev/null
+++ b/packages/bot/futurebutt.js
@@ -0,0 +1,261 @@
+const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
+
+const client = new Client({
+ auth: `Bot ${process.env.DISCORD_TOKEN}`,
+ gateway: {
+ intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
+ }
+});
+
+client.on("ready", () => console.log("Ready as", client.user.tag));
+
+client.on("messageCreate", async (msg) => {
+ console.log(msg.content)
+ if(msg.content.includes("!test")) {
+ await client.rest.channels.createMessage(msg.channelID, {
+ content: `HGERE IZ BUTTN'z 5 u, ${msg.author.mention}.`,
+ components: [
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
+ customID: "some-string-you-will-see-later",
+ label: "Click!",
+ disabled: false, // If the button is disabled, false by default.
+ },
+ {
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.PRIMARY,
+ customID: "some-other-string",
+ label: "This Is Disabled",
+ disabled: true
+ },
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
+ type: ComponentTypes.BUTTON,
+ style: ButtonStyles.LINK,
+ label: "Open Link",
+ url: "https://docs.oceanic.ws"
+ }
+ ]
+ },
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
+ type: ComponentTypes.STRING_SELECT,
+ customID: "string-select",
+ disabled: false,
+ maxValues: 1, // The maximum number of values that can be selected (default 1)
+ minValues: 1, // The minimum number of values that can be selected (default 1)
+ options: [
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
+ {
+ default: true, // If this option is selected by default
+ description: "The description of the option", // Optional description
+ label: "Option One",
+ value: "value-1"
+ },
+ {
+ label: "Option Two",
+ value: "option-2"
+ }
+ ],
+ placeholder: "Some Placeholder Text"
+ }
+ ]
+ },
+ {
+ // The top level component must always be an action row.
+ // Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
+ type: ComponentTypes.ACTION_ROW,
+ components: [
+ {
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
+ type: ComponentTypes.CHANNEL_SELECT,
+ channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
+ customID: "channel-select",
+ disabled: false,
+ maxValues: 1, // The maximum number of values that can be selected (default 1)
+ minValues: 1, // The minimum number of values that can be selected (default 1)
+ placeholder: "Some Placeholder Text"
+ }
+ ]
+ }
+ ]
+ });
+ }
+});
+
+
+client.on("interactionCreate", async(interaction) => {
+ console.log(`interaction!@`)
+ console.log(interaction)
+ switch(interaction.type) {
+ // https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
+ case InteractionTypes.APPLICATION_COMMAND: {
+ // defer interactions as soon as possible, you have three seconds to send any initial response
+ // if you wait too long, the interaction may be invalidated
+ await interaction.defer();
+ // If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
+ // await interaction.defer(MessageFlags.EPHEMERAL);
+
+ // data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
+ switch(interaction.data.type) {
+ // Chat Input commands are what you use in the chat, i.e. slash commands
+ case ApplicationCommandTypes.CHAT_INPUT: {
+ if(interaction.data.name === "greet") {
+ // assume we have two options, user (called user) then string (called greeting) - first is required, second is not
+
+ // Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
+ // Setting the second parameter to true will throw an error if the option is not present
+ const user = interaction.data.options.getUser("user", true);
+ const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
+
+ // since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
+ // we can only have one initial response, so we use createFollowup
+ await interaction.createFollowup({
+ content: `${greeting} ${user.mention}!`,
+ allowedMentions: {
+ users: [user.id]
+ }
+ });
+ }
+
+ // Chat Input application command interactions also have a set of resolved data, which is structured as so:
+ // https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
+ // the options wrapper pulls values out of resolved automatically, if you use the right method
+ break;
+ }
+
+ // User application commands are shown in the context menu when right-clicking on users
+ // `data` will have a target (and targetID) property with the user that the command was executed on
+ // These don't have options
+ case ApplicationCommandTypes.USER: {
+ if(interaction.data.name === "ping") {
+ await interaction.createFollowup({
+ content: `Pong! ${interaction.data.target.mention}`,
+ allowedMentions: {
+ users: [interaction.data.target.id]
+ }
+ });
+ }
+ break;
+ }
+
+ // Message application commands are shown in the context menu when right-clicking on messages
+ // `data` will have a target (and targetID) property with the message that the command was executed on
+ // Same as user commands, these don't have options
+ case ApplicationCommandTypes.MESSAGE: {
+ if(interaction.data.name === "author") {
+ await interaction.createFollowup({
+ content: `${interaction.data.target.author.mention} is the author of that message!`,
+ allowedMentions: {
+ users: [interaction.data.target.author.id]
+ }
+ });
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
+ case InteractionTypes.MESSAGE_COMPONENT: {
+ // same spiel as above
+ await interaction.defer();
+ // when you create a message with components, this will correspond with what you provided as the customID there
+ if(interaction.data.componentType === ComponentTypes.BUTTON) {
+ if(interaction.data.customID === "edit-message") {
+ // Edits the original message. This has an initial response variant: editParent
+ await interaction.editOriginal({
+ content: `This message was edited by ${interaction.user.mention}!`,
+ allowedMentions: {
+ users: [interaction.user.id]
+ }
+ });
+ } else if(interaction.data.customID === "my-amazing-button") {
+ await interaction.createFollowup({
+ content: "You clicked an amazing button!"
+ });
+ }
+ } else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
+ // The `values` property under data contains all the selected values
+ await interaction.createFollowup({
+ content: `You selected: **${interaction.data.values.join("**, **")}**`
+ });
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
+ case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
+ // Autocomplete Interactions cannot be deferred
+ switch(interaction.data.name) {
+ case "test-autocomplete": {
+ // Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
+ // along with one at the end, which will have focused
+ // Setting the first parameter to true will throw an error if no focused option is present
+ const option = interaction.data.options.getFocused(true);
+ switch(option.name) {
+ case "test-option": {
+ return interaction.result([
+ {
+ name: "Choice 1",
+ nameLocalizations: {
+ "es-ES": "Opción 1"
+ },
+ value: "choice-1"
+ },
+ {
+ name: "Choice 2",
+ nameLocalizations: {
+ "es-ES": "Opción 2"
+ },
+ value: "choice-2"
+ }
+ ]);
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
+ case InteractionTypes.MODAL_SUBMIT: {
+ // this will correspond with the customID you provided when creating the modal
+ switch(interaction.data.customID) {
+ case "test-modal": {
+ // the `components` property under data contains all the components that were submitted
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
+ console.log(interaction.data.components);
+ break;
+ }
+ }
+ break;
+ }
+ }
+});
+
+
+// An error handler
+client.on("error", (error) => {
+ console.error("Something went wrong:", error);
+});
+
+// Connect to Discord
+client.connect();
\ No newline at end of file
diff --git a/packages/bot/interactions.js b/packages/bot/interactions.js
new file mode 100644
index 0000000..91940a4
--- /dev/null
+++ b/packages/bot/interactions.js
@@ -0,0 +1,171 @@
+const { Client, InteractionTypes, MessageFlags, ComponentTypes, ApplicationCommandTypes } = require("oceanic.js");
+
+const client = new Client({
+ auth: `Bot ${process.env.DISCORD_TOKEN}`,
+ gateway: {
+ intents: 0 // No intents are needed if you are only using interactions
+ }
+});
+
+
+client.on("ready", async() => {
+ console.log("Ready as", client.user.tag);
+});
+
+client.on("interactionCreate", async(interaction) => {
+ switch(interaction.type) {
+ // https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
+ case InteractionTypes.APPLICATION_COMMAND: {
+ // defer interactions as soon as possible, you have three seconds to send any initial response
+ // if you wait too long, the interaction may be invalidated
+ await interaction.defer();
+ // If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
+ // await interaction.defer(MessageFlags.EPHEMERAL);
+
+ // data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
+ switch(interaction.data.type) {
+ // Chat Input commands are what you use in the chat, i.e. slash commands
+ case ApplicationCommandTypes.CHAT_INPUT: {
+ if(interaction.data.name === "greet") {
+ // assume we have two options, user (called user) then string (called greeting) - first is required, second is not
+
+ // Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
+ // Setting the second parameter to true will throw an error if the option is not present
+ const user = interaction.data.options.getUser("user", true);
+ const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
+
+ // since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
+ // we can only have one initial response, so we use createFollowup
+ await interaction.createFollowup({
+ content: `${greeting} ${user.mention}!`,
+ allowedMentions: {
+ users: [user.id]
+ }
+ });
+ }
+
+ // Chat Input application command interactions also have a set of resolved data, which is structured as so:
+ // https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
+ // the options wrapper pulls values out of resolved automatically, if you use the right method
+ break;
+ }
+
+ // User application commands are shown in the context menu when right-clicking on users
+ // `data` will have a target (and targetID) property with the user that the command was executed on
+ // These don't have options
+ case ApplicationCommandTypes.USER: {
+ if(interaction.data.name === "ping") {
+ await interaction.createFollowup({
+ content: `Pong! ${interaction.data.target.mention}`,
+ allowedMentions: {
+ users: [interaction.data.target.id]
+ }
+ });
+ }
+ break;
+ }
+
+ // Message application commands are shown in the context menu when right-clicking on messages
+ // `data` will have a target (and targetID) property with the message that the command was executed on
+ // Same as user commands, these don't have options
+ case ApplicationCommandTypes.MESSAGE: {
+ if(interaction.data.name === "author") {
+ await interaction.createFollowup({
+ content: `${interaction.data.target.author.mention} is the author of that message!`,
+ allowedMentions: {
+ users: [interaction.data.target.author.id]
+ }
+ });
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
+ case InteractionTypes.MESSAGE_COMPONENT: {
+ // same spiel as above
+ await interaction.defer();
+ // when you create a message with components, this will correspond with what you provided as the customID there
+ if(interaction.data.componentType === ComponentTypes.BUTTON) {
+ if(interaction.data.customID === "edit-message") {
+ // Edits the original message. This has an initial response variant: editParent
+ await interaction.editOriginal({
+ content: `This message was edited by ${interaction.user.mention}!`,
+ allowedMentions: {
+ users: [interaction.user.id]
+ }
+ });
+ } else if(interaction.data.customID === "my-amazing-button") {
+ await interaction.createFollowup({
+ content: "You clicked an amazing button!"
+ });
+ }
+ } else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
+ // The `values` property under data contains all the selected values
+ await interaction.createFollowup({
+ content: `You selected: **${interaction.data.values.join("**, **")}**`
+ });
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
+ case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
+ // Autocomplete Interactions cannot be deferred
+ switch(interaction.data.name) {
+ case "test-autocomplete": {
+ // Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
+ // along with one at the end, which will have focused
+ // Setting the first parameter to true will throw an error if no focused option is present
+ const option = interaction.data.options.getFocused(true);
+ switch(option.name) {
+ case "test-option": {
+ return interaction.result([
+ {
+ name: "Choice 1",
+ nameLocalizations: {
+ "es-ES": "Opción 1"
+ },
+ value: "choice-1"
+ },
+ {
+ name: "Choice 2",
+ nameLocalizations: {
+ "es-ES": "Opción 2"
+ },
+ value: "choice-2"
+ }
+ ]);
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ // https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
+ case InteractionTypes.MODAL_SUBMIT: {
+ // this will correspond with the customID you provided when creating the modal
+ switch(interaction.data.customID) {
+ case "test-modal": {
+ // the `components` property under data contains all the components that were submitted
+ // https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
+ console.log(interaction.data.components);
+ break;
+ }
+ }
+ break;
+ }
+ }
+});
+
+// An error handler
+client.on("error", (error) => {
+ console.error("Something went wrong:", error);
+});
+
+// Connect to Discord
+client.connect();
\ No newline at end of file
diff --git a/packages/bot/package.json b/packages/bot/package.json
new file mode 100644
index 0000000..a5d669c
--- /dev/null
+++ b/packages/bot/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "fp-bot",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.ts",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "node --import=tsx --watch ./src/index.ts"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "v20.x.x",
+ "npm": ">=6.x.x"
+ },
+ "devDependencies": {
+ "tsx": "^4.7.0"
+ },
+ "dependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "^20.11.0",
+ "discordeno": "^18.0.1",
+ "express": "^4.18.2",
+ "oceanic.js": "^1.9.0"
+ }
+}
diff --git a/packages/bot/src/configs.ts b/packages/bot/src/configs.ts
new file mode 100644
index 0000000..6deb9c9
--- /dev/null
+++ b/packages/bot/src/configs.ts
@@ -0,0 +1,28 @@
+import { getBotIdFromToken, Intents } from 'discordeno';
+
+
+/** The bot id, derived from the bot token. */
+export const BOT_ID = getBotIdFromToken(process.env.DISCORD_TOKEN as string);
+export const EVENT_HANDLER_URL = `http://${process.env.EVENT_HANDLER_HOST}:${process.env.EVENT_HANDLER_PORT}`;
+export const REST_URL = `http://${process.env.REST_HOST}:${process.env.REST_PORT}`;
+export const GATEWAY_URL = `http://${process.env.GATEWAY_HOST}:${process.env.GATEWAY_PORT}`;
+
+// Gateway Proxy Configurations
+/** The gateway intents you would like to use. */
+export const INTENTS: Intents =
+ // SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
+ Intents.DirectMessageReactions |
+ Intents.DirectMessageTyping |
+ Intents.DirectMessages |
+ Intents.GuildBans |
+ Intents.GuildEmojis |
+ Intents.GuildIntegrations |
+ Intents.GuildInvites |
+ Intents.GuildMembers |
+ Intents.GuildMessageReactions |
+ Intents.GuildMessageTyping |
+ Intents.GuildMessages |
+ Intents.GuildPresences |
+ Intents.GuildVoiceStates |
+ Intents.GuildWebhooks |
+ Intents.Guilds;
\ No newline at end of file
diff --git a/packages/bot/src/index.ts b/packages/bot/src/index.ts
new file mode 100644
index 0000000..ac2c148
--- /dev/null
+++ b/packages/bot/src/index.ts
@@ -0,0 +1,58 @@
+
+import { BASE_URL, createRestManager } from 'discordeno';
+import express, { Request, Response } from 'express';
+// import { setupAnalyticsHooks } from '../analytics.js';
+import { REST_URL } from './configs.js';
+
+const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
+const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
+const REST_PORT = process.env.REST_PORT as string;
+
+const rest = createRestManager({
+ token: DISCORD_TOKEN,
+ secretKey: REST_AUTHORIZATION,
+ customUrl: REST_URL,
+ debug: console.log,
+});
+
+// Add send fetching analytics hook to rest
+// setupAnalyticsHooks(rest);
+
+// @ts-expect-error
+rest.convertRestError = (errorStack, data) => {
+ if (!data) return { message: errorStack.message };
+ return { ...data, message: errorStack.message };
+};
+
+const app = express();
+
+app.use(
+ express.urlencoded({
+ extended: true,
+ }),
+);
+
+app.use(express.json());
+
+app.all('/*', async (req: Request, res: Response) => {
+ if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
+ return res.status(401).json({ error: 'Invalid authorization key.' });
+ }
+
+ try {
+ const result = await rest.runMethod(rest, req.method, `${BASE_URL}${req.url}`, req.body);
+
+ if (result) {
+ res.status(200).json(result);
+ } else {
+ res.status(204).json();
+ }
+ } catch (error: any) {
+ console.log(error);
+ res.status(500).json(error);
+ }
+});
+
+app.listen(REST_PORT, () => {
+ console.log(`REST listening at ${REST_URL}`);
+});
\ No newline at end of file
diff --git a/packages/bot/src/rest/README.md b/packages/bot/src/rest/README.md
new file mode 100644
index 0000000..fedd9ca
--- /dev/null
+++ b/packages/bot/src/rest/README.md
@@ -0,0 +1,9 @@
+# REST Proxy
+
+This folder will contain the code for our REST proxy. This is going to become the single source that all of our bot will
+use to communciate to the Discord API.
+
+## Further Steps
+
+- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
+ bloated framework. Feel free to optimize to a better framework.
\ No newline at end of file
diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json
new file mode 100644
index 0000000..23b8976
--- /dev/null
+++ b/packages/bot/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "es2022",
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "allowUnusedLabels": false,
+ "noImplicitOverride": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noUncheckedIndexedAccess": true,
+ "strict": true,
+ "stripInternal": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": false,
+ "allowUnreachableCode": false,
+ "skipLibCheck": true,
+ "moduleResolution": "node"
+ },
+ "include": ["./src/**/*", ".env"],
+ "ts-node": {
+ "esm": true,
+ "experimentalSpecifierResolution": "node",
+ "swc": true
+ }
+}
\ No newline at end of file
diff --git a/packages/link2cid b/packages/link2cid
new file mode 160000
index 0000000..ddc50df
--- /dev/null
+++ b/packages/link2cid
@@ -0,0 +1 @@
+Subproject commit ddc50dff4d48134793300eb5ae8dd32df9e0fdbe
diff --git a/packages/next/app/components/contributors.tsx b/packages/next/app/components/contributors.tsx
index 74f877c..7f0d8d7 100644
--- a/packages/next/app/components/contributors.tsx
+++ b/packages/next/app/components/contributors.tsx
@@ -5,29 +5,36 @@ import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default async function Contributors() {
- const contributors = await getContributors();
- if (!contributors || contributors.length < 1) return (
-
Failed to fetch contributor list
+ } } \ No newline at end of file