failsafe when contributors cant be fetched
This commit is contained in:
parent
bd85718107
commit
54991cd386
|
@ -0,0 +1 @@
|
||||||
|
lts/iron
|
|
@ -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"]
|
||||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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}`);
|
||||||
|
});
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ddc50dff4d48134793300eb5ae8dd32df9e0fdbe
|
|
@ -5,29 +5,36 @@ import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
export default async function Contributors() {
|
export default async function Contributors() {
|
||||||
const contributors = await getContributors();
|
try {
|
||||||
if (!contributors || contributors.length < 1) return (
|
const contributors = await getContributors();
|
||||||
<SkeletonTheme baseColor="#000" highlightColor="#000" width="25%">
|
if (!contributors || contributors.length < 1) return (
|
||||||
<Skeleton count={1} enableAnimation={false} />
|
<SkeletonTheme baseColor="#000" highlightColor="#000" width="25%">
|
||||||
</SkeletonTheme>
|
<Skeleton count={1} enableAnimation={false} />
|
||||||
)
|
</SkeletonTheme>
|
||||||
const contributorList = contributors.map((contributor, index) => (
|
)
|
||||||
<span key={index}>
|
const contributorList = contributors.map((contributor, index) => (
|
||||||
{contributor.attributes.url ? (
|
<span key={index}>
|
||||||
<Link href={contributor.attributes.url} target="_blank">
|
{contributor.attributes.url ? (
|
||||||
<span className="mr-1">{contributor.attributes.name}</span>
|
<Link href={contributor.attributes.url} target="_blank">
|
||||||
<FontAwesomeIcon
|
<span className="mr-1">{contributor.attributes.name}</span>
|
||||||
icon={faExternalLinkAlt}
|
<FontAwesomeIcon
|
||||||
className="fab fa-external-link-alt"
|
icon={faExternalLinkAlt}
|
||||||
></FontAwesomeIcon>
|
className="fab fa-external-link-alt"
|
||||||
</Link>
|
></FontAwesomeIcon>
|
||||||
) : (
|
</Link>
|
||||||
contributor.attributes.name
|
) : (
|
||||||
)}
|
contributor.attributes.name
|
||||||
{index !== contributors.length - 1 ? ", " : ""}
|
)}
|
||||||
</span>
|
{index !== contributors.length - 1 ? ", " : ""}
|
||||||
));
|
</span>
|
||||||
return (
|
));
|
||||||
<>{contributorList}</>
|
return (
|
||||||
)
|
<>{contributorList}</>
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
return <p>Failed to fetch contributor list</p>
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue