From 54991cd386629e6d8601ddadd2626dc81d1f89bf Mon Sep 17 00:00:00 2001 From: Chris Grimmett Date: Tue, 27 Feb 2024 09:11:16 -0800 Subject: [PATCH] failsafe when contributors cant be fetched --- packages/bot/.nvmrc | 1 + packages/bot/Dockerfile | 19 ++ packages/bot/components.js | 106 +++++++ packages/bot/embeds.js | 94 +++++++ packages/bot/futurebutt.js | 261 ++++++++++++++++++ packages/bot/interactions.js | 171 ++++++++++++ packages/bot/package.json | 27 ++ packages/bot/src/configs.ts | 28 ++ packages/bot/src/index.ts | 58 ++++ packages/bot/src/rest/README.md | 9 + packages/bot/tsconfig.json | 30 ++ packages/link2cid | 1 + packages/next/app/components/contributors.tsx | 57 ++-- 13 files changed, 837 insertions(+), 25 deletions(-) create mode 100644 packages/bot/.nvmrc create mode 100644 packages/bot/Dockerfile create mode 100644 packages/bot/components.js create mode 100644 packages/bot/embeds.js create mode 100644 packages/bot/futurebutt.js create mode 100644 packages/bot/interactions.js create mode 100644 packages/bot/package.json create mode 100644 packages/bot/src/configs.ts create mode 100644 packages/bot/src/index.ts create mode 100644 packages/bot/src/rest/README.md create mode 100644 packages/bot/tsconfig.json create mode 160000 packages/link2cid 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 ( - - - - ) - const contributorList = contributors.map((contributor, index) => ( - - {contributor.attributes.url ? ( - - {contributor.attributes.name} - - - ) : ( - contributor.attributes.name - )} - {index !== contributors.length - 1 ? ", " : ""} - - )); - return ( - <>{contributorList} - ) + try { + const contributors = await getContributors(); + if (!contributors || contributors.length < 1) return ( + + + + ) + const contributorList = contributors.map((contributor, index) => ( + + {contributor.attributes.url ? ( + + {contributor.attributes.name} + + + ) : ( + contributor.attributes.name + )} + {index !== contributors.length - 1 ? ", " : ""} + + )); + return ( + <>{contributorList} + ) + } catch (e) { + if (e instanceof Error) { + console.error(e) + } + return

Failed to fetch contributor list

+ } } \ No newline at end of file