failsafe when contributors cant be fetched

This commit is contained in:
Chris Grimmett 2024-02-27 09:11:16 -08:00
parent bd85718107
commit 54991cd386
13 changed files with 837 additions and 25 deletions

1
packages/bot/.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/iron

19
packages/bot/Dockerfile Normal file
View File

@ -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"]

106
packages/bot/components.js Normal file
View File

@ -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();

94
packages/bot/embeds.js Normal file
View File

@ -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();

261
packages/bot/futurebutt.js Normal file
View File

@ -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();

View File

@ -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();

27
packages/bot/package.json Normal file
View File

@ -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"
}
}

View File

@ -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;

58
packages/bot/src/index.ts Normal file
View File

@ -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}`);
});

View File

@ -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.

View File

@ -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
}
}

1
packages/link2cid Submodule

@ -0,0 +1 @@
Subproject commit ddc50dff4d48134793300eb5ae8dd32df9e0fdbe

View File

@ -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 (
<SkeletonTheme baseColor="#000" highlightColor="#000" width="25%">
<Skeleton count={1} enableAnimation={false} />
</SkeletonTheme>
)
const contributorList = contributors.map((contributor, index) => (
<span key={index}>
{contributor.attributes.url ? (
<Link href={contributor.attributes.url} target="_blank">
<span className="mr-1">{contributor.attributes.name}</span>
<FontAwesomeIcon
icon={faExternalLinkAlt}
className="fab fa-external-link-alt"
></FontAwesomeIcon>
</Link>
) : (
contributor.attributes.name
)}
{index !== contributors.length - 1 ? ", " : ""}
</span>
));
return (
<>{contributorList}</>
)
try {
const contributors = await getContributors();
if (!contributors || contributors.length < 1) return (
<SkeletonTheme baseColor="#000" highlightColor="#000" width="25%">
<Skeleton count={1} enableAnimation={false} />
</SkeletonTheme>
)
const contributorList = contributors.map((contributor, index) => (
<span key={index}>
{contributor.attributes.url ? (
<Link href={contributor.attributes.url} target="_blank">
<span className="mr-1">{contributor.attributes.name}</span>
<FontAwesomeIcon
icon={faExternalLinkAlt}
className="fab fa-external-link-alt"
></FontAwesomeIcon>
</Link>
) : (
contributor.attributes.name
)}
{index !== contributors.length - 1 ? ", " : ""}
</span>
));
return (
<>{contributorList}</>
)
} catch (e) {
if (e instanceof Error) {
console.error(e)
}
return <p>Failed to fetch contributor list</p>
}
}