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";
|
||||
|
||||
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…
Reference in New Issue