failsafe when contributors cant be fetched
This commit is contained in:
		
							parent
							
								
									bd85718107
								
							
						
					
					
						commit
						54991cd386
					
				
							
								
								
									
										1
									
								
								packages/bot/.nvmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/bot/.nvmrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
lts/iron
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/bot/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/bot/Dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										106
									
								
								packages/bot/components.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										94
									
								
								packages/bot/embeds.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										261
									
								
								packages/bot/futurebutt.js
									
									
									
									
									
										Normal 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();
 | 
			
		||||
							
								
								
									
										171
									
								
								packages/bot/interactions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								packages/bot/interactions.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										27
									
								
								packages/bot/package.json
									
									
									
									
									
										Normal 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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								packages/bot/src/configs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/bot/src/configs.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										58
									
								
								packages/bot/src/index.ts
									
									
									
									
									
										Normal 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}`);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										9
									
								
								packages/bot/src/rest/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/bot/src/rest/README.md
									
									
									
									
									
										Normal 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.
 | 
			
		||||
							
								
								
									
										30
									
								
								packages/bot/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								packages/bot/tsconfig.json
									
									
									
									
									
										Normal 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
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										1
									
								
								packages/link2cid
									
									
									
									
									
										Submodule
									
								
							@ -0,0 +1 @@
 | 
			
		||||
Subproject commit ddc50dff4d48134793300eb5ae8dd32df9e0fdbe
 | 
			
		||||
@ -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>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user